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

@@ -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)