feat: harden SaaS authentication and authorization

This commit is contained in:
2026-07-28 12:15:51 +08:00
parent f22f329d33
commit 5d2248efee
123 changed files with 9090 additions and 2822 deletions

View File

@@ -9,22 +9,30 @@ internal sealed class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ConfigureEntity("users");
builder.ToTable("users");
builder.HasKey(entity => entity.Id);
builder.Property(entity => entity.Id).HasDefaultValueSql("gen_random_uuid()");
builder.ConfigureTimestamps();
builder.Property(entity => entity.LegacyId).HasMaxLength(64);
builder.Property(entity => entity.Username).HasMaxLength(100);
builder.Property(entity => entity.UserName).HasMaxLength(100);
builder.Property(entity => entity.NormalizedUserName).HasMaxLength(100);
builder.Property(entity => entity.Email).HasColumnType("citext").HasMaxLength(320);
builder.Property(entity => entity.NormalizedEmail).HasMaxLength(320);
builder.Property(entity => entity.Phone).HasMaxLength(32);
builder.Property(entity => entity.PhoneNumber).HasMaxLength(32);
builder.Property(entity => entity.PasswordHash).HasMaxLength(1024);
builder.Property(entity => entity.SecurityStamp).HasMaxLength(64);
builder.Property(entity => entity.ConcurrencyStamp).HasMaxLength(64).IsConcurrencyToken();
builder.Property(entity => entity.Name).HasMaxLength(200);
builder.Property(entity => entity.AvatarUrl).HasMaxLength(2048);
builder.Property(entity => entity.PrimaryRole).HasMaxLength(50);
builder.Property(entity => entity.LegacyPasswordHash).HasMaxLength(512);
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.RawProfile).IsJson("{}");
builder.HasIndex(entity => entity.LegacyId).IsUnique();
builder.HasIndex(entity => entity.Username).IsUnique();
builder.HasIndex(entity => entity.Email).IsUnique();
builder.HasIndex(entity => entity.NormalizedUserName).IsUnique();
builder.HasIndex(entity => entity.NormalizedEmail);
builder.HasIndex(entity => entity.Phone).IsUnique();
}
}
@@ -42,8 +50,6 @@ internal sealed class UserIdentityConfiguration : IEntityTypeConfiguration<UserI
builder.Property(entity => entity.OpenId).HasMaxLength(255);
builder.Property(entity => entity.Phone).HasMaxLength(32);
builder.Property(entity => entity.Email).HasColumnType("citext").HasMaxLength(320);
builder.Property(entity => entity.SecretPayload).IsJson("{}");
builder.HasIndex(entity => new { entity.Provider, entity.ProviderSubject }).IsUnique();
builder.HasOne<User>()
.WithMany()

View File

