Created the API ot Create new Tenant

This commit is contained in:
ashutosh.nehete 2025-07-31 16:54:06 +05:30
parent 7fe8dc60cb
commit b7d770716a
13 changed files with 4543 additions and 180 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
namespace Marco.Pms.Model.Dtos.Tenant
{
public class CreateTenantDto
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
public required string Email { get; set; }
public string? Description { get; set; }
public string? DomainName { get; set; }
public required string BillingAddress { get; set; }
public string? TaxId { get; set; }
public string? logoImage { get; set; }
public required string OragnizationName { get; set; }
public required string ContactNumber { get; set; }
public required DateTime OnBoardingDate { get; set; }
public required string OragnizationSize { get; set; }
public required Guid IndustryId { get; set; }
public required string Reference { get; set; }
}
}

View File

@ -1,25 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using Marco.Pms.Model.Master;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Marco.Pms.Model.Entitlements
{
public class Tenant
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public string? DomainName { get; set; }
public string? ContactName { get; set; }
public string? ContactNumber { get; set; }
public DateTime OnBoardingDate { get; set; }
public string? OragnizationSize { get; set; }
public Guid? IndustryId { get; set; }
[ForeignKey("IndustryId")]
[ValidateNever]
public Industry? Industry { get; set; }
public bool IsActive { get; set; } = true;
}
}

View File

