feat: enforce tenant isolation and shared question bank

This commit is contained in:
2026-07-27 16:59:12 +08:00
parent 28e9a9fa41
commit db4c7b4496
137 changed files with 6402 additions and 112274 deletions

View File

@@ -182,7 +182,7 @@ internal sealed class ContentImportItemConfiguration : IEntityTypeConfiguration<
builder.Property(entity => entity.SourcePayload).IsJson("{}");
builder.Property(entity => entity.NormalizedPayload).IsJson("{}");
builder.Property(entity => entity.ContentHash).HasMaxLength(128);
builder.HasIndex(entity => new { entity.JobId, entity.RowNo }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.JobId, entity.RowNo }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.JobId, entity.Status, entity.RowNo });
builder.ToTable(table =>
{

View File

@@ -134,7 +134,7 @@ internal sealed class PaymentConfiguration : IEntityTypeConfiguration<Payment>
builder.Property(entity => entity.RawPayload).IsJson("{}");
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.OrderId, entity.UpdatedAt });
builder.HasIndex(entity => new { entity.Provider, entity.ProviderTradeNo })
builder.HasIndex(entity => new { entity.TenantId, entity.Provider, entity.ProviderTradeNo })
.IsUnique()
.HasFilter("provider_trade_no is not null");
builder.ToTable(table =>
@@ -160,7 +160,7 @@ internal sealed class PaymentEventConfiguration : IEntityTypeConfiguration<Payme
builder.Property(entity => entity.EventId).HasMaxLength(200);
builder.Property(entity => entity.Payload).IsJson("{}");
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.Provider, entity.EventId }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.Provider, entity.EventId }).IsUnique();
builder.HasOne<Tenant>().WithMany().HasForeignKey(entity => entity.TenantId).OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Payment>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.PaymentId })
@@ -438,7 +438,7 @@ internal sealed class CommerceReconciliationItemConfiguration : IEntityTypeConfi
builder.Property(entity => entity.IssueCode).HasMaxLength(100);
builder.Property(entity => entity.Details).IsJson("{}");
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.BatchId, entity.RowNo }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.BatchId, entity.RowNo }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.BatchId, entity.MatchStatus, entity.RowNo });
builder.HasIndex(entity => new { entity.TenantId, entity.OrderNo, entity.CreatedAt });
builder.HasIndex(entity => new { entity.TenantId, entity.RefundNo, entity.ProviderRefundNo, entity.CreatedAt });

View File

@@ -180,7 +180,7 @@ internal sealed class QuestionCollectionItemConfiguration :
{
entity.TenantId,
entity.CollectionId,
entity.QuestionId
entity.QuestionReferenceId
}).IsUnique();
builder.HasIndex(entity => new
{
@@ -194,10 +194,14 @@ internal sealed class QuestionCollectionItemConfiguration :
.HasForeignKey(entity => new { entity.TenantId, entity.CollectionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
builder.HasOne<TenantQuestionReference>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@@ -65,72 +65,48 @@ internal sealed class ContentAssetSecurityScanEventConfiguration : IEntityTypeCo
}
}
internal sealed class QuestionBankGrantConfiguration : IEntityTypeConfiguration<QuestionBankGrant>
internal sealed class TenantQuestionBankPreferenceConfiguration : IEntityTypeConfiguration<TenantQuestionBankPreference>
{
public void Configure(EntityTypeBuilder<QuestionBankGrant> builder)
public void Configure(EntityTypeBuilder<TenantQuestionBankPreference> builder)
{
builder.ConfigureEntity("question_bank_grants");
builder.ConfigureTenantEntity("tenant_question_bank_preferences");
builder.ConfigureTimestamps();
builder.Property(entity => entity.GrantScope).HasSnakeCaseEnum();
builder.Property(entity => entity.AllowedPlanCodes)
.HasColumnType("text[]")
.HasDefaultValueSql("'{}'::text[]");
builder.Property(entity => entity.AllowedTenantIds)
.HasColumnType("uuid[]")
.HasDefaultValueSql("'{}'::uuid[]");
builder.Property(entity => entity.AllowedRegionIds)
.HasColumnType("uuid[]")
.HasDefaultValueSql("'{}'::uuid[]");
builder.Property(entity => entity.AllowedSubjectIds)
.HasColumnType("uuid[]")
.HasDefaultValueSql("'{}'::uuid[]");
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.Alias).HasMaxLength(300);
builder.Property(entity => entity.NavigationLocation).HasMaxLength(100);
builder.Property(entity => entity.Metadata).IsJson("{}");
builder.HasIndex(entity => new { entity.SourceQuestionBankId, entity.Status, entity.StartsAt, entity.ExpiresAt });
builder.HasIndex(entity => entity.AllowedTenantIds).HasMethod("gin");
builder.HasIndex(entity => entity.AllowedPlanCodes).HasMethod("gin");
builder.HasIndex(entity => new
{
entity.TenantId,
entity.QuestionBankOwnerTenantId,
entity.QuestionBankId
}).IsUnique();
builder.HasOne<QuestionBank>().WithMany()
.HasForeignKey(entity => entity.SourceQuestionBankId)
.OnDelete(DeleteBehavior.Cascade);
.HasForeignKey(entity => new { entity.QuestionBankOwnerTenantId, entity.QuestionBankId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
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 TenantQuestionBankAdoptionConfiguration : IEntityTypeConfiguration<TenantQuestionBankAdoption>
internal sealed class TenantQuestionReferenceConfiguration : IEntityTypeConfiguration<TenantQuestionReference>
{
public void Configure(EntityTypeBuilder<TenantQuestionBankAdoption> builder)
public void Configure(EntityTypeBuilder<TenantQuestionReference> builder)
{
builder.ConfigureTenantEntity("tenant_question_bank_adoptions");
builder.ConfigureTenantEntity("tenant_question_references");
builder.ConfigureTimestamps();
builder.Property(entity => entity.AdoptionMode).HasSnakeCaseEnum();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.SyncStatus).HasSnakeCaseEnum();
builder.Property(entity => entity.SourceSnapshot).IsJson("{}");
builder.Property(entity => entity.Metadata).IsJson("{}");
builder.HasIndex(entity => new { entity.TenantId, entity.SourceQuestionBankId }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.UpdatedAt });
builder.ToTable(table => table.HasCheckConstraint("ck_tenant_question_bank_adoptions_copied_count", "copied_question_count >= 0"));
builder.HasOne<QuestionBank>().WithMany()
.HasForeignKey(entity => entity.SourceQuestionBankId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<QuestionBankGrant>().WithMany()
.HasForeignKey(entity => entity.GrantId)
.OnDelete(DeleteBehavior.SetNull);
builder.HasOne<QuestionBank>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.TargetQuestionBankId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<ContentEntry>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.TargetEntryId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<QuestionCollection>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.TargetCollectionId })
builder.Property(entity => entity.Source).HasSnakeCaseEnum();
builder.HasAlternateKey(entity => new
{
entity.TenantId,
entity.QuestionOwnerTenantId,
entity.QuestionId
});
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.CreatedBy).OnDelete(DeleteBehavior.SetNull);
builder.HasOne<User>().WithMany().HasForeignKey(entity => entity.UpdatedBy).OnDelete(DeleteBehavior.SetNull);
}
}

View File

@@ -17,7 +17,6 @@ internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration<Pr
builder.Property(entity => entity.Mode).HasMaxLength(50);
builder.Property(entity => entity.TargetType).HasMaxLength(50);
builder.Property(entity => entity.StartedAt).HasDefaultValueSql("now()");
builder.Property(entity => entity.QuestionIds).IsJson("[]");
builder.Property(entity => entity.TotalScore).HasPrecision(8, 2);
builder.Property(entity => entity.AccessMode)
.HasSnakeCaseEnum()
@@ -55,6 +54,45 @@ internal sealed class PracticeSessionConfiguration : IEntityTypeConfiguration<Pr
}
}
internal sealed class PracticeSessionQuestionConfiguration : IEntityTypeConfiguration<PracticeSessionQuestion>
{
public void Configure(EntityTypeBuilder<PracticeSessionQuestion> builder)
{
builder.ConfigureTenantEntity("practice_session_questions");
builder.HasAlternateKey(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Id });
builder.HasIndex(entity => new { entity.TenantId, entity.PracticeSessionId, entity.Position }).IsUnique();
builder.Property(entity => entity.Score).HasPrecision(8, 2);
builder.HasOne<PracticeSession>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.PracticeSessionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<TenantQuestionReference>().WithMany()
.HasForeignKey(entity => new
{
entity.TenantId,
entity.QuestionOwnerTenantId,
entity.QuestionId
})
.HasPrincipalKey(entity => new
{
entity.TenantId,
entity.QuestionOwnerTenantId,
entity.QuestionId
})
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<QuestionVersion>().WithMany()
.HasForeignKey(entity => new
{
TenantId = entity.QuestionOwnerTenantId,
entity.QuestionId,
Id = entity.QuestionVersionId
})
.HasPrincipalKey(entity => new { entity.TenantId, entity.QuestionId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}
internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<AnswerRecord>
{
public void Configure(EntityTypeBuilder<AnswerRecord> builder)
@@ -67,13 +105,6 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<Answe
builder.Property(entity => entity.AnsweredAt).HasDefaultValueSql("now()");
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.TenantId, entity.LegacyId }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.QuestionId });
builder.HasIndex(entity => new
{
entity.TenantId,
entity.QuestionId,
entity.QuestionVersionId
});
builder.HasIndex(entity => new
{
entity.TenantId,
@@ -81,31 +112,9 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<Answe
entity.PracticeSessionId
});
builder.ToTable(table => table.HasCheckConstraint(
"ck_answer_records_version_requires_question",
"question_version_id is null or question_id is not null"));
builder.HasOne<User>().WithMany()
.HasForeignKey(entity => entity.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<QuestionVersion>().WithMany()
.HasForeignKey(entity => new
{
entity.TenantId,
entity.QuestionId,
entity.QuestionVersionId
})
.HasPrincipalKey(entity => new
{
entity.TenantId,
entity.QuestionId,
entity.Id
})
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<PracticeSession>().WithMany()
.HasForeignKey(entity => new
{
@@ -120,6 +129,20 @@ internal sealed class AnswerRecordConfiguration : IEntityTypeConfiguration<Answe
entity.Id
})
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne<PracticeSessionQuestion>().WithMany()
.HasForeignKey(entity => new
{
entity.TenantId,
entity.PracticeSessionId,
Id = entity.SessionQuestionId
})
.HasPrincipalKey(entity => new
{
entity.TenantId,
entity.PracticeSessionId,
entity.Id
})
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -128,7 +151,7 @@ internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration<F
public void Configure(EntityTypeBuilder<FavoriteQuestion> builder)
{
builder.ToTable("favorite_questions");
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionId });
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionReferenceId });
builder.Property(entity => entity.Source).HasMaxLength(50);
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
@@ -138,10 +161,14 @@ internal sealed class FavoriteQuestionConfiguration : IEntityTypeConfiguration<F
builder.HasOne<User>().WithMany()
.HasForeignKey(entity => entity.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
builder.HasOne<TenantQuestionReference>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -150,7 +177,7 @@ internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration<Wron
public void Configure(EntityTypeBuilder<WrongQuestion> builder)
{
builder.ToTable("wrong_questions");
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionId });
builder.HasKey(entity => new { entity.TenantId, entity.UserId, entity.QuestionReferenceId });
builder.Property(entity => entity.WrongCount).HasDefaultValue(1);
builder.Property(entity => entity.LastWrongAt).HasDefaultValueSql("now()");
@@ -160,10 +187,14 @@ internal sealed class WrongQuestionConfiguration : IEntityTypeConfiguration<Wron
builder.HasOne<User>().WithMany()
.HasForeignKey(entity => entity.UserId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
builder.HasOne<TenantQuestionReference>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionReferenceId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.QuestionOwnerTenantId, entity.QuestionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@@ -59,7 +59,7 @@ internal sealed class TenantInvoiceConfiguration : IEntityTypeConfiguration<Tena
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.Currency).HasMaxLength(10);
builder.Property(entity => entity.Metadata).IsJson("{}");
builder.HasIndex(entity => entity.InvoiceNo).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.InvoiceNo }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.Status, entity.DueDate });
builder.HasIndex(entity => new { entity.TenantId, entity.BillingPeriodStart, entity.BillingPeriodEnd })
.IsUnique()
@@ -110,7 +110,7 @@ internal sealed class TenantInvoicePaymentConfiguration : IEntityTypeConfigurati
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.ProviderTradeNo).HasMaxLength(200);
builder.Property(entity => entity.RawPayload).IsJson("{}");
builder.HasIndex(entity => entity.PaymentNo).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.PaymentNo }).IsUnique();
builder.HasIndex(entity => new { entity.InvoiceId, entity.Status });
builder.ToTable(table => table.HasCheckConstraint("ck_tenant_invoice_payments_amount", "amount_cents >= 0"));
builder.HasOne<TenantInvoice>().WithMany()
@@ -229,7 +229,7 @@ internal sealed class PlatformDunningNotificationEventConfiguration : IEntityTyp
builder.Property(entity => entity.RequestPayload).IsJson("{}");
builder.Property(entity => entity.Metadata).IsJson("{}");
builder.Property(entity => entity.ScheduledAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.ChannelId, entity.ReminderId }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.ChannelId, entity.ReminderId }).IsUnique();
builder.HasIndex(entity => new { entity.Status, entity.NextAttemptAt, entity.ScheduledAt, entity.CreatedAt });
builder.HasIndex(entity => new { entity.ReminderId, entity.Status, entity.CreatedAt });
builder.HasIndex(entity => new { entity.InvoiceId, entity.Status, entity.CreatedAt });

