diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.Designer.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.Designer.cs new file mode 100644 index 00000000..0a45ba86 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.Designer.cs @@ -0,0 +1,742 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CodeBeam.UltimateAuth.IdentityLab.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261031185511_AddProfileFieldLimits")] + partial class AddProfileFieldLimits + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CredentialType") + .HasColumnType("integer"); + + b.Property("FailedAttempts") + .HasColumnType("integer"); + + b.Property("LastFailedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("RequiresReauthentication") + .HasColumnType("boolean"); + + b.Property("ResetAttempts") + .HasColumnType("integer"); + + b.Property("ResetConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetRequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Scope") + .HasColumnType("integer"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("SecurityStamp") + .HasColumnType("uuid"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ActiveSessionId") + .HasColumnType("text"); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Device") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RootId") + .HasColumnType("uuid"); + + b.Property("RotationCount") + .HasColumnType("integer"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TouchCount") + .HasColumnType("integer"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Device") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("bigint"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("uuid"); + + b.Property("SecurityVersion") + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenId") + .HasColumnType("uuid"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityVersion") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.cs new file mode 100644 index 00000000..67d4fada --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.cs @@ -0,0 +1,178 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.IdentityLab.Migrations +{ + /// + public partial class AddProfileFieldLimits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TimeZone", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Language", + table: "UAuth_UserProfiles", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Gender", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FirstName", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "UAuth_UserProfiles", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Culture", + table: "UAuth_UserProfiles", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Bio", + table: "UAuth_UserProfiles", + type: "character varying(4096)", + maxLength: 4096, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TimeZone", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Language", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Gender", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FirstName", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Culture", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Bio", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(4096)", + oldMaxLength: 4096, + oldNullable: true); + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs index 7b6b4d47..381996b7 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs @@ -641,7 +641,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("Bio") - .HasColumnType("text"); + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); b.Property("BirthDate") .HasColumnType("date"); @@ -650,25 +651,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("Culture") - .HasColumnType("text"); + .HasMaxLength(64) + .HasColumnType("character varying(64)"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); b.Property("DisplayName") - .HasColumnType("text"); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("FirstName") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Gender") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Language") - .HasColumnType("text"); + .HasMaxLength(64) + .HasColumnType("character varying(64)"); b.Property("LastName") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Metadata") .HasColumnType("text"); @@ -684,7 +691,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(128)"); b.Property("TimeZone") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); diff --git a/docs/content/plugin-domains/authorization.md b/docs/content/plugin-domains/authorization.md index f5369dbb..8cb2dc89 100644 --- a/docs/content/plugin-domains/authorization.md +++ b/docs/content/plugin-domains/authorization.md @@ -1,6 +1,6 @@ --- title: Authorization -order: 4 +order: 5 group: plugin-domains --- diff --git a/docs/content/plugin-domains/credentials.md b/docs/content/plugin-domains/credentials.md index c9ad7f59..0858f034 100644 --- a/docs/content/plugin-domains/credentials.md +++ b/docs/content/plugin-domains/credentials.md @@ -1,6 +1,6 @@ --- title: Credentials -order: 3 +order: 4 group: plugin-domains --- diff --git a/docs/content/plugin-domains/policies.md b/docs/content/plugin-domains/policies.md index 63c660aa..47ceff9c 100644 --- a/docs/content/plugin-domains/policies.md +++ b/docs/content/plugin-domains/policies.md @@ -1,6 +1,6 @@ --- title: Policies & Access Control -order: 5 +order: 6 group: plugin-domains --- diff --git a/docs/content/plugin-domains/profile-validation-limits.md b/docs/content/plugin-domains/profile-validation-limits.md new file mode 100644 index 00000000..edaa3b70 --- /dev/null +++ b/docs/content/plugin-domains/profile-validation-limits.md @@ -0,0 +1,110 @@ +--- +title: Profile Validation & Limits +order: 3 +group: plugin-domains +--- + +# Profile Validation & Limits + +UltimateAuth enforces framework-level safety limits on standard user profile fields and metadata. + +These limits protect applications from excessively large profile values, unnecessary storage consumption, and resource exhaustion. + +They apply consistently to the reference Users domain, regardless of whether InMemory or Entity Framework Core persistence is used. + +## Profile Field Limits + +The following maximum lengths are enforced: + +| Field | Maximum characters | +|---|---:| +| FirstName | 128 | +| LastName | 128 | +| DisplayName | 256 | +| Gender | 128 | +| Bio | 4,096 | +| Language | 64 | +| TimeZone | 128 | +| Culture | 64 | + +All fields are optional unless an application introduces additional requirements. + +Limits are defined centrally in `UserProfileLimits` and cannot be increased through application configuration. + +## Metadata Limits + +User profiles support optional key-value metadata. + +To prevent unbounded metadata storage, UltimateAuth enforces the following limits: + +| Constraint | Maximum | +|---|---:| +| Metadata entries | 32 | +| Key length | 128 characters | +| Value length | 2,048 characters | +| Total metadata size | 8,192 UTF-8 bytes | + +The total size includes metadata keys and values. + +These limits apply to the complete metadata collection, not individually to each entry. + +## Validation Enforcement + +Profile safety limits are enforced during: + +- User creation. +- Additional profile creation. +- Profile updates. +- Profile cloning. + +Validation occurs before invalid data is persisted. + +Framework-level safety validation is independent of `IUserProfileValidator`, ensuring that replacing the application validator cannot bypass these limits. + +The same restrictions apply to both self-service and administrative operations. + +## Custom Validation + +Applications can implement `IUserProfileValidator` to introduce additional validation rules. + +For example, an application may require a display name, restrict accepted languages, or enforce a shorter biography. + +Custom validation can be stricter than the framework limits but cannot override the built-in safety boundaries. + +The recommended approach is to use `IUserProfileValidator` for application-specific rules while relying on UltimateAuth for baseline safety validation. + +## Validation Errors + +When a profile exceeds the supported limits, UltimateAuth rejects the operation with a validation error. + +Common error codes include: + +| Error code | Meaning | +|---|---| +| `profile_field_too_long` | A standard profile field exceeds its maximum length. | +| `profile_metadata_entries_exceeded` | The metadata collection contains too many entries. | +| `profile_metadata_key_too_long` | A metadata key exceeds its maximum length. | +| `profile_metadata_value_too_long` | A metadata value exceeds its maximum length. | +| `profile_metadata_too_large` | The combined metadata size exceeds the allowed limit. | + +Applications should use the error `Code` rather than relying on exception message text. + +## Persistence Considerations + +The reference Entity Framework Core implementation configures profile column lengths using the same framework limits. + +Existing databases may require a migration when upgrading to a version that introduces these constraints. + +Before applying restrictive database changes, review existing profile records for values exceeding the new limits. + +The InMemory implementation uses the same application-level validation rules. + +## Security Considerations + +Profile limits are a defense-in-depth measure. + +They do not replace HTTP request size limits, rate limiting, or application-specific validation. + +Applications should configure these additional protections according to their deployment requirements. + +For an overview of user identity and profile management, see [Users Domain](/docs/plugin-domains/users). \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json index a2d1d0fc..ab0acd2b 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json @@ -132,24 +132,31 @@ "Group": "plugin-domains", "GroupOrder": 5 }, + { + "Title": "Profile Validation \u0026 Limits", + "Slug": "plugin-domains/profile-validation-limits", + "Order": 3, + "Group": "plugin-domains", + "GroupOrder": 5 + }, { "Title": "Credentials", "Slug": "plugin-domains/credentials", - "Order": 3, + "Order": 4, "Group": "plugin-domains", "GroupOrder": 5 }, { "Title": "Authorization", "Slug": "plugin-domains/authorization", - "Order": 4, + "Order": 5, "Group": "plugin-domains", "GroupOrder": 5 }, { "Title": "Policies \u0026 Access Control", "Slug": "plugin-domains/policies", - "Order": 5, + "Order": 6, "Group": "plugin-domains", "GroupOrder": 5 }, diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json index a8d97bf2..43d6f13d 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json @@ -154,6 +154,6 @@ "Level": 0 } ], - "SourceHash": "E8E9478440BD9758F067833BE9E8597AEE9A25F0870CB9416A4390F8674A8D07", + "SourceHash": "D39724293797A9792171FDEB55D849FA5D3845BF2E1B2413F10EC86A39B92123", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json index 6e4d4195..5b33614a 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json @@ -109,6 +109,6 @@ "Level": 0 } ], - "SourceHash": "F0E1E773A693F7405C202D30CDB37AF1154B0EA97BCE385786157DF1B6E20D24", + "SourceHash": "8618A9ED0DF478D9B489CBC7C51420145259EB1DBA1956617ECF3600D163BF2F", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json index 0251d021..02ccc7b1 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json @@ -159,6 +159,6 @@ "Level": 0 } ], - "SourceHash": "05FA98F9ED56849BAB9A4450CA6B85F273091D8A4F1422F7238421375E5DCA62", + "SourceHash": "DFAC894A519ADC862CC8E8B5450A17DC934DAAAB2004C631BFD9594CC7AA37E4", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/profile-validation-limits.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/profile-validation-limits.json new file mode 100644 index 00000000..3750624d --- /dev/null +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/profile-validation-limits.json @@ -0,0 +1,44 @@ +{ + "Slug": "plugin-domains/profile-validation-limits", + "Title": "Profile Validation \u0026 Limits", + "Html": "\n\u003Cp\u003EUltimateAuth enforces framework-level safety limits on standard user profile fields and metadata.\u003C/p\u003E\n\u003Cp\u003EThese limits protect applications from excessively large profile values, unnecessary storage consumption, and resource exhaustion.\u003C/p\u003E\n\u003Cp\u003EThey apply consistently to the reference Users domain, regardless of whether InMemory or Entity Framework Core persistence is used.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-field-limits\u0022\u003EProfile Field Limits\u003C/h2\u003E\n\u003Cp\u003EThe following maximum lengths are enforced:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EField\u003C/th\u003E\n\u003Cth style=\u0022text-align: right;\u0022\u003EMaximum characters\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EFirstName\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ELastName\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDisplayName\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E256\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EGender\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBio\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E4,096\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ELanguage\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E64\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETimeZone\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECulture\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E64\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAll fields are optional unless an application introduces additional requirements.\u003C/p\u003E\n\u003Cp\u003ELimits are defined centrally in \u003Ccode\u003EUserProfileLimits\u003C/code\u003E and cannot be increased through application configuration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022metadata-limits\u0022\u003EMetadata Limits\u003C/h2\u003E\n\u003Cp\u003EUser profiles support optional key-value metadata.\u003C/p\u003E\n\u003Cp\u003ETo prevent unbounded metadata storage, UltimateAuth enforces the following limits:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConstraint\u003C/th\u003E\n\u003Cth style=\u0022text-align: right;\u0022\u003EMaximum\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMetadata entries\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E32\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EKey length\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128 characters\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EValue length\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E2,048 characters\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETotal metadata size\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E8,192 UTF-8 bytes\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe total size includes metadata keys and values.\u003C/p\u003E\n\u003Cp\u003EThese limits apply to the complete metadata collection, not individually to each entry.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-enforcement\u0022\u003EValidation Enforcement\u003C/h2\u003E\n\u003Cp\u003EProfile safety limits are enforced during:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EUser creation.\u003C/li\u003E\n\u003Cli\u003EAdditional profile creation.\u003C/li\u003E\n\u003Cli\u003EProfile updates.\u003C/li\u003E\n\u003Cli\u003EProfile cloning.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EValidation occurs before invalid data is persisted.\u003C/p\u003E\n\u003Cp\u003EFramework-level safety validation is independent of \u003Ccode\u003EIUserProfileValidator\u003C/code\u003E, ensuring that replacing the application validator cannot bypass these limits.\u003C/p\u003E\n\u003Cp\u003EThe same restrictions apply to both self-service and administrative operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-validation\u0022\u003ECustom Validation\u003C/h2\u003E\n\u003Cp\u003EApplications can implement \u003Ccode\u003EIUserProfileValidator\u003C/code\u003E to introduce additional validation rules.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may require a display name, restrict accepted languages, or enforce a shorter biography.\u003C/p\u003E\n\u003Cp\u003ECustom validation can be stricter than the framework limits but cannot override the built-in safety boundaries.\u003C/p\u003E\n\u003Cp\u003EThe recommended approach is to use \u003Ccode\u003EIUserProfileValidator\u003C/code\u003E for application-specific rules while relying on UltimateAuth for baseline safety validation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-errors\u0022\u003EValidation Errors\u003C/h2\u003E\n\u003Cp\u003EWhen a profile exceeds the supported limits, UltimateAuth rejects the operation with a validation error.\u003C/p\u003E\n\u003Cp\u003ECommon error codes include:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EError code\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_field_too_long\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA standard profile field exceeds its maximum length.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_entries_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe metadata collection contains too many entries.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_key_too_long\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA metadata key exceeds its maximum length.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_value_too_long\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA metadata value exceeds its maximum length.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_too_large\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe combined metadata size exceeds the allowed limit.\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EApplications should use the error \u003Ccode\u003ECode\u003C/code\u003E rather than relying on exception message text.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022persistence-considerations\u0022\u003EPersistence Considerations\u003C/h2\u003E\n\u003Cp\u003EThe reference Entity Framework Core implementation configures profile column lengths using the same framework limits.\u003C/p\u003E\n\u003Cp\u003EExisting databases may require a migration when upgrading to a version that introduces these constraints.\u003C/p\u003E\n\u003Cp\u003EBefore applying restrictive database changes, review existing profile records for values exceeding the new limits.\u003C/p\u003E\n\u003Cp\u003EThe InMemory implementation uses the same application-level validation rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h2\u003E\n\u003Cp\u003EProfile limits are a defense-in-depth measure.\u003C/p\u003E\n\u003Cp\u003EThey do not replace HTTP request size limits, rate limiting, or application-specific validation.\u003C/p\u003E\n\u003Cp\u003EApplications should configure these additional protections according to their deployment requirements.\u003C/p\u003E\n\u003Cp\u003EFor an overview of user identity and profile management, see \u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers Domain\u003C/a\u003E.\u003C/p\u003E\n", + "Headings": [ + { + "Id": "profile-field-limits", + "Text": "Profile Field Limits", + "Level": 0 + }, + { + "Id": "metadata-limits", + "Text": "Metadata Limits", + "Level": 0 + }, + { + "Id": "validation-enforcement", + "Text": "Validation Enforcement", + "Level": 0 + }, + { + "Id": "custom-validation", + "Text": "Custom Validation", + "Level": 0 + }, + { + "Id": "validation-errors", + "Text": "Validation Errors", + "Level": 0 + }, + { + "Id": "persistence-considerations", + "Text": "Persistence Considerations", + "Level": 0 + }, + { + "Id": "security-considerations", + "Text": "Security Considerations", + "Level": 0 + } + ], + "SourceHash": "5F102B8246218017DC2F9A89EB40C588773A804D661CE3C97625DA31C0461590", + "BuilderVersion": "1" +} \ No newline at end of file diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor index 140615ad..87dbc04d 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor +++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor @@ -1,4 +1,5 @@ -@inject IUAuthClient UAuthClient +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient @inject ISnackbar Snackbar @@ -11,9 +12,7 @@
Edit Profile - - Update identity details and personal preferences. - + Update identity details and personal preferences.
@@ -21,8 +20,7 @@ @if (_isLoading) { - + } else if (_loadError is not null) { @@ -36,9 +34,7 @@
@if (PreviewMode) { - + Preview mode is active. Profile changes cannot be saved. } @@ -51,9 +47,7 @@
- +
Basic Information How this identity appears throughout the application. @@ -65,12 +59,18 @@ Label="First Name" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.FirstName" + Counter="@UserProfileLimits.FirstName" + Immediate="true" Disabled="@_isSaving" />
@@ -78,6 +78,9 @@ Label="Display Name" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.DisplayName" + Counter="@UserProfileLimits.DisplayName" + Immediate="true" Disabled="@_isSaving" />
@@ -87,9 +90,7 @@
- +
Personal Details Optional information associated with this profile. @@ -107,6 +108,9 @@ Label="Gender" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.Gender" + Counter="@UserProfileLimits.Gender" + Immediate="true" Disabled="@_isSaving" />
@@ -115,6 +119,9 @@ Lines="3" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.Bio" + Counter="@UserProfileLimits.Bio" + Immediate="true" Disabled="@_isSaving" />
@@ -124,9 +131,7 @@
- +
Localization Language, time zone and culture preferences. @@ -139,6 +144,9 @@ Placeholder="e.g. en" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.Language" + Counter="@UserProfileLimits.Language" + Immediate="true" Disabled="@_isSaving" />
@@ -155,6 +166,9 @@ Variant="Variant.Outlined" Margin="Margin.Dense" SearchBox="true" + MaxLength="@UserProfileLimits.TimeZone" + Counter="@UserProfileLimits.TimeZone" + Immediate="true" Disabled="@_isSaving"> @foreach (var tz in _timeZones) { @@ -173,20 +187,12 @@
- + Cancel - + Save Changes
diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.Designer.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.Designer.cs new file mode 100644 index 00000000..d3d07c21 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.Designer.cs @@ -0,0 +1,729 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261031185108_AddProfileFieldLimits")] + partial class AddProfileFieldLimits + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.cs new file mode 100644 index 00000000..64065857 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + /// + public partial class AddProfileFieldLimits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs index de4aec39..5e2dd6f8 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs @@ -628,6 +628,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Bio") + .HasMaxLength(4096) .HasColumnType("TEXT"); b.Property("BirthDate") @@ -637,24 +638,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Culture") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("DeletedAt") .HasColumnType("TEXT"); b.Property("DisplayName") + .HasMaxLength(256) .HasColumnType("TEXT"); b.Property("FirstName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Gender") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Language") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("LastName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Metadata") @@ -671,6 +678,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("TimeZone") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("UpdatedAt") diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.Designer.cs new file mode 100644 index 00000000..c81fe3d6 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.Designer.cs @@ -0,0 +1,729 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261031184917_AddProfileFieldLimits")] + partial class AddProfileFieldLimits + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.cs new file mode 100644 index 00000000..31574eaa --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + /// + public partial class AddProfileFieldLimits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs index 3e5045d5..ff7c7c41 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs @@ -628,6 +628,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Bio") + .HasMaxLength(4096) .HasColumnType("TEXT"); b.Property("BirthDate") @@ -637,24 +638,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Culture") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("DeletedAt") .HasColumnType("TEXT"); b.Property("DisplayName") + .HasMaxLength(256) .HasColumnType("TEXT"); b.Property("FirstName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Gender") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Language") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("LastName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Metadata") @@ -671,6 +678,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("TimeZone") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("UpdatedAt") diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm index cec5825e..38ab800e 100644 Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm differ diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal index 80ea6afe..c3298258 100644 Binary files a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal and b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-wal differ diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs new file mode 100644 index 00000000..1a6b4e48 --- /dev/null +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs @@ -0,0 +1,76 @@ +using System.Text; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Errors; +using CodeBeam.UltimateAuth.Users.Contracts; + +namespace CodeBeam.UltimateAuth.Server.Infrastructure; + +public static class UserProfileSafetyValidation +{ + public static IReadOnlyList Validate(UserProfileInfo profile) + { + ArgumentNullException.ThrowIfNull(profile); + + var errors = new List(); + + Check(profile.FirstName, UserProfileLimits.FirstName, "first_name"); + Check(profile.LastName, UserProfileLimits.LastName, "last_name"); + Check(profile.DisplayName, UserProfileLimits.DisplayName, "display_name"); + Check(profile.Gender, UserProfileLimits.Gender, "gender"); + Check(profile.Bio, UserProfileLimits.Bio, "bio"); + Check(profile.Language, UserProfileLimits.Language, "language"); + Check(profile.TimeZone, UserProfileLimits.TimeZone, "time_zone"); + Check(profile.Culture, UserProfileLimits.Culture, "culture"); + + if (profile.Metadata is { } metadata) + { + if (metadata.Count > UserProfileLimits.MetadataEntries) + { + errors.Add(new UAuthValidationError("profile_metadata_entries_exceeded", "metadata")); + } + + long totalBytes = 0; + + foreach (var (key, value) in metadata) + { + if (key.Length > UserProfileLimits.MetadataKey) + { + errors.Add(new UAuthValidationError("profile_metadata_key_too_long" ,"metadata")); + } + + if (value is null || value.Length > UserProfileLimits.MetadataValue) + { + errors.Add(new UAuthValidationError("profile_metadata_value_too_long", "metadata")); + } + + totalBytes += Encoding.UTF8.GetByteCount(key); + + if (value is not null) + totalBytes += Encoding.UTF8.GetByteCount(value); + } + + if (totalBytes > UserProfileLimits.MetadataTotalUtf8Bytes) + { + errors.Add(new UAuthValidationError("profile_metadata_too_large", "metadata")); + } + } + + return errors; + + void Check(string? value, int maxLength, string field) + { + if (value is not null && value.Length > maxLength) + { + errors.Add(new UAuthValidationError("profile_field_too_long", field)); + } + } + } + + public static void EnsureValid(UserProfileInfo profile) + { + var errors = Validate(profile); + + if (errors.Count > 0) + throw new UAuthValidationException(errors[0].Code); + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs index 7d9ac809..22d948e7 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs @@ -9,6 +9,10 @@ public Task ValidateAsync(AccessContext context, Us { ct.ThrowIfCancellationRequested(); - return Task.FromResult(UserProfileValidationResult.Success()); + var errors = UserProfileSafetyValidation.Validate(profile); + + return Task.FromResult(errors.Count == 0 + ? UserProfileValidationResult.Success() + : UserProfileValidationResult.Failed(errors)); } } diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs index 934d33a0..967b1055 100644 --- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs @@ -1,9 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; namespace CodeBeam.UltimateAuth.Server.MultiTenancy; diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileLimits.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileLimits.cs new file mode 100644 index 00000000..237530d9 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileLimits.cs @@ -0,0 +1,24 @@ +namespace CodeBeam.UltimateAuth.Users.Contracts; + +/// +/// Framework-level safety limits for user profile content. +/// Applications may enforce stricter limits but cannot increase these limits. +/// +public static class UserProfileLimits +{ + public const int FirstName = 128; + public const int LastName = 128; + public const int DisplayName = 256; + + public const int Gender = 128; + public const int Bio = 4096; + + public const int Language = 64; + public const int TimeZone = 128; + public const int Culture = 64; + + public const int MetadataEntries = 32; + public const int MetadataKey = 128; + public const int MetadataValue = 2048; + public const int MetadataTotalUtf8Bytes = 8192; +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs index f011d9ce..bd2c0fa5 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs @@ -117,6 +117,15 @@ private static void ConfigureProfiles(ModelBuilder b) .HasMaxLength(64) .IsRequired(); + e.Property(x => x.FirstName).HasMaxLength(UserProfileLimits.FirstName); + e.Property(x => x.LastName).HasMaxLength(UserProfileLimits.LastName); + e.Property(x => x.DisplayName).HasMaxLength(UserProfileLimits.DisplayName); + e.Property(x => x.Gender).HasMaxLength(UserProfileLimits.Gender); + e.Property(x => x.Bio).HasMaxLength(UserProfileLimits.Bio); + e.Property(x => x.Language).HasMaxLength(UserProfileLimits.Language); + e.Property(x => x.TimeZone).HasMaxLength(UserProfileLimits.TimeZone); + e.Property(x => x.Culture).HasMaxLength(UserProfileLimits.Culture); + e.Property(x => x.Metadata) .HasConversion(new NullableJsonValueConverter>()) .Metadata.SetValueComparer(JsonValueComparers.Create>()); @@ -128,4 +137,4 @@ private static void ConfigureProfiles(ModelBuilder b) e.HasIndex(x => new { x.Tenant, x.UserKey, x.ProfileKey }).IsUnique(); }); } -} \ No newline at end of file +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index 41ac9a69..cf5e6bb1 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -76,6 +76,20 @@ public async Task CreateUserAsync(AccessContext context, Creat throw new UAuthValidationException(string.Join(", ", validationResult.Errors)); } + UserProfileSafetyValidation.EnsureValid(new UserProfileInfo + { + FirstName = request.FirstName, + LastName = request.LastName, + DisplayName = request.DisplayName ?? request.UserName ?? request.Email ?? request.Phone, + BirthDate = request.BirthDate, + Gender = request.Gender, + Bio = request.Bio, + Language = request.Language, + TimeZone = request.TimeZone, + Culture = request.Culture, + Metadata = request.Metadata + }); + EnsureCreateUserIdentifierLimits(request); return await _atomicExecutor.ExecuteAsync( @@ -466,6 +480,7 @@ public async Task CreateProfileAsync(AccessContext context, CreateProfileRequest metadata: request.Metadata); } + UserProfileSafetyValidation.EnsureValid(profile.ToDto()); await store.AddAsync(profile, innerCt); }); @@ -491,6 +506,21 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq if (!_options.UserProfile.EnableMultiProfile && profileKey != ProfileKey.Default) throw new UAuthConflictException("multi_profile_disabled"); + UserProfileSafetyValidation.EnsureValid(new UserProfileInfo + { + ProfileKey = profileKey, + FirstName = request.FirstName, + LastName = request.LastName, + DisplayName = request.DisplayName, + BirthDate = request.BirthDate, + Gender = request.Gender, + Bio = request.Bio, + Language = request.Language, + TimeZone = request.TimeZone, + Culture = request.Culture, + Metadata = request.Metadata + }); + var expectedVersion = profile.Version; profile diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs index e34d512c..05dc040f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs @@ -20,8 +20,7 @@ namespace CodeBeam.UltimateAuth.Tests.Unit.Users; public sealed class UserApplicationServiceTests { - private static readonly DateTimeOffset Now = - new(2026, 9, 21, 12, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset Now = new(2026, 9, 21, 12, 0, 0, TimeSpan.Zero); // ============================================================ // Create @@ -2210,6 +2209,153 @@ public async Task CreateUserAsync_WithDefaultLimit_AllowsThreeInitialIdentifiers Times.Exactly(3)); } + [Fact] + public async Task CreateUserAsync_WhenProfileFieldExceedsLimit_DoesNotPersist() + { + var f = CreateFixture(); + var context = CreateContext(); + + var request = new CreateUserRequest + { + UserName = "alice", + FirstName = new string('x', UserProfileLimits.FirstName + 1) + }; + + // Simulate an application validator that accepts the request. + SetupValidCreate(f, context, request); + + var act = () => f.Sut.CreateUserAsync(context, request); + + var exception = await act.Should().ThrowAsync(); + exception.Which.Code.Should().Be("profile_field_too_long"); + + f.LifecycleStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.ProfileStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.IdentifierStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.Atomic.Verify( + x => x.ExecuteAsync( + It.IsAny>>(), + It.IsAny()), + Times.Never); + } + + [Fact] + public async Task CreateProfileAsync_WhenMetadataExceedsLimit_DoesNotPersist() + { + var f = CreateFixtureWithOptions(options => + { + options.UserProfile.EnableMultiProfile = true; + }); + + var userKey = UserKey.New(); + + var context = CreateContext( + actorUserKey: userKey, + targetUserKey: userKey); + + var profileKey = ProfileKey.Parse("secondary", null); + + var request = new CreateProfileRequest + { + ProfileKey = profileKey, + Metadata = Enumerable + .Range(0, UserProfileLimits.MetadataEntries + 1) + .ToDictionary( + x => $"key{x}", + _ => "value") + }; + + f.ProfileStore + .Setup(x => x.ExistsAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(false); + + var act = () => f.Sut.CreateProfileAsync(context, request); + + var exception = await act.Should().ThrowAsync(); + exception.Which.Code.Should().Be("profile_metadata_entries_exceeded"); + + f.ProfileStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public async Task UpdateUserProfileAsync_WhenFieldExceedsLimit_DoesNotMutateOrPersist() + { + var f = CreateFixture(); + + var userKey = UserKey.New(); + + var context = CreateContext( + actorUserKey: userKey, + targetUserKey: userKey); + + var profile = UserProfile.Create( + Guid.NewGuid(), + context.ResourceTenant, + userKey, + ProfileKey.Default, + Now.AddDays(-1), + firstName: "Alice", + lastName: "Smith", + displayName: "Alice Smith"); + + var originalVersion = profile.Version; + var originalUpdatedAt = profile.UpdatedAt; + + f.ProfileStore + .Setup(x => x.GetAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(profile); + + var request = new UpdateProfileRequest + { + FirstName = new string('x', UserProfileLimits.FirstName + 1), + LastName = "Changed", + DisplayName = "Changed Name" + }; + + var act = () => f.Sut.UpdateUserProfileAsync(context, request); + + var exception = await act.Should().ThrowAsync(); + exception.Which.Code.Should().Be("profile_field_too_long"); + + // The existing tracked entity must remain unchanged. + profile.FirstName.Should().Be("Alice"); + profile.LastName.Should().Be("Smith"); + profile.DisplayName.Should().Be("Alice Smith"); + + profile.Version.Should().Be(originalVersion); + profile.UpdatedAt.Should().Be(originalUpdatedAt); + + f.ProfileStore.Verify( + x => x.SaveAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + // ============================================================ // Helpers // ============================================================ @@ -2588,4 +2734,4 @@ private static UserIdentifier CreateIdentifier( deletedAt: null, version: version); } -} \ No newline at end of file +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileSafetyValidationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileSafetyValidationTests.cs new file mode 100644 index 00000000..2d359ad8 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileSafetyValidationTests.cs @@ -0,0 +1,135 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Users.Contracts; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public sealed class UserProfileSafetyValidationTests +{ + [Theory] + [InlineData(nameof(UserProfileInfo.FirstName), UserProfileLimits.FirstName)] + [InlineData(nameof(UserProfileInfo.LastName), UserProfileLimits.LastName)] + [InlineData(nameof(UserProfileInfo.DisplayName), UserProfileLimits.DisplayName)] + [InlineData(nameof(UserProfileInfo.Gender), UserProfileLimits.Gender)] + [InlineData(nameof(UserProfileInfo.Bio), UserProfileLimits.Bio)] + [InlineData(nameof(UserProfileInfo.Language), UserProfileLimits.Language)] + [InlineData(nameof(UserProfileInfo.TimeZone), UserProfileLimits.TimeZone)] + [InlineData(nameof(UserProfileInfo.Culture), UserProfileLimits.Culture)] + public void ProfileFields_AtLimit_AreAccepted(string property, int maxLength) + { + var profile = new UserProfileInfo(); + + typeof(UserProfileInfo) + .GetProperty(property)! + .SetValue(profile, new string('x', maxLength)); + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().BeEmpty(); + } + + [Theory] + [InlineData(nameof(UserProfileInfo.FirstName), UserProfileLimits.FirstName)] + [InlineData(nameof(UserProfileInfo.LastName), UserProfileLimits.LastName)] + [InlineData(nameof(UserProfileInfo.DisplayName), UserProfileLimits.DisplayName)] + [InlineData(nameof(UserProfileInfo.Gender), UserProfileLimits.Gender)] + [InlineData(nameof(UserProfileInfo.Bio), UserProfileLimits.Bio)] + [InlineData(nameof(UserProfileInfo.Language), UserProfileLimits.Language)] + [InlineData(nameof(UserProfileInfo.TimeZone), UserProfileLimits.TimeZone)] + [InlineData(nameof(UserProfileInfo.Culture), UserProfileLimits.Culture)] + public void ProfileFields_OverLimit_AreRejected(string property, int maxLength) + { + var profile = new UserProfileInfo(); + + typeof(UserProfileInfo) + .GetProperty(property)! + .SetValue(profile, new string('x', maxLength + 1)); + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().ContainSingle() + .Which.Code.Should().Be("profile_field_too_long"); + } + + [Fact] + public void Metadata_TooManyEntries_IsRejected() + { + var metadata = Enumerable + .Range(0, UserProfileLimits.MetadataEntries + 1) + .ToDictionary(x => $"key{x}", _ => "value"); + + var profile = new UserProfileInfo + { + Metadata = metadata + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_entries_exceeded"); + } + + [Fact] + public void Metadata_KeyTooLong_IsRejected() + { + var profile = new UserProfileInfo + { + Metadata = new Dictionary + { + [new string('k', UserProfileLimits.MetadataKey + 1)] = "value" + } + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_key_too_long"); + } + + [Fact] + public void Metadata_ValueTooLong_IsRejected() + { + var profile = new UserProfileInfo + { + Metadata = new Dictionary + { + ["key"] = new string( + 'v', + UserProfileLimits.MetadataValue + 1) + } + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_value_too_long"); + } + + [Fact] + public void Metadata_TotalUtf8SizeExceeded_IsRejected() + { + var metadata = Enumerable.Range(0, 5) + .ToDictionary( + x => $"key{x}", + _ => new string('é', 1000)); + + var profile = new UserProfileInfo + { + Metadata = metadata + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_too_large"); + } + + [Fact] + public void EmptyProfile_IsValid() + { + var errors = UserProfileSafetyValidation.Validate( + new UserProfileInfo()); + + errors.Should().BeEmpty(); + } +}