@ -2,6 +2,9 @@
{ {
public static class PermissionsMaster public static class PermissionsMaster
{ {
public static readonly Guid ManageTenants = Guid.Parse("d032cb1a-3f30-462c-bef0-7ace73a71c0b");
public static readonly Guid ModifyTenant = Guid.Parse("00e20637-ce8d-4417-bec4-9b31b5e65092");
public static readonly Guid ViewTenant = Guid.Parse("647145c6-2108-4c98-aab4-178602236e55");
public static readonly Guid DirectoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda"); public static readonly Guid DirectoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda");
public static readonly Guid DirectoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5"); public static readonly Guid DirectoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5");
public static readonly Guid DirectoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb"); public static readonly Guid DirectoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb");

View File

@ -0,0 +1,37 @@
using Marco.Pms.Model.Master;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.Entitlements
{
public class Tenant
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string? Description { get; set; }
public string? DomainName { get; set; }
public string ContactName { get; set; } = string.Empty;
public string ContactNumber { get; set; } = string.Empty;
public string BillingAddress { get; set; } = string.Empty;
public string? TaxId { get; set; }
public string? logoImage { get; set; } // Base64
public DateTime OnBoardingDate { get; set; }
public string? OragnizationSize { get; set; }
public Guid? IndustryId { get; set; }
[ForeignKey("IndustryId")]
[ValidateNever]
public Industry? Industry { get; set; }
public Guid? CreatedById { get; set; } // EmployeeId
public Guid TenantStatusId { get; set; }
[ForeignKey("TenantStatusId")]
[ValidateNever]
public TenantStatus? TenantStatus { get; set; }
public string Reference { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public bool IsSuperTenant { get; set; } = false;
}
}

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.Master
{
public class TenantStatus
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,28 @@
using Marco.Pms.Model.Master;
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Tenant
{
public class TenantVM
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string? Description { get; set; }
public string? DomainName { get; set; }
public string ContactName { get; set; } = string.Empty;
public string ContactNumber { get; set; } = string.Empty;
public string BillingAddress { get; set; } = string.Empty;
public string? TaxId { get; set; }
public string? logoImage { get; set; } // Base64
public DateTime OnBoardingDate { get; set; }
public string? OragnizationSize { get; set; }
public Industry? Industry { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; } // EmployeeId
public TenantStatus? TenantStatus { get; set; }
public string Reference { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public bool IsSuperTenant { get; set; } = false;
}
}

View File

@ -0,0 +1,242 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Tenant;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Roles;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Tenant;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Net;
using System.Text.RegularExpressions;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class TenantController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILoggingService _logger;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IMapper _mapper;
private readonly static Guid activeStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191");
public TenantController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
UserManager<ApplicationUser> userManager,
IMapper mapper)
{
_dbContextFactory = dbContextFactory;
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
_userManager = userManager;
_mapper = mapper;
}
// GET: api/<TenantController>
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<TenantController>/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
// POST api/<TenantController>
[HttpPost("create")]
public async Task<IActionResult> Post([FromBody] CreateTenantDto model)
{
using var scope = _serviceScopeFactory.CreateScope();
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var _configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var _emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
var userHelper = scope.ServiceProvider.GetRequiredService<UserHelper>();
var loggedInEmployee = await userHelper.GetCurrentEmployeeAsync();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
if (!hasPermission || !(loggedInEmployee.ApplicationUser?.IsRootUser ?? false))
{
_logger.LogWarning("User {EmployeeId} attmpted to create new tenant but not have permissions", loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "User don't have rights for this action", 403));
}
var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser != null)
{
_logger.LogWarning("User {EmployeeId} attempted to create tenant with email {Email} but already exists in database", loggedInEmployee.Id, model.Email);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant can't be created", "User with same email already exists", 409));
}
var isTenantExists = await _context.Tenants.AnyAsync(t => t.TaxId != null && t.TaxId == model.TaxId);
if (isTenantExists)
{
_logger.LogWarning("User {EmployeeId} attempted to create tenant with duplicate taxId", loggedInEmployee.Id);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant can't be created", "User with same taxId already exists", 409));
}
if (!string.IsNullOrWhiteSpace(model.logoImage) && !IsBase64String(model.logoImage))
{
_logger.LogWarning("User {EmployeeId} attempted to create tenant with Invalid logoImage", loggedInEmployee.Id);
return StatusCode(400, ApiResponse<object>.ErrorResponse("Tenant can't be created", "User with same taxId already exists", 400));
}
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var tenant = new Tenant
{
Name = model.OragnizationName,
ContactName = $"{model.FirstName} {model.LastName}",
ContactNumber = model.ContactNumber,
Email = model.Email,
IndustryId = model.IndustryId,
TenantStatusId = activeStatus,
Description = model.Description,
OnBoardingDate = model.OnBoardingDate,
OragnizationSize = model.OragnizationSize,
Reference = model.Reference,
CreatedById = loggedInEmployee.Id,
BillingAddress = model.BillingAddress,
TaxId = model.TaxId,
logoImage = model.logoImage,
DomainName = model.DomainName,
IsSuperTenant = false
};
_context.Tenants.Add(tenant);
await _context.SaveChangesAsync();
var designation = new JobRole
{
Name = "Admin",
Description = "Root degination for tenant only",
TenantId = tenant.Id
};
var applicationUser = new ApplicationUser
{
Email = model.Email,
UserName = model.Email,
IsRootUser = true,
EmailConfirmed = true,
TenantId = tenant.Id
};
_context.JobRoles.Add(designation);
var result = await _userManager.CreateAsync(applicationUser, "User@123");
if (!result.Succeeded)
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", result.Errors, 400));
await _context.SaveChangesAsync();
var employeeUser = new Employee
{
FirstName = model.FirstName,
LastName = model.LastName,
Email = model.Email,
PhoneNumber = model.ContactNumber,
ApplicationUserId = applicationUser.Id,
JobRole = designation,
CurrentAddress = model.BillingAddress,
TenantId = tenant.Id
};
await _context.SaveChangesAsync();
var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}";
if (employeeUser.FirstName != null)
{
await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink);
}
var vm = _mapper.Map<TenantVM>(tenant);
vm.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
return Ok(tenant);
}
catch (DbUpdateException dbEx)
{
_logger.LogError(dbEx, "Database Exception occured while creating tenant");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while creating tenant");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500));
}
}
// PUT api/<TenantController>/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/<TenantController>/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
private static object ExceptionMapper(Exception ex)
{
return new
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
InnerException = new
{
Message = ex.InnerException?.Message,
StackTrace = ex.InnerException?.StackTrace,
Source = ex.InnerException?.Source,
}
};
}
private bool IsBase64String(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return false;
// Normalize string
input = input.Trim();
// Length must be multiple of 4
if (input.Length % 4 != 0)
return false;
// Valid Base64 characters with correct padding
var base64Regex = new Regex(@"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$");
if (!base64Regex.IsMatch(input))
return false;
try
{
// Decode and re-encode to confirm validity
var bytes = Convert.FromBase64String(input);
var reEncoded = Convert.ToBase64String(bytes);
return input == reEncoded;
}
catch
{
return false;
}
}
}
}

View File

@ -1,11 +1,14 @@
using AutoMapper; using AutoMapper;
using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Master; using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Employee;
using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Model.ViewModels.Tenant;
namespace Marco.Pms.Services.MappingProfiles namespace Marco.Pms.Services.MappingProfiles
{ {
@ -13,6 +16,10 @@ namespace Marco.Pms.Services.MappingProfiles
{ {
public MappingProfile() public MappingProfile()
{ {
#region ======================================================= Employees =======================================================
CreateMap<Tenant, TenantVM>();
#endregion
#region ======================================================= Projects ======================================================= #region ======================================================= Projects =======================================================
// Your mappings // Your mappings
CreateMap<Project, ProjectVM>(); CreateMap<Project, ProjectVM>();
@ -60,8 +67,9 @@ namespace Marco.Pms.Services.MappingProfiles
opt => opt.MapFrom(src => src.Comment)); opt => opt.MapFrom(src => src.Comment));
#endregion #endregion
#region ======================================================= Projects ======================================================= #region ======================================================= Employees =======================================================
CreateMap<Employee, EmployeeVM>(); CreateMap<Employee, EmployeeVM>();
CreateMap<Employee, BasicEmployeeVM>();
#endregion #endregion
} }
} }

