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.Projects; 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 _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILoggingService _logger; private readonly UserManager _userManager; private readonly IMapper _mapper; private readonly UserHelper _userHelper; private readonly static Guid activeStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191"); private readonly static string AdminRoleName = "Admin"; public TenantController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, ILoggingService logger, UserManager userManager, IMapper mapper, UserHelper userHelper) { _dbContextFactory = dbContextFactory; _serviceScopeFactory = serviceScopeFactory; _logger = logger; _userManager = userManager; _mapper = mapper; _userHelper = userHelper; } // GET: api/ [HttpGet] public IEnumerable Get() { return new string[] { "value1", "value2" }; } // GET api//5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/ [HttpPost("create")] public async Task CreateTenant([FromBody] CreateTenantDto model) { using var scope = _serviceScopeFactory.CreateScope(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); var _configuration = scope.ServiceProvider.GetRequiredService(); var _emailSender = scope.ServiceProvider.GetRequiredService(); var _permissionService = scope.ServiceProvider.GetRequiredService(); _logger.LogInfo("Attempting to create a new tenant with organization name: {OrganizationName}", model.OragnizationName); // 1. --- PERMISSION CHECK --- var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { // This case should ideally be handled by an [Authorize] attribute, but it's good practice to double-check. return Unauthorized(ApiResponse.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.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); if (existingUser != null) { _logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", model.Email); return StatusCode(409, ApiResponse.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409)); } // Check if a tenant with the same Tax ID already exists. if (!string.IsNullOrWhiteSpace(model.TaxId)) { var isTenantExists = await _context.Tenants.AnyAsync(t => t.TaxId == model.TaxId); 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.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)) { _logger.LogWarning("Tenant creation failed for user {EmployeeId}: The provided logo image was not a valid Base64 string.", loggedInEmployee.Id); return StatusCode(400, ApiResponse.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(); try { // Create the primary Tenant entity var tenant = new Tenant { Name = model.OragnizationName, ContactName = $"{model.FirstName} {model.LastName}", ContactNumber = model.ContactNumber, Email = model.Email, IndustryId = model.IndustryId, TenantStatusId = activeStatus, // Assuming 'activeStatus' is a defined variable or constant 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); // Create the root ApplicationUser for the new tenant var applicationUser = new ApplicationUser { Email = model.Email, UserName = model.Email, // Best practice to use email as username for simplicity IsRootUser = true, EmailConfirmed = true, // Auto-confirming email as it's part of a trusted setup process TenantId = tenant.Id }; // 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 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.ErrorResponse("Failed to create user", errors, 400)); } // 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 { FirstName = model.FirstName, LastName = model.LastName, Email = model.Email, PhoneNumber = model.ContactNumber, ApplicationUserId = applicationUser.Id, JobRole = adminJobRole, // Link to the newly created role CurrentAddress = model.BillingAddress, 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(); // 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 resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}&email={WebUtility.UrlEncode(applicationUser.Email)}"; await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink); // Map the result to a ViewModel for the API response. var tenantVM = _mapper.Map(tenant); tenantVM.CreatedBy = _mapper.Map(loggedInEmployee); // 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.SuccessResponse(tenantVM, "Tenant created successfully.", 201)); } catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); // 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.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500)); } catch (Exception ex) { await transaction.RollbackAsync(); // Log the general exception. _logger.LogError(ex, "An unexpected exception occurred while creating tenant for email {Email}.", model.Email); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500)); } } // PUT api//5 [HttpPut("{id}")] public void Put(int id, [FromBody] string value) { } // DELETE api//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; } } } }