Optimized the create tenant API

This commit is contained in:
ashutosh.nehete 2025-07-31 18:56:19 +05:30
parent b7d770716a
commit e565a80f7a

View File

@ -3,6 +3,7 @@ using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Dtos.Tenant;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Roles; using Marco.Pms.Model.Roles;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Activities;
@ -31,18 +32,22 @@ namespace Marco.Pms.Services.Controllers
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly UserHelper _userHelper;
private readonly static Guid activeStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191"); private readonly static Guid activeStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191");
private readonly static string AdminRoleName = "Admin";
public TenantController(IDbContextFactory<ApplicationDbContext> dbContextFactory, public TenantController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
ILoggingService logger, ILoggingService logger,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IMapper mapper) IMapper mapper,
UserHelper userHelper)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_logger = logger; _logger = logger;
_userManager = userManager; _userManager = userManager;
_mapper = mapper; _mapper = mapper;
_userHelper = userHelper;
} }
// GET: api/<TenantController> // GET: api/<TenantController>
[HttpGet] [HttpGet]
@ -60,45 +65,65 @@ namespace Marco.Pms.Services.Controllers
// POST api/<TenantController> // POST api/<TenantController>
[HttpPost("create")] [HttpPost("create")]
public async Task<IActionResult> Post([FromBody] CreateTenantDto model) public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto model)
{ {
using var scope = _serviceScopeFactory.CreateScope(); using var scope = _serviceScopeFactory.CreateScope();
await using var _context = await _dbContextFactory.CreateDbContextAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync();
var _configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>(); var _configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var _emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>(); var _emailSender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var userHelper = scope.ServiceProvider.GetRequiredService<UserHelper>(); _logger.LogInfo("Attempting to create a new tenant with organization name: {OrganizationName}", model.OragnizationName);
var loggedInEmployee = await userHelper.GetCurrentEmployeeAsync();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>(); // 1. --- PERMISSION CHECK ---
var hasPermission = await permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (!hasPermission || !(loggedInEmployee.ApplicationUser?.IsRootUser ?? false)) if (loggedInEmployee == null)
{ {
_logger.LogWarning("User {EmployeeId} attmpted to create new tenant but not have permissions", loggedInEmployee.Id); // This case should ideally be handled by an [Authorize] attribute, but it's good practice to double-check.
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "User don't have rights for this action", 403)); return Unauthorized(ApiResponse<object>.ErrorResponse("Authentication required", "User is not logged in.", 401));
} }
var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
if (!hasPermission || !(loggedInEmployee.ApplicationUser?.IsRootUser ?? false))
{
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to create a tenant without sufficient rights.", loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
}
// 2. --- VALIDATION ---
// Check if a user with the same email already exists.
var existingUser = await _userManager.FindByEmailAsync(model.Email); var existingUser = await _userManager.FindByEmailAsync(model.Email);
if (existingUser != null) if (existingUser != null)
{ {
_logger.LogWarning("User {EmployeeId} attempted to create tenant with email {Email} but already exists in database", loggedInEmployee.Id, model.Email); _logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", model.Email);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant can't be created", "User with same email already exists", 409)); return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409));
} }
var isTenantExists = await _context.Tenants.AnyAsync(t => t.TaxId != null && t.TaxId == model.TaxId);
if (isTenantExists) // Check if a tenant with the same Tax ID already exists.
if (!string.IsNullOrWhiteSpace(model.TaxId))
{ {
_logger.LogWarning("User {EmployeeId} attempted to create tenant with duplicate taxId", loggedInEmployee.Id); var isTenantExists = await _context.Tenants.AnyAsync(t => t.TaxId == model.TaxId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant can't be created", "User with same taxId already exists", 409)); if (isTenantExists)
{
_logger.LogWarning("Tenant creation failed for Tax ID {TaxId}: a tenant with this Tax ID already exists.", model.TaxId);
return StatusCode(409, ApiResponse<object>.ErrorResponse("Tenant cannot be created", "A tenant with the same Tax ID already exists.", 409));
}
} }
// Check if the provided logo is a valid Base64 string.
if (!string.IsNullOrWhiteSpace(model.logoImage) && !IsBase64String(model.logoImage)) if (!string.IsNullOrWhiteSpace(model.logoImage) && !IsBase64String(model.logoImage))
{ {
_logger.LogWarning("User {EmployeeId} attempted to create tenant with Invalid logoImage", loggedInEmployee.Id); _logger.LogWarning("Tenant creation failed for user {EmployeeId}: The provided logo image was not a valid Base64 string.", loggedInEmployee.Id);
return StatusCode(400, ApiResponse<object>.ErrorResponse("Tenant can't be created", "User with same taxId already exists", 400)); return StatusCode(400, ApiResponse<object>.ErrorResponse("Tenant cannot be created", "The provided logo image is invalid.", 400));
} }
// 3. --- DATABASE TRANSACTION ---
// Use a transaction to ensure all related entities are created successfully or none at all.
await using var transaction = await _context.Database.BeginTransactionAsync(); await using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
// Create the primary Tenant entity
var tenant = new Tenant var tenant = new Tenant
{ {
Name = model.OragnizationName, Name = model.OragnizationName,
@ -106,7 +131,7 @@ namespace Marco.Pms.Services.Controllers
ContactNumber = model.ContactNumber, ContactNumber = model.ContactNumber,
Email = model.Email, Email = model.Email,
IndustryId = model.IndustryId, IndustryId = model.IndustryId,
TenantStatusId = activeStatus, TenantStatusId = activeStatus, // Assuming 'activeStatus' is a defined variable or constant
Description = model.Description, Description = model.Description,
OnBoardingDate = model.OnBoardingDate, OnBoardingDate = model.OnBoardingDate,
OragnizationSize = model.OragnizationSize, OragnizationSize = model.OragnizationSize,
@ -118,32 +143,42 @@ namespace Marco.Pms.Services.Controllers
DomainName = model.DomainName, DomainName = model.DomainName,
IsSuperTenant = false IsSuperTenant = false
}; };
_context.Tenants.Add(tenant); _context.Tenants.Add(tenant);
await _context.SaveChangesAsync(); // Create the root ApplicationUser for the new tenant
var designation = new JobRole
{
Name = "Admin",
Description = "Root degination for tenant only",
TenantId = tenant.Id
};
var applicationUser = new ApplicationUser var applicationUser = new ApplicationUser
{ {
Email = model.Email, Email = model.Email,
UserName = model.Email, UserName = model.Email, // Best practice to use email as username for simplicity
IsRootUser = true, IsRootUser = true,
EmailConfirmed = true, EmailConfirmed = true, // Auto-confirming email as it's part of a trusted setup process
TenantId = tenant.Id TenantId = tenant.Id
}; };
_context.JobRoles.Add(designation);
var result = await _userManager.CreateAsync(applicationUser, "User@123"); // SECURITY WARNING: Hardcoded passwords are a major vulnerability.
// Replace "User@123" with a securely generated random password.
var initialPassword = "User@123"; // TODO: Replace with password generation service.
var result = await _userManager.CreateAsync(applicationUser, initialPassword);
if (!result.Succeeded) if (!result.Succeeded)
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", result.Errors, 400)); {
// If user creation fails, roll back the transaction immediately and return the errors.
await transaction.RollbackAsync();
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("Failed to create ApplicationUser for tenant {TenantName}. Errors: {Errors}", model.OragnizationName, string.Join(", ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to create user", errors, 400));
}
await _context.SaveChangesAsync(); // Create the default "Admin" Job Role for the tenant
var adminJobRole = new JobRole
{
Name = AdminRoleName,
Description = "Default administrator role for the tenant.",
TenantId = tenant.Id
};
_context.JobRoles.Add(adminJobRole);
// Create the primary Employee record and link it to the ApplicationUser and JobRole
var employeeUser = new Employee var employeeUser = new Employee
{ {
FirstName = model.FirstName, FirstName = model.FirstName,
@ -151,32 +186,57 @@ namespace Marco.Pms.Services.Controllers
Email = model.Email, Email = model.Email,
PhoneNumber = model.ContactNumber, PhoneNumber = model.ContactNumber,
ApplicationUserId = applicationUser.Id, ApplicationUserId = applicationUser.Id,
JobRole = designation, JobRole = adminJobRole, // Link to the newly created role
CurrentAddress = model.BillingAddress, CurrentAddress = model.BillingAddress,
TenantId = tenant.Id TenantId = tenant.Id
}; };
_context.Employees.Add(employeeUser);
// Create a default project for the new tenant
var project = new Project
{
Name = $"{model.OragnizationName} - Default Project",
ProjectStatusId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"), // Consider using a constant for this GUID
ProjectAddress = model.BillingAddress,
ContactPerson = tenant.ContactName,
TenantId = tenant.Id
};
_context.Projects.Add(project);
// All entities are now added to the context. Save them all in a single database operation.
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
// 4. --- POST-CREATION ACTIONS ---
// Generate a password reset token so the new user can set their own password.
_logger.LogInfo("User {Email} created. Sending password setup email.", applicationUser.Email);
var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser); var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser);
var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}&email={WebUtility.UrlEncode(applicationUser.Email)}";
if (employeeUser.FirstName != null) await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink);
{
await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink); // Map the result to a ViewModel for the API response.
} var tenantVM = _mapper.Map<TenantVM>(tenant);
var vm = _mapper.Map<TenantVM>(tenant); tenantVM.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
vm.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
return Ok(tenant); // Commit the transaction as all operations were successful.
await transaction.CommitAsync();
_logger.LogInfo("Successfully created tenant {TenantId} for organization {OrganizationName}.", tenant.Id, tenant.Name);
return StatusCode(201, ApiResponse<TenantVM>.SuccessResponse(tenantVM, "Tenant created successfully.", 201));
} }
catch (DbUpdateException dbEx) catch (DbUpdateException dbEx)
{ {
_logger.LogError(dbEx, "Database Exception occured while creating tenant"); await transaction.RollbackAsync();
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500)); // Log the detailed database exception, including the inner exception if available.
_logger.LogError(dbEx, "A database update exception occurred while creating tenant for email {Email}. Inner Exception: {InnerException}",
model.Email, dbEx.InnerException?.Message ?? string.Empty);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Exception occured while creating tenant"); await transaction.RollbackAsync();
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500)); // Log the general exception.
_logger.LogError(ex, "An unexpected exception occurred while creating tenant for email {Email}.", model.Email);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500));
} }
} }