View File

@@ -14,7 +14,6 @@ internal sealed class QuestionBankConfiguration : IEntityTypeConfiguration<Quest
builder.ConfigureTenantEntity("question_banks");
builder.ConfigureTimestamps();
builder.Property(entity => entity.Name).HasMaxLength(300);
builder.Property(entity => entity.SourceScope).HasSnakeCaseEnum();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.Metadata).IsJson("{}");
@@ -94,7 +93,7 @@ internal sealed class QuestionVersionConfiguration : IEntityTypeConfiguration<Qu
builder.Property(entity => entity.SourceHash).HasMaxLength(128);
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasIndex(entity => new { entity.QuestionId, entity.VersionNo }).IsUnique();
builder.HasIndex(entity => new { entity.TenantId, entity.QuestionId, entity.VersionNo }).IsUnique();
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })

View File

@@ -0,0 +1,53 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Tiku.Domain.Catalog;
using Tiku.Domain.QuestionBanks;
namespace Tiku.Infrastructure.Persistence.Configurations;
internal sealed class TaxonomyNodeConfiguration : IEntityTypeConfiguration<TaxonomyNode>
{
public void Configure(EntityTypeBuilder<TaxonomyNode> builder)
{
builder.ConfigureTenantEntity("taxonomy_nodes");
builder.ConfigureTimestamps();
builder.Property(entity => entity.NodeType).HasSnakeCaseEnum();
builder.Property(entity => entity.Code).HasMaxLength(100);
builder.Property(entity => entity.Name).HasMaxLength(300);
builder.Property(entity => entity.Path).HasColumnType("ltree");
builder.Property(entity => entity.Metadata).IsJson("{}");
builder.HasIndex(entity => new { entity.TenantId, entity.Code }).IsUnique();
builder.HasOne<TaxonomyNode>().WithMany()
.HasForeignKey(entity => new { TenantId = entity.ParentOwnerTenantId, Id = entity.ParentId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
builder.ToTable(table => table.HasCheckConstraint(
"ck_taxonomy_nodes_parent_pair",
"(parent_owner_tenant_id is null) = (parent_id is null)"));
}
}
internal sealed class QuestionTaxonomyAssignmentConfiguration : IEntityTypeConfiguration<QuestionTaxonomyAssignment>
{
public void Configure(EntityTypeBuilder<QuestionTaxonomyAssignment> builder)
{
builder.ConfigureEntity("question_taxonomy_assignments");
builder.HasAlternateKey(entity => new { entity.TenantId, entity.Id });
builder.HasIndex(entity => new
{
entity.TenantId,
entity.QuestionId,
entity.TaxonomyOwnerTenantId,
entity.TaxonomyNodeId
}).IsUnique();
builder.Property(entity => entity.CreatedAt).HasDefaultValueSql("now()");
builder.HasOne<Question>().WithMany()
.HasForeignKey(entity => new { entity.TenantId, entity.QuestionId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<TaxonomyNode>().WithMany()
.HasForeignKey(entity => new { TenantId = entity.TaxonomyOwnerTenantId, Id = entity.TaxonomyNodeId })
.HasPrincipalKey(entity => new { entity.TenantId, entity.Id })
.OnDelete(DeleteBehavior.Restrict);
}
}

View File

@@ -23,6 +23,9 @@ internal sealed class TenantConfiguration : IEntityTypeConfiguration<Tenant>
builder.HasIndex(entity => entity.Slug).IsUnique();
builder.HasIndex(entity => entity.LegacyId).IsUnique();
builder.HasIndex(entity => entity.Mode)
.IsUnique()
.HasFilter("mode = 'platform_owned'");
builder.HasOne<User>()
.WithMany()
@@ -67,7 +70,10 @@ internal sealed class TenantDomainConfiguration : IEntityTypeConfiguration<Tenan
builder.Property(entity => entity.DomainType).HasSnakeCaseEnum();
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.Property(entity => entity.VerificationToken).HasMaxLength(256);
builder.HasIndex(entity => entity.Host).IsUnique();
builder.Property(entity => entity.LastFailureReason).HasMaxLength(2000);
builder.HasIndex(entity => entity.Host)
.IsUnique()
.HasAnnotation("Tiku:GlobalUnique", true);
builder.HasIndex(entity => new { entity.TenantId, entity.IsPrimary })
.IsUnique()
.HasFilter("is_primary");
@@ -114,3 +120,29 @@ internal sealed class TenantSettingsConfiguration : IEntityTypeConfiguration<Ten
.OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class TenantFrontendConfigConfiguration : IEntityTypeConfiguration<TenantFrontendConfig>
{
public void Configure(EntityTypeBuilder<TenantFrontendConfig> builder)
{
builder.ConfigureTenantEntity("tenant_frontend_configs");
builder.ConfigureTimestamps();
builder.HasIndex(entity => entity.TenantId).IsUnique();
builder.Property(entity => entity.ConfigVersion).IsConcurrencyToken();
builder.Property(entity => entity.PublishedBranding).IsJson("{}");
builder.Property(entity => entity.PublishedTheme).IsJson("{}");
builder.Property(entity => entity.PublishedFeatures).IsJson("{}");
builder.Property(entity => entity.PublishedNavigation).IsJson("[]");
builder.Property(entity => entity.PublishedHomeModules).IsJson("[]");
builder.Property(entity => entity.DraftBranding).IsJson("{}");
builder.Property(entity => entity.DraftTheme).IsJson("{}");
builder.Property(entity => entity.DraftFeatures).IsJson("{}");
builder.Property(entity => entity.DraftNavigation).IsJson("[]");
builder.Property(entity => entity.DraftHomeModules).IsJson("[]");
builder.ToTable(table =>
{
table.HasCheckConstraint("ck_tenant_frontend_configs_schema_version", "schema_version > 0");
table.HasCheckConstraint("ck_tenant_frontend_configs_config_version", "config_version > 0");
});
}
}

View File

@@ -111,7 +111,9 @@ internal sealed class AuthSessionConfiguration : IEntityTypeConfiguration<AuthSe
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();
builder.HasIndex(entity => entity.TokenHash)
.IsUnique()
.HasAnnotation("Tiku:GlobalUnique", true);
builder.HasIndex(entity => new { entity.TenantId, entity.UserId, entity.ExpiresAt })
.HasFilter("revoked_at is null");