216 lines
9.5 KiB
C#

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<StartupUserSeeder> _logger;
// Constants to avoid "magic strings"
private const string AdminJobRoleName = "Admin";
private const string SuperUserRoleName = "Super User";
public StartupUserSeeder(IServiceProvider serviceProvider, ILogger<StartupUserSeeder> 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<UserManager<ApplicationUser>>();
var dbContext = serviceProvider.GetRequiredService<ApplicationDbContext>();
var adminSettings = serviceProvider.GetRequiredService<IOptions<SuperAdminSettings>>().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<ApplicationUser> SeedSuperAdminUserAsync(UserManager<ApplicationUser> 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,
TenantId = tenantId
};
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<Guid>(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.");
}
}
/// <summary>
/// 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.
/// </summary>
private async Task<T> GetOrCreateAsync<T>(DbSet<T> dbSet, Expression<Func<T, bool>> predicate, Func<T> 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;
}
}