feat(security): add distributed authorization foundation

This commit is contained in:
2026-07-29 10:40:10 +08:00
parent c7f9a4e3c9
commit df88fa19cb
76 changed files with 22020 additions and 88 deletions

View File

@@ -27,6 +27,56 @@ internal sealed class PlatformSaasPlanConfiguration : IEntityTypeConfiguration<P
}
}
internal sealed class ProductModuleConfiguration : IEntityTypeConfiguration<ProductModule>
{
public void Configure(EntityTypeBuilder<ProductModule> builder)
{
builder.ConfigureEntity("product_modules");
builder.ConfigureTimestamps();
builder.Property(entity => entity.Code).HasMaxLength(100);
builder.Property(entity => entity.Name).HasMaxLength(200);
builder.Property(entity => entity.Description).HasMaxLength(1000);
builder.Property(entity => entity.Status).HasSnakeCaseEnum();
builder.HasIndex(entity => entity.Code).IsUnique();
}
}
internal sealed class PlanModuleEntitlementConfiguration : IEntityTypeConfiguration<PlanModuleEntitlement>
{
public void Configure(EntityTypeBuilder<PlanModuleEntitlement> builder)
{
builder.ConfigureEntity("plan_module_entitlements");
builder.Property(entity => entity.PlanCode).HasMaxLength(100);
builder.Property(entity => entity.ModuleCode).HasMaxLength(100);
builder.HasIndex(entity => new { entity.PlanCode, entity.ModuleCode }).IsUnique();
builder.HasOne<PlatformSaasPlan>().WithMany()
.HasPrincipalKey(entity => entity.Code)
.HasForeignKey(entity => entity.PlanCode)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<ProductModule>().WithMany()
.HasPrincipalKey(entity => entity.Code)
.HasForeignKey(entity => entity.ModuleCode)
.OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class TenantModuleOverrideConfiguration : IEntityTypeConfiguration<TenantModuleOverride>
{
public void Configure(EntityTypeBuilder<TenantModuleOverride> builder)
{
builder.ConfigureTenantEntity("tenant_module_overrides");
builder.ConfigureTimestamps();
builder.Property(entity => entity.ModuleCode).HasMaxLength(100);
builder.Property(entity => entity.Mode).HasSnakeCaseEnum();
builder.Property(entity => entity.Reason).HasMaxLength(1000);
builder.HasIndex(entity => new { entity.TenantId, entity.ModuleCode }).IsUnique();
builder.HasOne<ProductModule>().WithMany()
.HasPrincipalKey(entity => entity.Code)
.HasForeignKey(entity => entity.ModuleCode)
.OnDelete(DeleteBehavior.Restrict);
}
}
internal sealed class TenantBillingProfileConfiguration : IEntityTypeConfiguration<TenantBillingProfile>
{
public void Configure(EntityTypeBuilder<TenantBillingProfile> builder)

View File

@@ -115,6 +115,20 @@ internal sealed class TenantSettingsConfiguration : IEntityTypeConfiguration<Ten
}
}
internal sealed class TenantAuthPolicyConfiguration : IEntityTypeConfiguration<TenantAuthPolicy>
{
public void Configure(EntityTypeBuilder<TenantAuthPolicy> builder)
{
builder.ToTable("tenant_auth_policies");
builder.HasKey(entity => entity.TenantId);
builder.ConfigureTimestamps();
builder.Property(entity => entity.AllowExternalStudentSelfRegistration).HasDefaultValue(false);
builder.HasOne<Tenant>().WithOne()
.HasForeignKey<TenantAuthPolicy>(entity => entity.TenantId)
.OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class TenantFrontendConfigConfiguration : IEntityTypeConfiguration<TenantFrontendConfig>
{
public void Configure(EntityTypeBuilder<TenantFrontendConfig> builder)

View File

@@ -0,0 +1,297 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Tiku.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddDistributedSecurityFoundation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddUniqueConstraint(
name: "ak_platform_saas_plans_code",
table: "platform_saas_plans",
column: "code");
migrationBuilder.CreateTable(
name: "inbox_state",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
message_id = table.Column<Guid>(type: "uuid", nullable: false),
consumer_id = table.Column<Guid>(type: "uuid", nullable: false),
lock_id = table.Column<Guid>(type: "uuid", nullable: false),
row_version = table.Column<byte[]>(type: "bytea", rowVersion: true, nullable: true),
received = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
receive_count = table.Column<int>(type: "integer", nullable: false),
expiration_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
consumed = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
delivered = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
last_sequence_number = table.Column<long>(type: "bigint", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_inbox_state", x => x.id);
table.UniqueConstraint("ak_inbox_state_message_id_consumer_id", x => new { x.message_id, x.consumer_id });
});
migrationBuilder.CreateTable(
name: "outbox_state",
columns: table => new
{
outbox_id = table.Column<Guid>(type: "uuid", nullable: false),
lock_id = table.Column<Guid>(type: "uuid", nullable: false),
row_version = table.Column<byte[]>(type: "bytea", rowVersion: true, nullable: true),
created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
delivered = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
last_sequence_number = table.Column<long>(type: "bigint", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_outbox_state", x => x.outbox_id);
});
migrationBuilder.CreateTable(
name: "product_modules",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
sort_order = table.Column<int>(type: "integer", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
},
constraints: table =>
{
table.PrimaryKey("pk_product_modules", x => x.id);
table.UniqueConstraint("ak_product_modules_code", x => x.code);
});
migrationBuilder.CreateTable(
name: "tenant_auth_policies",
columns: table => new
{
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
allow_external_student_self_registration = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
},
constraints: table =>
{
table.PrimaryKey("pk_tenant_auth_policies", x => x.tenant_id);
table.ForeignKey(
name: "fk_tenant_auth_policies_tenants_tenant_id",
column: x => x.tenant_id,
principalTable: "tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
// Existing tenants retain the historical external-login behavior. New tenants
// receive an explicit fail-closed policy when created by PlatformAdminService.
migrationBuilder.Sql("""
INSERT INTO tenant_auth_policies
(tenant_id, allow_external_student_self_registration, created_at, updated_at)
SELECT id, TRUE, now(), now()
FROM tenants
ON CONFLICT (tenant_id) DO NOTHING;
""");
migrationBuilder.CreateTable(
name: "outbox_message",
columns: table => new
{
sequence_number = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
enqueue_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
sent_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
headers = table.Column<string>(type: "text", nullable: true),
properties = table.Column<string>(type: "text", nullable: true),
inbox_message_id = table.Column<Guid>(type: "uuid", nullable: true),
inbox_consumer_id = table.Column<Guid>(type: "uuid", nullable: true),
outbox_id = table.Column<Guid>(type: "uuid", nullable: true),
message_id = table.Column<Guid>(type: "uuid", nullable: false),
content_type = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
message_type = table.Column<string>(type: "text", nullable: false),
body = table.Column<string>(type: "text", nullable: false),
conversation_id = table.Column<Guid>(type: "uuid", nullable: true),
correlation_id = table.Column<Guid>(type: "uuid", nullable: true),
initiator_id = table.Column<Guid>(type: "uuid", nullable: true),
request_id = table.Column<Guid>(type: "uuid", nullable: true),
source_address = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
destination_address = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
response_address = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
fault_address = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
expiration_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_outbox_message", x => x.sequence_number);
table.ForeignKey(
name: "fk_outbox_message_inbox_state_inbox_message_id_inbox_consumer_~",
columns: x => new { x.inbox_message_id, x.inbox_consumer_id },
principalTable: "inbox_state",
principalColumns: new[] { "message_id", "consumer_id" });
table.ForeignKey(
name: "fk_outbox_message_outbox_state_outbox_id",
column: x => x.outbox_id,
principalTable: "outbox_state",
principalColumn: "outbox_id");
});
migrationBuilder.CreateTable(
name: "plan_module_entitlements",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
plan_code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
module_code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
enabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_plan_module_entitlements", x => x.id);
table.ForeignKey(
name: "fk_plan_module_entitlements_platform_saas_plans_plan_code",
column: x => x.plan_code,
principalTable: "platform_saas_plans",
principalColumn: "code",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_plan_module_entitlements_product_modules_module_code",
column: x => x.module_code,
principalTable: "product_modules",
principalColumn: "code",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "tenant_module_overrides",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false, defaultValueSql: "gen_random_uuid()"),
module_code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
mode = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
expires_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
reason = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
created_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
updated_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()")
},
constraints: table =>
{
table.PrimaryKey("pk_tenant_module_overrides", x => x.id);
table.UniqueConstraint("ak_tenant_module_overrides_tenant_id_id", x => new { x.tenant_id, x.id });
table.ForeignKey(
name: "fk_tenant_module_overrides_product_modules_module_code",
column: x => x.module_code,
principalTable: "product_modules",
principalColumn: "code",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "fk_tenant_module_overrides_tenants_tenant_id",
column: x => x.tenant_id,
principalTable: "tenants",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_inbox_state_delivered",
table: "inbox_state",
column: "delivered");
migrationBuilder.CreateIndex(
name: "ix_outbox_message_enqueue_time",
table: "outbox_message",
column: "enqueue_time");
migrationBuilder.CreateIndex(
name: "ix_outbox_message_expiration_time",
table: "outbox_message",
column: "expiration_time");
migrationBuilder.CreateIndex(
name: "ix_outbox_message_inbox_message_id_inbox_consumer_id_sequence_~",
table: "outbox_message",
columns: new[] { "inbox_message_id", "inbox_consumer_id", "sequence_number" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_outbox_message_outbox_id_sequence_number",
table: "outbox_message",
columns: new[] { "outbox_id", "sequence_number" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_outbox_state_created",
table: "outbox_state",
column: "created");
migrationBuilder.CreateIndex(
name: "ix_plan_module_entitlements_module_code",
table: "plan_module_entitlements",
column: "module_code");
migrationBuilder.CreateIndex(
name: "ix_plan_module_entitlements_plan_code_module_code",
table: "plan_module_entitlements",
columns: new[] { "plan_code", "module_code" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_product_modules_code",
table: "product_modules",
column: "code",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_tenant_module_overrides_module_code",
table: "tenant_module_overrides",
column: "module_code");
migrationBuilder.CreateIndex(
name: "ix_tenant_module_overrides_tenant_id_module_code",
table: "tenant_module_overrides",
columns: new[] { "tenant_id", "module_code" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "outbox_message");
migrationBuilder.DropTable(
name: "plan_module_entitlements");
migrationBuilder.DropTable(
name: "tenant_auth_policies");
migrationBuilder.DropTable(
name: "tenant_module_overrides");
migrationBuilder.DropTable(
name: "inbox_state");
migrationBuilder.DropTable(
name: "outbox_state");
migrationBuilder.DropTable(
name: "product_modules");
migrationBuilder.DropUniqueConstraint(
name: "ak_platform_saas_plans_code",
table: "platform_saas_plans");
}
}
}

View File

@@ -25,6 +25,224 @@ namespace Tiku.Infrastructure.Persistence.Migrations
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "ltree");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("Consumed")
.HasColumnType("timestamp with time zone")
.HasColumnName("consumed");
b.Property<Guid>("ConsumerId")
.HasColumnType("uuid")
.HasColumnName("consumer_id");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone")
.HasColumnName("delivered");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint")
.HasColumnName("last_sequence_number");
b.Property<Guid>("LockId")
.HasColumnType("uuid")
.HasColumnName("lock_id");
b.Property<Guid>("MessageId")
.HasColumnType("uuid")
.HasColumnName("message_id");
b.Property<int>("ReceiveCount")
.HasColumnType("integer")
.HasColumnName("receive_count");
b.Property<DateTime>("Received")
.HasColumnType("timestamp with time zone")
.HasColumnName("received");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea")
.HasColumnName("row_version");
b.HasKey("Id")
.HasName("pk_inbox_state");
b.HasAlternateKey("MessageId", "ConsumerId")
.HasName("ak_inbox_state_message_id_consumer_id");
b.HasIndex("Delivered")
.HasDatabaseName("ix_inbox_state_delivered");
b.ToTable("inbox_state", (string)null);
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.Property<long>("SequenceNumber")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("sequence_number");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceNumber"));
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text")
.HasColumnName("body");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("content_type");
b.Property<Guid?>("ConversationId")
.HasColumnType("uuid")
.HasColumnName("conversation_id");
b.Property<Guid?>("CorrelationId")
.HasColumnType("uuid")
.HasColumnName("correlation_id");
b.Property<string>("DestinationAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("destination_address");
b.Property<DateTime?>("EnqueueTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("enqueue_time");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<string>("FaultAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("fault_address");
b.Property<string>("Headers")
.HasColumnType("text")
.HasColumnName("headers");
b.Property<Guid?>("InboxConsumerId")
.HasColumnType("uuid")
.HasColumnName("inbox_consumer_id");
b.Property<Guid?>("InboxMessageId")
.HasColumnType("uuid")
.HasColumnName("inbox_message_id");
b.Property<Guid?>("InitiatorId")
.HasColumnType("uuid")
.HasColumnName("initiator_id");
b.Property<Guid>("MessageId")
.HasColumnType("uuid")
.HasColumnName("message_id");
b.Property<string>("MessageType")
.IsRequired()
.HasColumnType("text")
.HasColumnName("message_type");
b.Property<Guid?>("OutboxId")
.HasColumnType("uuid")
.HasColumnName("outbox_id");
b.Property<string>("Properties")
.HasColumnType("text")
.HasColumnName("properties");
b.Property<Guid?>("RequestId")
.HasColumnType("uuid")
.HasColumnName("request_id");
b.Property<string>("ResponseAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("response_address");
b.Property<DateTime>("SentTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("sent_time");
b.Property<string>("SourceAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("source_address");
b.HasKey("SequenceNumber")
.HasName("pk_outbox_message");
b.HasIndex("EnqueueTime")
.HasDatabaseName("ix_outbox_message_enqueue_time");
b.HasIndex("ExpirationTime")
.HasDatabaseName("ix_outbox_message_expiration_time");
b.HasIndex("OutboxId", "SequenceNumber")
.IsUnique()
.HasDatabaseName("ix_outbox_message_outbox_id_sequence_number");
b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
.IsUnique()
.HasDatabaseName("ix_outbox_message_inbox_message_id_inbox_consumer_id_sequence_~");
b.ToTable("outbox_message", (string)null);
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
{
b.Property<Guid>("OutboxId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("outbox_id");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone")
.HasColumnName("created");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone")
.HasColumnName("delivered");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint")
.HasColumnName("last_sequence_number");
b.Property<Guid>("LockId")
.HasColumnType("uuid")
.HasColumnName("lock_id");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea")
.HasColumnName("row_version");
b.HasKey("OutboxId")
.HasName("pk_outbox_state");
b.HasIndex("Created")
.HasDatabaseName("ix_outbox_state_created");
b.ToTable("outbox_state", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
@@ -11755,6 +11973,43 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.ToTable("user_notifications", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Platform.PlanModuleEntitlement", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<bool>("Enabled")
.HasColumnType("boolean")
.HasColumnName("enabled");
b.Property<string>("ModuleCode")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("module_code");
b.Property<string>("PlanCode")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("plan_code");
b.HasKey("Id")
.HasName("pk_plan_module_entitlements");
b.HasIndex("ModuleCode")
.HasDatabaseName("ix_plan_module_entitlements_module_code");
b.HasIndex("PlanCode", "ModuleCode")
.IsUnique()
.HasDatabaseName("ix_plan_module_entitlements_plan_code_module_code");
b.ToTable("plan_module_entitlements", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Platform.PlatformAuditAlert", b =>
{
b.Property<Guid>("Id")
@@ -12304,6 +12559,9 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.HasKey("Id")
.HasName("pk_platform_saas_plans");
b.HasAlternateKey("Code")
.HasName("ak_platform_saas_plans_code");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("ix_platform_saas_plans_code");
@@ -12317,6 +12575,66 @@ namespace Tiku.Infrastructure.Persistence.Migrations
});
});
modelBuilder.Entity("Tiku.Domain.Platform.ProductModule", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("code");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)")
.HasColumnName("description");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("name");
b.Property<int>("SortOrder")
.HasColumnType("integer")
.HasColumnName("sort_order");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("status");
b.Property<DateTimeOffset>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at")
.HasDefaultValueSql("now()");
b.HasKey("Id")
.HasName("pk_product_modules");
b.HasAlternateKey("Code")
.HasName("ak_product_modules_code");
b.HasIndex("Code")
.IsUnique()
.HasDatabaseName("ix_product_modules_code");
b.ToTable("product_modules", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Platform.TenantBillingProfile", b =>
{
b.Property<Guid>("TenantId")
@@ -12818,6 +13136,67 @@ namespace Tiku.Infrastructure.Persistence.Migrations
});
});
modelBuilder.Entity("Tiku.Domain.Platform.TenantModuleOverride", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at")
.HasDefaultValueSql("now()");
b.Property<DateTimeOffset?>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<string>("Mode")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("mode");
b.Property<string>("ModuleCode")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("module_code");
b.Property<string>("Reason")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)")
.HasColumnName("reason");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasColumnName("tenant_id");
b.Property<DateTimeOffset>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at")
.HasDefaultValueSql("now()");
b.HasKey("Id")
.HasName("pk_tenant_module_overrides");
b.HasAlternateKey("TenantId", "Id")
.HasName("ak_tenant_module_overrides_tenant_id_id");
b.HasIndex("ModuleCode")
.HasDatabaseName("ix_tenant_module_overrides_module_code");
b.HasIndex("TenantId", "ModuleCode")
.IsUnique()
.HasDatabaseName("ix_tenant_module_overrides_tenant_id_module_code");
b.ToTable("tenant_module_overrides", (string)null);
});
modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b =>
{
b.Property<Guid>("Id")
@@ -13642,6 +14021,36 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.ToTable("tenants", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantAuthPolicy", b =>
{
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasColumnName("tenant_id");
b.Property<bool>("AllowExternalStudentSelfRegistration")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("allow_external_student_self_registration");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at")
.HasDefaultValueSql("now()");
b.Property<DateTimeOffset>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at")
.HasDefaultValueSql("now()");
b.HasKey("TenantId")
.HasName("pk_tenant_auth_policies");
b.ToTable("tenant_auth_policies", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantBranding", b =>
{
b.Property<Guid>("TenantId")
@@ -14618,6 +15027,20 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.ToTable("tenant_student_notes", (string)null);
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null)
.WithMany()
.HasForeignKey("OutboxId")
.HasConstraintName("fk_outbox_message_outbox_state_outbox_id");
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null)
.WithMany()
.HasForeignKey("InboxMessageId", "InboxConsumerId")
.HasPrincipalKey("MessageId", "ConsumerId")
.HasConstraintName("fk_outbox_message_inbox_state_inbox_message_id_inbox_consumer_~");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("Tiku.Domain.Identity.User", null)
@@ -17550,6 +17973,25 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasConstraintName("fk_user_notifications_users_user_id");
});
modelBuilder.Entity("Tiku.Domain.Platform.PlanModuleEntitlement", b =>
{
b.HasOne("Tiku.Domain.Platform.ProductModule", null)
.WithMany()
.HasForeignKey("ModuleCode")
.HasPrincipalKey("Code")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_plan_module_entitlements_product_modules_module_code");
b.HasOne("Tiku.Domain.Platform.PlatformSaasPlan", null)
.WithMany()
.HasForeignKey("PlanCode")
.HasPrincipalKey("Code")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_plan_module_entitlements_platform_saas_plans_plan_code");
});
modelBuilder.Entity("Tiku.Domain.Platform.PlatformAuditAlert", b =>
{
b.HasOne("Tiku.Domain.Identity.User", null)
@@ -17719,6 +18161,24 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasConstraintName("fk_tenant_invoice_reminders_tenant_invoices_tenant_id_invoice_~");
});
modelBuilder.Entity("Tiku.Domain.Platform.TenantModuleOverride", b =>
{
b.HasOne("Tiku.Domain.Platform.ProductModule", null)
.WithMany()
.HasForeignKey("ModuleCode")
.HasPrincipalKey("Code")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired()
.HasConstraintName("fk_tenant_module_overrides_product_modules_module_code");
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_tenant_module_overrides_tenants_tenant_id");
});
modelBuilder.Entity("Tiku.Domain.QuestionBanks.Question", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
@@ -17895,6 +18355,16 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasConstraintName("fk_tenants_users_owner_user_id");
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantAuthPolicy", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithOne()
.HasForeignKey("Tiku.Domain.Tenancy.TenantAuthPolicy", "TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_tenant_auth_policies_tenants_tenant_id");
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantBranding", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)

View File

@@ -16,6 +16,8 @@ using Tiku.Domain.Operations;
using Tiku.Domain.Platform;
using Tiku.Domain.QuestionBanks;
using Tiku.Domain.Tenancy;
using MassTransit;
using MassTransit.EntityFrameworkCoreIntegration;
namespace Tiku.Infrastructure.Persistence;
@@ -47,6 +49,7 @@ public sealed class TikuDbContext(
public DbSet<TenantDomain> TenantDomains => Set<TenantDomain>();
public DbSet<TenantBranding> TenantBrandings => Set<TenantBranding>();
public DbSet<TenantSettings> TenantSettings => Set<TenantSettings>();
public DbSet<TenantAuthPolicy> TenantAuthPolicies => Set<TenantAuthPolicy>();
public DbSet<TenantFrontendConfig> TenantFrontendConfigs => Set<TenantFrontendConfig>();
public DbSet<TenantExternalProvider> TenantExternalProviders => Set<TenantExternalProvider>();
public DbSet<TenantSecret> TenantSecrets => Set<TenantSecret>();
@@ -178,6 +181,9 @@ public sealed class TikuDbContext(
public DbSet<TenantThemeTemplate> TenantThemeTemplates => Set<TenantThemeTemplate>();
public DbSet<TenantThemeConfig> TenantThemeConfigs => Set<TenantThemeConfig>();
public DbSet<PlatformSaasPlan> PlatformSaasPlans => Set<PlatformSaasPlan>();
public DbSet<ProductModule> ProductModules => Set<ProductModule>();
public DbSet<PlanModuleEntitlement> PlanModuleEntitlements => Set<PlanModuleEntitlement>();
public DbSet<TenantModuleOverride> TenantModuleOverrides => Set<TenantModuleOverride>();
public DbSet<TenantBillingProfile> TenantBillingProfiles => Set<TenantBillingProfile>();
public DbSet<TenantInvoice> TenantInvoices => Set<TenantInvoice>();
public DbSet<TenantInvoiceItem> TenantInvoiceItems => Set<TenantInvoiceItem>();
@@ -201,6 +207,12 @@ public sealed class TikuDbContext(
modelBuilder.HasPostgresExtension("ltree");
modelBuilder.ApplyConfigurationsFromAssembly(typeof(TikuDbContext).Assembly);
modelBuilder.Entity<DataProtectionKey>().ToTable("data_protection_keys");
modelBuilder.AddInboxStateEntity();
modelBuilder.AddOutboxMessageEntity();
modelBuilder.AddOutboxStateEntity();
modelBuilder.Entity<InboxState>().ToTable("inbox_state");
modelBuilder.Entity<OutboxMessage>().ToTable("outbox_message");
modelBuilder.Entity<OutboxState>().ToTable("outbox_state");
ApplyTenantQueryFilters(modelBuilder);
ValidateTenantModel(modelBuilder);
modelBuilder.UseSnakeCaseIdentifiers();