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

@@ -25,6 +25,110 @@ namespace Tiku.Infrastructure.Persistence.Migrations
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "ltree");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("FriendlyName")
.HasColumnType("text")
.HasColumnName("friendly_name");
b.Property<string>("Xml")
.HasColumnType("text")
.HasColumnName("xml");
b.HasKey("Id")
.HasName("pk_data_protection_keys");
b.ToTable("data_protection_keys", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text")
.HasColumnName("claim_type");
b.Property<string>("ClaimValue")
.HasColumnType("text")
.HasColumnName("claim_value");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_user_claims");
b.HasIndex("UserId")
.HasDatabaseName("ix_user_claims_user_id");
b.ToTable("user_claims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text")
.HasColumnName("login_provider");
b.Property<string>("ProviderKey")
.HasColumnType("text")
.HasColumnName("provider_key");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text")
.HasColumnName("provider_display_name");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("LoginProvider", "ProviderKey")
.HasName("pk_user_logins");
b.HasIndex("UserId")
.HasDatabaseName("ix_user_logins_user_id");
b.ToTable("user_logins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.Property<string>("LoginProvider")
.HasColumnType("text")
.HasColumnName("login_provider");
b.Property<string>("Name")
.HasColumnType("text")
.HasColumnName("name");
b.Property<string>("Value")
.HasColumnType("text")
.HasColumnName("value");
b.HasKey("UserId", "LoginProvider", "Name")
.HasName("pk_user_tokens");
b.ToTable("user_tokens", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Catalog.Category", b =>
{
b.Property<Guid>("Id")
@@ -7915,11 +8019,21 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer")
.HasColumnName("access_failed_count");
b.Property<string>("AvatarUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("avatar_url");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("concurrency_stamp");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
@@ -7931,6 +8045,14 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasColumnType("citext")
.HasColumnName("email");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasColumnName("email_confirmed");
b.Property<bool>("ForcePasswordChange")
.HasColumnType("boolean")
.HasColumnName("force_password_change");
b.Property<DateTimeOffset?>("LastSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_at");
@@ -7940,25 +8062,48 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasColumnType("character varying(64)")
.HasColumnName("legacy_id");
b.Property<string>("LegacyPasswordHash")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("legacy_password_hash");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasColumnName("lockout_enabled");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone")
.HasColumnName("lockout_end");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("name");
b.Property<bool>("PasswordMigrationRequired")
.HasColumnType("boolean")
.HasColumnName("password_migration_required");
b.Property<string>("NormalizedEmail")
.HasMaxLength(320)
.HasColumnType("character varying(320)")
.HasColumnName("normalized_email");
b.Property<string>("NormalizedUserName")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("normalized_user_name");
b.Property<string>("PasswordHash")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("password_hash");
b.Property<string>("Phone")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("phone");
b.Property<string>("PhoneNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("phone_number");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean")
.HasColumnName("phone_number_confirmed");
b.Property<string>("PrimaryRole")
.IsRequired()
.HasMaxLength(50)
@@ -7975,36 +8120,50 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasColumnType("integer")
.HasColumnName("score");
b.Property<string>("SecurityStamp")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("security_stamp");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("status");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean")
.HasColumnName("two_factor_enabled");
b.Property<DateTimeOffset>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at")
.HasDefaultValueSql("now()");
b.Property<string>("Username")
b.Property<string>("UserName")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("username");
.HasColumnName("user_name");
b.HasKey("Id")
.HasName("pk_users");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("ix_users_email");
b.HasIndex("LegacyId")
.IsUnique()
.HasDatabaseName("ix_users_legacy_id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("email_index");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("user_name_index");
b.HasIndex("Phone")
.IsUnique()
.HasDatabaseName("ix_users_phone");
b.HasIndex("Username")
.IsUnique()
.HasDatabaseName("ix_users_username");
b.ToTable("users", (string)null);
});
@@ -8049,12 +8208,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasColumnType("character varying(255)")
.HasColumnName("provider_subject");
b.Property<JsonElement>("SecretPayload")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("secret_payload")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<string>("UnionId")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
@@ -12648,6 +12801,95 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.ToTable("question_versions", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Tenancy.AuthChallenge", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<DateTimeOffset?>("ConsumedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("consumed_at");
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>("IpAddress")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("ip_address");
b.Property<string>("Provider")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("provider");
b.Property<string>("Purpose")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("purpose");
b.Property<string>("Realm")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("realm");
b.Property<string>("SecurityStamp")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("security_stamp");
b.Property<Guid?>("TenantId")
.HasColumnType("uuid")
.HasColumnName("tenant_id");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("token_hash");
b.Property<string>("UserAgent")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("user_agent");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("Id")
.HasName("pk_auth_challenges");
b.HasIndex("TenantId")
.HasDatabaseName("ix_auth_challenges_tenant_id");
b.HasIndex("TokenHash")
.IsUnique()
.HasDatabaseName("ix_auth_challenges_token_hash");
b.HasIndex("UserId", "Purpose", "ExpiresAt")
.HasDatabaseName("ix_auth_challenges_user_id_purpose_expires_at");
b.ToTable("auth_challenges", null, t =>
{
t.HasCheckConstraint("ck_auth_challenges_realm_tenant", "(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)");
});
});
modelBuilder.Entity("Tiku.Domain.Tenancy.AuthLoginEvent", b =>
{
b.Property<Guid>("Id")
@@ -12752,20 +12994,53 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasColumnName("metadata")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<bool>("MfaSatisfied")
.HasColumnType("boolean")
.HasColumnName("mfa_satisfied");
b.Property<Guid?>("ParentSessionId")
.HasColumnType("uuid")
.HasColumnName("parent_session_id");
b.Property<string>("Provider")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("provider");
b.Property<string>("Realm")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("realm");
b.Property<Guid?>("ReplacedBySessionId")
.HasColumnType("uuid")
.HasColumnName("replaced_by_session_id");
b.Property<DateTimeOffset?>("RevokedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked_at");
b.Property<Guid>("TenantId")
b.Property<string>("RevokedReason")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("revoked_reason");
b.Property<string>("SecurityStamp")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("security_stamp");
b.Property<Guid?>("TenantId")
.HasColumnType("uuid")
.HasColumnName("tenant_id");
b.Property<Guid>("TokenFamilyId")
.HasColumnType("uuid")
.HasColumnName("token_family_id");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(256)
@@ -12790,8 +13065,8 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.HasKey("Id")
.HasName("pk_auth_sessions");
b.HasAlternateKey("TenantId", "Id")
.HasName("ak_auth_sessions_tenant_id_id");
b.HasIndex("TenantId")
.HasDatabaseName("ix_auth_sessions_tenant_id");
b.HasIndex("TokenHash")
.IsUnique()
@@ -12801,11 +13076,17 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.HasIndex("UserId")
.HasDatabaseName("ix_auth_sessions_user_id");
b.HasIndex("TenantId", "UserId", "ExpiresAt")
.HasDatabaseName("ix_auth_sessions_tenant_id_user_id_expires_at")
b.HasIndex("TokenFamilyId", "RevokedAt")
.HasDatabaseName("ix_auth_sessions_token_family_id_revoked_at");
b.HasIndex("Realm", "TenantId", "UserId", "ExpiresAt")
.HasDatabaseName("ix_auth_sessions_realm_tenant_id_user_id_expires_at")
.HasFilter("revoked_at is null");
b.ToTable("auth_sessions", (string)null);
b.ToTable("auth_sessions", null, t =>
{
t.HasCheckConstraint("ck_auth_sessions_realm_tenant", "(realm = 'tenant' and tenant_id is not null) or (realm = 'platform' and tenant_id is null)");
});
});
modelBuilder.Entity("Tiku.Domain.Tenancy.SmsSendRateLimit", b =>
@@ -12931,9 +13212,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.HasKey("Id")
.HasName("pk_sms_verification_codes");
b.HasAlternateKey("TenantId", "Id")
.HasName("ak_sms_verification_codes_tenant_id_id");
b.HasIndex("TenantId", "Phone", "Purpose")
.IsUnique()
.HasDatabaseName("ix_sms_verification_codes_tenant_id_phone_purpose")
@@ -13623,22 +13901,12 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasColumnType("character varying(50)")
.HasColumnName("legacy_role");
b.Property<JsonElement>("Permissions")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("permissions")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("role");
b.Property<Guid?>("RoleTemplateId")
.HasColumnType("uuid")
.HasColumnName("role_template_id");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
@@ -13668,9 +13936,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.HasIndex("UserId")
.HasDatabaseName("ix_tenant_memberships_user_id");
b.HasIndex("TenantId", "RoleTemplateId")
.HasDatabaseName("ix_tenant_memberships_tenant_id_role_template_id");
b.HasIndex("TenantId", "UserId", "Role")
.IsUnique()
.HasDatabaseName("ix_tenant_memberships_tenant_id_user_id_role");
@@ -13678,126 +13943,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.ToTable("tenant_memberships", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantRoleTemplate", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id")
.HasDefaultValueSql("gen_random_uuid()");
b.Property<string>("BaseRole")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("base_role");
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<Guid?>("CreatedBy")
.HasColumnType("uuid")
.HasColumnName("created_by");
b.Property<JsonElement>("DataScope")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("data_scope")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<JsonElement>("FieldPermissions")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("field_permissions")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<bool>("IsSystem")
.HasColumnType("boolean")
.HasColumnName("is_system");
b.Property<JsonElement>("MenuPermissions")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("menu_permissions")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<JsonElement>("ModulePermissions")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("module_permissions")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("name");
b.Property<JsonElement>("Permissions")
.ValueGeneratedOnAdd()
.HasColumnType("jsonb")
.HasColumnName("permissions")
.HasDefaultValueSql("'{}'::jsonb");
b.Property<int>("SortOrder")
.HasColumnType("integer")
.HasColumnName("sort_order");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("status");
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.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid")
.HasColumnName("updated_by");
b.HasKey("Id")
.HasName("pk_tenant_role_templates");
b.HasAlternateKey("TenantId", "Id")
.HasName("ak_tenant_role_templates_tenant_id_id");
b.HasIndex("CreatedBy")
.HasDatabaseName("ix_tenant_role_templates_created_by");
b.HasIndex("UpdatedBy")
.HasDatabaseName("ix_tenant_role_templates_updated_by");
b.HasIndex("TenantId", "Code")
.IsUnique()
.HasDatabaseName("ix_tenant_role_templates_tenant_id_code");
b.HasIndex("TenantId", "Status", "SortOrder")
.HasDatabaseName("ix_tenant_role_templates_tenant_id_status_sort_order");
b.ToTable("tenant_role_templates", (string)null);
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantSecret", b =>
{
b.Property<Guid>("Id")
@@ -14153,6 +14298,36 @@ namespace Tiku.Infrastructure.Persistence.Migrations
b.ToTable("tenant_student_notes", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("Tiku.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_user_claims_users_user_id");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("Tiku.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_user_logins_users_user_id");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("Tiku.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_user_tokens_users_user_id");
});
modelBuilder.Entity("Tiku.Domain.Catalog.Category", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
@@ -17204,12 +17379,28 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.HasConstraintName("fk_question_versions_questions_tenant_id_question_id");
});
modelBuilder.Entity("Tiku.Domain.Tenancy.AuthLoginEvent", b =>
modelBuilder.Entity("Tiku.Domain.Tenancy.AuthChallenge", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_auth_challenges_tenants_tenant_id");
b.HasOne("Tiku.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_auth_challenges_users_user_id");
});
modelBuilder.Entity("Tiku.Domain.Tenancy.AuthLoginEvent", b =>
{
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.SetNull)
.IsRequired()
.HasConstraintName("fk_auth_login_events_tenants_tenant_id");
@@ -17226,7 +17417,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_auth_sessions_tenants_tenant_id");
b.HasOne("Tiku.Domain.Identity.User", null)
@@ -17375,35 +17565,6 @@ namespace Tiku.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_tenant_memberships_users_user_id");
b.HasOne("Tiku.Domain.Tenancy.TenantRoleTemplate", null)
.WithMany()
.HasForeignKey("TenantId", "RoleTemplateId")
.HasPrincipalKey("TenantId", "Id")
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_tenant_memberships_tenant_role_templates_tenant_id_role_tem~");
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantRoleTemplate", b =>
{
b.HasOne("Tiku.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("CreatedBy")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("fk_tenant_role_templates_users_created_by");
b.HasOne("Tiku.Domain.Tenancy.Tenant", null)
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_tenant_role_templates_tenants_tenant_id");
b.HasOne("Tiku.Domain.Identity.User", null)
.WithMany()
.HasForeignKey("UpdatedBy")
.OnDelete(DeleteBehavior.SetNull)
.HasConstraintName("fk_tenant_role_templates_users_updated_by");
});
modelBuilder.Entity("Tiku.Domain.Tenancy.TenantSecret", b =>