using System.Security.Cryptography; using System.Text.Json; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Npgsql; using Tiku.Application.Security; using Tiku.Domain.Identity; using Tiku.Domain.Operations; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Bootstrap; public static class DevelopmentPlatformAdminSeeder { public const string Email = "admin@tiku.local"; public const string RoleCode = PlatformAdminBootstrapper.SuperAdminRoleCode; public static void Configure(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSeeding((context, _) => Seed((TikuDbContext)context)) .UseAsyncSeeding((context, _, cancellationToken) => SeedAsync((TikuDbContext)context, cancellationToken)); } public static bool Seed(TikuDbContext dbContext) { ReloadPostgresTypes(dbContext); if (dbContext.PlatformBackendUserRoles.Any()) { return false; } var temporaryPassword = GenerateTemporaryPassword(); EnsureEmailIsAvailable(dbContext.Users.Any(user => user.NormalizedEmail == Email.ToUpperInvariant() || user.NormalizedUserName == Email.ToUpperInvariant())); var permissionCodes = BackendPermissions.Platform.ToArray(); var existingPermissionCodes = dbContext.BackendPermissions .Where(permission => permissionCodes.Contains(permission.Code)) .Select(permission => permission.Code) .ToHashSet(StringComparer.Ordinal); AddSeedGraph(dbContext, temporaryPassword, permissionCodes, existingPermissionCodes); dbContext.SaveChanges(); WriteFirstLoginInstructions(temporaryPassword); return true; } public static async Task SeedAsync( TikuDbContext dbContext, CancellationToken cancellationToken = default) { await ReloadPostgresTypesAsync(dbContext, cancellationToken); if (await dbContext.PlatformBackendUserRoles.AnyAsync(cancellationToken)) { return false; } var temporaryPassword = GenerateTemporaryPassword(); var normalizedEmail = Email.ToUpperInvariant(); EnsureEmailIsAvailable(await dbContext.Users.AnyAsync(user => user.NormalizedEmail == normalizedEmail || user.NormalizedUserName == normalizedEmail, cancellationToken)); var permissionCodes = BackendPermissions.Platform.ToArray(); var existingPermissionCodes = (await dbContext.BackendPermissions .Where(permission => permissionCodes.Contains(permission.Code)) .Select(permission => permission.Code) .ToArrayAsync(cancellationToken)) .ToHashSet(StringComparer.Ordinal); AddSeedGraph(dbContext, temporaryPassword, permissionCodes, existingPermissionCodes); await dbContext.SaveChangesAsync(cancellationToken); WriteFirstLoginInstructions(temporaryPassword); return true; } private static void AddSeedGraph( TikuDbContext dbContext, string temporaryPassword, IReadOnlyCollection permissionCodes, IReadOnlySet existingPermissionCodes) { var normalizedEmail = Email.ToUpperInvariant(); var user = new User { Email = Email, NormalizedEmail = normalizedEmail, UserName = Email, NormalizedUserName = normalizedEmail, Name = "Local Platform Administrator", EmailConfirmed = true, Status = UserStatus.Active, ForcePasswordChange = true }; var passwordHasher = new PasswordHasher(Options.Create(new PasswordHasherOptions { IterationCount = 210_000 })); user.PasswordHash = passwordHasher.HashPassword(user, temporaryPassword); var role = new PlatformBackendRole { Code = RoleCode, Name = "Platform Super Administrator", Description = "Built-in Development administrator created by EF Core data seeding.", Status = BackendRoleStatus.Active, IsSystem = true }; dbContext.Users.Add(user); dbContext.PlatformBackendRoles.Add(role); dbContext.BackendPermissions.AddRange( permissionCodes .Where(code => !existingPermissionCodes.Contains(code)) .Select(code => new BackendPermission { Code = code, Name = code, Area = BackendPermissionArea.Platform, Module = "platform", Description = "Built-in platform permission.", IsSystem = true })); dbContext.PlatformBackendRolePermissions.AddRange( permissionCodes.Select(code => new PlatformBackendRolePermission { RoleId = role.Id, PermissionCode = code })); dbContext.PlatformBackendUserRoles.Add(new PlatformBackendUserRole { UserId = user.Id, RoleId = role.Id }); dbContext.AuditLogs.Add(new AuditLog { ActorUserId = user.Id, Action = "platform.bootstrap_admin.created", TargetType = "users", TargetId = user.Id.ToString(), Details = JsonSerializer.SerializeToElement(new { user.Email, RoleCode, ForcePasswordChange = true, Source = "ef_core_use_seeding" }) }); } private static string GenerateTemporaryPassword() => $"Tiku!{Convert.ToHexString(RandomNumberGenerator.GetBytes(16))}9a"; private static void ReloadPostgresTypes(TikuDbContext dbContext) { if (dbContext.Database.IsNpgsql()) { ((NpgsqlConnection)dbContext.Database.GetDbConnection()).ReloadTypes(); } } private static async Task ReloadPostgresTypesAsync( TikuDbContext dbContext, CancellationToken cancellationToken) { if (dbContext.Database.IsNpgsql()) { await ((NpgsqlConnection)dbContext.Database.GetDbConnection()) .ReloadTypesAsync(cancellationToken); } } private static void EnsureEmailIsAvailable(bool isAssigned) { if (isAssigned) { throw new InvalidOperationException( $"Cannot create the Development platform administrator because '{Email}' is already assigned."); } } private static void WriteFirstLoginInstructions(string temporaryPassword) { Console.WriteLine("Development platform administrator created by EF Core data seeding."); Console.WriteLine($" Account: {Email}"); Console.WriteLine($" Temporary password: {temporaryPassword}"); Console.WriteLine(" Change the temporary password at first sign-in. This password is shown only once."); } }