View File

@ -4,134 +4,211 @@ using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Roles; using Marco.Pms.Model.Roles;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; // For configuration
using System.Linq.Expressions;
namespace Marco.Pms.Services.Service 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 public class StartupUserSeeder : IHostedService
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ILogger<StartupUserSeeder> _logger;
public StartupUserSeeder(IServiceProvider serviceProvider) // 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; _serviceProvider = serviceProvider;
_logger = logger;
} }
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Starting database seeding process...");
using var scope = _serviceProvider.CreateScope(); using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var serviceProvider = scope.ServiceProvider;
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userEmail = "admin@marcoaiot.com"; // 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);
var user = await userManager.FindByEmailAsync(userEmail); // Use a database transaction to ensure all operations succeed or none do.
var newUser = new ApplicationUser await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try
{ {
UserName = userEmail, // 1. Seed the Application User (Super Admin)
Email = userEmail, var user = await SeedSuperAdminUserAsync(userManager, adminSettings, tenantId);
EmailConfirmed = true,
IsRootUser = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
};
var result = new IdentityResult();
// 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) if (user == null)
{ {
result = await userManager.CreateAsync(newUser, "User@123"); user = new ApplicationUser
}
else
{
newUser = user;
}
var jobRole = new JobRole
{
Id = Guid.Empty,
Name = "Admin",
Description = "Admin",
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"),
};
if (!await dbContext.JobRoles.Where(j => j.Name == "Admin").AnyAsync())
{
await dbContext.JobRoles.AddAsync(jobRole);
}
else
{
jobRole = await dbContext.JobRoles.Where(j => j.Name == "Admin").FirstOrDefaultAsync();
}
var role = new ApplicationRole
{
Role = "Super User",
Description = "Super User",
IsSystem = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
};
if (!await dbContext.ApplicationRoles.Where(a => a.Role == "Super User").AnyAsync())
{
await dbContext.ApplicationRoles.AddAsync(role);
}
else
{
role = await dbContext.ApplicationRoles.Where(a => a.Role == "Super User").FirstOrDefaultAsync();
}
await dbContext.SaveChangesAsync();
var employee = new Employee
{
ApplicationUserId = newUser.Id,
FirstName = "Admin",
LastName = "",
Email = userEmail,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"),
CurrentAddress = "",
BirthDate = Convert.ToDateTime("1965-04-20 10:11:17.588000"),
EmergencyPhoneNumber = "",
EmergencyContactPerson = "",
AadharNumber = "",
Gender = "",
MiddleName = "",
PanNumber = "",
PermanentAddress = "",
PhoneNumber = "9876543210",
Photo = null, // GetFileDetails(model.Photo).Result.FileData,
JobRoleId = jobRole != null ? jobRole.Id : Guid.Empty,
IsSystem = true,
JoiningDate = Convert.ToDateTime("2000-04-20 10:11:17.588000"),
};
if ((!await dbContext.Employees.Where(e => e.Email == "admin@marcoaiot.com").AnyAsync()) && jobRole?.Id != Guid.Empty)
{
await dbContext.Employees.AddAsync(employee);
}
else
{
employee = await dbContext.Employees.Where(e => e.Email == "admin@marcoaiot.com").FirstOrDefaultAsync();
}
await dbContext.SaveChangesAsync();
if (!await dbContext.EmployeeRoleMappings.AnyAsync())
{
await dbContext.EmployeeRoleMappings.AddAsync(new EmployeeRoleMapping
{ {
EmployeeId = employee?.Id ?? Guid.Empty, UserName = settings.Email,
IsEnabled = true, Email = settings.Email,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"), EmailConfirmed = true,
RoleId = role?.Id ?? Guid.Empty IsRootUser = true,
}); TenantId = tenantId
} };
if (!await dbContext.RolePermissionMappings.AnyAsync()) var result = await userManager.CreateAsync(user, settings.Password);
{
List<FeaturePermission> permissions = await dbContext.FeaturePermissions.ToListAsync(); if (!result.Succeeded)
List<RolePermissionMappings> rolesMapping = new List<RolePermissionMappings>();
foreach (var permission in permissions)
{ {
rolesMapping.Add(new RolePermissionMappings // If user creation fails, it's a critical error.
{ var errors = string.Join(", ", result.Errors.Select(e => e.Description));
ApplicationRoleId = role != null ? role.Id : Guid.Empty, _logger.LogError("Failed to create super admin user. Errors: {Errors}", errors);
FeaturePermissionId = permission.Id throw new InvalidOperationException($"Failed to create super admin user: {errors}");
});
} }
await dbContext.RolePermissionMappings.AddRangeAsync(rolesMapping); _logger.LogInformation("Super Admin user created successfully.");
} }
await dbContext.SaveChangesAsync(); 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; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;