@@ -43,7 +43,6 @@ internal sealed class TenantMembershipConfiguration : IEntityTypeConfiguration<T
builder.Property(entity => entity.Role).HasSnakeCaseEnum();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.Permissions).IsJson("{}");
builder.Property(entity => entity.LegacyRole).HasMaxLength(50);
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.Role }).IsUnique();
@@ -52,11 +51,6 @@ internal sealed class TenantMembershipConfiguration : IEntityTypeConfiguration<T
.WithMany()
.HasForeignKey(entity => entity.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<TenantRoleTemplate>()
.WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.RoleTemplateId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@@ -51,7 +51,6 @@ internal sealed class SmsVerificationCodeConfiguration : IEntityTypeConfiguratio
public void Configure(EntityTypeBuilder<SmsVerificationCode> builder)
{
builder.ConfigureEntity("sms_verification_codes");
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
builder.Property(entity => entity.Phone).HasMaxLength(32);
builder.Property(entity => entity.Purpose).HasSnakeCaseEnum();
builder.Property(entity => entity.CodeHash).HasMaxLength(256);
@@ -96,7 +95,7 @@ internal sealed class AuthLoginEventConfiguration : IEntityTypeConfiguration<Aut
builder.HasOne<Tenant>().WithMany()
.HasForeignKey(entity => entity.TenantId)
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.SetNull);
builder.HasOne<User>().WithMany()
.HasForeignKey(entity => entity.UserId)
.OnDelete(DeleteBehavior.SetNull);
@@ -107,18 +106,30 @@ internal sealed class AuthSessionConfiguration : IEntityTypeConfiguration<AuthSe
{
public void Configure(EntityTypeBuilder<AuthSession> builder)
{
builder.ConfigureTenantEntity("auth_sessions");
builder.ConfigureEntity("auth_sessions");
builder.ConfigureTimestamps();
builder.Property(entity => entity.Realm).HasSnakeCaseEnum();
builder.Property(entity => entity.TokenHash).HasMaxLength(256);
builder.Property(entity => entity.SecurityStamp).HasMaxLength(128);
builder.Property(entity => entity.Provider).HasMaxLength(50);
builder.Property(entity => entity.RevokedReason).HasMaxLength(100);
builder.Property(entity => entity.IpAddress).HasMaxLength(64);
builder.Property(entity => entity.UserAgent).HasMaxLength(1000);
builder.Property(entity => entity.Metadata).IsJson("{}");
builder.HasIndex(entity => entity.TokenHash)
.IsUnique()
.HasAnnotation("Tiku:GlobalUnique", true);
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.ExpiresAt })
builder.HasIndex(entity => new { entity.Realm, entity.TenantId, entity.UserId, entity.ExpiresAt })
.HasFilter("revoked_at is null");
builder.HasIndex(entity => new { entity.TokenFamilyId, entity.RevokedAt });
builder.ToTable(table => table.HasCheckConstraint(
"ck_auth_sessions_realm_tenant",
"(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)"));
builder.HasOne<Tenant>().WithMany()
.HasForeignKey(entity => entity.TenantId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<User>().WithMany()
.HasForeignKey(entity => entity.UserId)
@@ -126,6 +137,29 @@ internal sealed class AuthSessionConfiguration : IEntityTypeConfiguration<AuthSe
}
}
internal sealed class AuthChallengeConfiguration : IEntityTypeConfiguration<AuthChallenge>
{
public void Configure(EntityTypeBuilder<AuthChallenge> builder)
{
builder.ConfigureEntity("auth_challenges");
builder.Property(entity => entity.Realm).HasSnakeCaseEnum();
builder.Property(entity => entity.Purpose).HasSnakeCaseEnum();
builder.Property(entity => entity.TokenHash).HasMaxLength(64);
builder.Property(entity => entity.SecurityStamp).HasMaxLength(128);
builder.Property(entity => entity.Provider).HasMaxLength(50);
builder.Property(entity => entity.IpAddress).HasMaxLength(100);
builder.Property(entity => entity.UserAgent).HasMaxLength(1024);
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => entity.TokenHash).IsUnique();
builder.HasIndex(entity => new { entity.UserId, entity.Purpose, entity.ExpiresAt });
builder.ToTable(table => table.HasCheckConstraint(
"ck_auth_challenges_realm_tenant",
"(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)"));
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UserId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class SmsSendRateLimitConfiguration : IEntityTypeConfiguration<SmsSendRateLimit>
{
public void Configure(EntityTypeBuilder<SmsSendRateLimit> builder)
@@ -154,32 +188,6 @@ internal sealed class SmsSendRateLimitConfiguration : IEntityTypeConfiguration<S
}
}
internal sealed class TenantRoleTemplateConfiguration : IEntityTypeConfiguration<TenantRoleTemplate>
{
public void Configure(EntityTypeBuilder<TenantRoleTemplate> builder)
{
builder.ConfigureTenantEntity("tenant_role_templates");
builder.ConfigureTimestamps();
builder.Property(entity => entity.Code).HasMaxLength(100);
builder.Property(entity => entity.Name).HasMaxLength(200);
builder.Property(entity => entity.BaseRole).HasSnakeCaseEnum();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.Permissions).IsJson("{}");
builder.Property(entity => entity.MenuPermissions).IsJson("{}");
builder.Property(entity => entity.ModulePermissions).IsJson("{}");
builder.Property(entity => entity.FieldPermissions).IsJson("{}");
builder.Property(entity => entity.DataScope).IsJson("{}");
builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.SortOrder });
builder.HasOne<User>().WithMany()
.HasForeignKey(entity => entity.CreatedBy)
.OnDelete(DeleteBehavior.SetNull);
builder.HasOne<User>().WithMany()
.HasForeignKey(entity => entity.UpdatedBy)
.OnDelete(DeleteBehavior.SetNull);
}
}
internal sealed class TenantClassConfiguration : IEntityTypeConfiguration<TenantClass>
{
public void Configure(EntityTypeBuilder<TenantClass> builder)