Created the API ot Create new Tenant
This commit is contained in:
parent
7fe8dc60cb
commit
b7d770716a
File diff suppressed because one or more lines are too long
3547
Marco.Pms.DataAccess/Migrations/20250731100859_Added_New_Parameters_In_Tenant_Table.Designer.cs
generated
Normal file
3547
Marco.Pms.DataAccess/Migrations/20250731100859_Added_New_Parameters_In_Tenant_Table.Designer.cs
generated
Normal file
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
20
Marco.Pms.Model/Dtos/Tenant/CreateTenantDto.cs
Normal file
20
Marco.Pms.Model/Dtos/Tenant/CreateTenantDto.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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");
|
||||||
|
37
Marco.Pms.Model/Entitlements/Tenant.cs
Normal file
37
Marco.Pms.Model/Entitlements/Tenant.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
8
Marco.Pms.Model/Master/TenantStatus.cs
Normal file
8
Marco.Pms.Model/Master/TenantStatus.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
28
Marco.Pms.Model/ViewModels/Tenant/TenantVM.cs
Normal file
28
Marco.Pms.Model/ViewModels/Tenant/TenantVM.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
242
Marco.Pms.Services/Controllers/TenantController.cs
Normal file
242
Marco.Pms.Services/Controllers/TenantController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
{
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
newUser = user;
|
_logger.LogInformation("Super Admin user already exists.");
|
||||||
|
}
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobRole = new JobRole
|
private async Task SeedRolePermissionsAsync(ApplicationDbContext dbContext, Guid superUserRoleId)
|
||||||
{
|
{
|
||||||
Id = Guid.Empty,
|
_logger.LogInformation("Seeding permissions for Super User role (ID: {RoleId})", superUserRoleId);
|
||||||
Name = "Admin",
|
|
||||||
Description = "Admin",
|
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!await dbContext.JobRoles.Where(j => j.Name == "Admin").AnyAsync())
|
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())
|
||||||
{
|
{
|
||||||
await dbContext.JobRoles.AddAsync(jobRole);
|
var newMappings = missingPermissionIds.Select(permissionId => new RolePermissionMappings
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
jobRole = await dbContext.JobRoles.Where(j => j.Name == "Admin").FirstOrDefaultAsync();
|
ApplicationRoleId = superUserRoleId,
|
||||||
}
|
FeaturePermissionId = permissionId
|
||||||
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,
|
|
||||||
IsEnabled = true,
|
|
||||||
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"),
|
|
||||||
RoleId = role?.Id ?? Guid.Empty
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await dbContext.RolePermissionMappings.AddRangeAsync(newMappings);
|
||||||
|
_logger.LogInformation("Added {Count} new permission mappings to the Super User role.", missingPermissionIds.Count);
|
||||||
}
|
}
|
||||||
if (!await dbContext.RolePermissionMappings.AnyAsync())
|
else
|
||||||
{
|
{
|
||||||
List<FeaturePermission> permissions = await dbContext.FeaturePermissions.ToListAsync();
|
_logger.LogInformation("Super User role already has all available permissions.");
|
||||||
List<RolePermissionMappings> rolesMapping = new List<RolePermissionMappings>();
|
|
||||||
foreach (var permission in permissions)
|
|
||||||
{
|
|
||||||
rolesMapping.Add(new RolePermissionMappings
|
|
||||||
{
|
|
||||||
ApplicationRoleId = role != null ? role.Id : Guid.Empty,
|
|
||||||
FeaturePermissionId = permission.Id
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
await dbContext.RolePermissionMappings.AddRangeAsync(rolesMapping);
|
|
||||||
}
|
}
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
|
/// <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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user