using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Roles; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; // For configuration using System.Linq.Expressions; namespace Marco.Pms.Services.Service { // A configuration class to hold settings from appsettings.json // This avoids hardcoding sensitive information. public class SuperAdminSettings { public const string CONFIG_SECTION_NAME = "SuperAdminAccount"; public string Email { get; set; } = "admin@marcoaiot.com"; public string Password { get; set; } = "User@123"; public string TenantId { get; set; } = "b3466e83-7e11-464c-b93a-daf047838b26"; } public class StartupUserSeeder : IHostedService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; // Constants to avoid "magic strings" private const string AdminJobRoleName = "Admin"; private const string SuperUserRoleName = "Super User"; public StartupUserSeeder(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting database seeding process..."); using var scope = _serviceProvider.CreateScope(); var serviceProvider = scope.ServiceProvider; // Get services from the scoped provider var userManager = serviceProvider.GetRequiredService>(); var dbContext = serviceProvider.GetRequiredService(); var adminSettings = serviceProvider.GetRequiredService>().Value; var tenantId = Guid.Parse(adminSettings.TenantId); // Use a database transaction to ensure all operations succeed or none do. await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); try { // 1. Seed the Application User (Super Admin) var user = await SeedSuperAdminUserAsync(userManager, adminSettings, tenantId); // 2. Seed the Job Role var jobRole = await GetOrCreateAsync( dbContext.JobRoles, j => j.Name == AdminJobRoleName && j.TenantId == tenantId, () => new JobRole { Name = AdminJobRoleName, Description = "Administrator with full system access.", TenantId = tenantId }); // 3. Seed the Application Role var appRole = await GetOrCreateAsync( dbContext.ApplicationRoles, a => a.Role == SuperUserRoleName && a.TenantId == tenantId, () => new ApplicationRole { Role = SuperUserRoleName, Description = "System role with all permissions.", IsSystem = true, TenantId = tenantId }); // 4. Seed the Employee record linked to the user and job role var employee = await GetOrCreateAsync( dbContext.Employees, e => e.Email == adminSettings.Email && e.TenantId == tenantId, () => new Employee { ApplicationUserId = user.Id, FirstName = "Admin", LastName = "User", Email = adminSettings.Email, TenantId = tenantId, PhoneNumber = "9876543210", JobRoleId = jobRole.Id, IsSystem = true, JoiningDate = DateTime.UtcNow, BirthDate = new DateTime(1970, 1, 1) // Set other non-nullable fields to sensible defaults }); // 5. Seed the Employee-Role Mapping await GetOrCreateAsync( dbContext.EmployeeRoleMappings, erm => erm.EmployeeId == employee.Id && erm.RoleId == appRole.Id, () => new EmployeeRoleMapping { EmployeeId = employee.Id, RoleId = appRole.Id, TenantId = tenantId, IsEnabled = true }); // 6. Seed Role Permissions (Efficiently) await SeedRolePermissionsAsync(dbContext, appRole.Id); // All entities are now tracked by the DbContext. // A single SaveChanges call is more efficient. await dbContext.SaveChangesAsync(cancellationToken); // If all operations were successful, commit the transaction. await transaction.CommitAsync(cancellationToken); _logger.LogInformation("Database seeding process completed successfully."); } catch (Exception ex) { _logger.LogError(ex, "An error occurred during database seeding. Rolling back changes."); await transaction.RollbackAsync(cancellationToken); // Optionally re-throw or handle the exception as needed throw; } } private async Task SeedSuperAdminUserAsync(UserManager userManager, SuperAdminSettings settings, Guid tenantId) { _logger.LogInformation("Seeding Super Admin user: {Email}", settings.Email); var user = await userManager.FindByEmailAsync(settings.Email); if (user == null) { user = new ApplicationUser { UserName = settings.Email, Email = settings.Email, EmailConfirmed = true, IsRootUser = true }; var result = await userManager.CreateAsync(user, settings.Password); if (!result.Succeeded) { // If user creation fails, it's a critical error. var errors = string.Join(", ", result.Errors.Select(e => e.Description)); _logger.LogError("Failed to create super admin user. Errors: {Errors}", errors); throw new InvalidOperationException($"Failed to create super admin user: {errors}"); } _logger.LogInformation("Super Admin user created successfully."); } else { _logger.LogInformation("Super Admin user already exists."); } return user; } private async Task SeedRolePermissionsAsync(ApplicationDbContext dbContext, Guid superUserRoleId) { _logger.LogInformation("Seeding permissions for Super User role (ID: {RoleId})", superUserRoleId); var allPermissionIds = await dbContext.FeaturePermissions .Select(p => p.Id) .ToListAsync(); var permissionIdsFromDb = await dbContext.RolePermissionMappings .Where(pm => pm.ApplicationRoleId == superUserRoleId) .Select(pm => pm.FeaturePermissionId) .ToListAsync(); // 1. Fetch data from DB into a List var existingPermissionIds = new HashSet(permissionIdsFromDb); // 2. Convert the List to a HashSet in memory var missingPermissionIds = allPermissionIds.Except(existingPermissionIds).ToList(); if (missingPermissionIds.Any()) { var newMappings = missingPermissionIds.Select(permissionId => new RolePermissionMappings { ApplicationRoleId = superUserRoleId, FeaturePermissionId = permissionId }); await dbContext.RolePermissionMappings.AddRangeAsync(newMappings); _logger.LogInformation("Added {Count} new permission mappings to the Super User role.", missingPermissionIds.Count); } else { _logger.LogInformation("Super User role already has all available permissions."); } } /// /// A generic helper to find an entity by a predicate or create, add, and return it if not found. /// This promotes code reuse and makes the main logic cleaner. /// private async Task GetOrCreateAsync(DbSet dbSet, Expression> predicate, Func factory) where T : class { var entity = await dbSet.FirstOrDefaultAsync(predicate); if (entity == null) { entity = factory(); await dbSet.AddAsync(entity); _logger.LogInformation("Creating new entity of type {EntityType}.", typeof(T).Name); } return entity; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } }