diff --git a/Marco.Pms.Model/Dtos/Tenant/CreateTenantDto.cs b/Marco.Pms.Model/Dtos/Tenant/CreateTenantDto.cs new file mode 100644 index 0000000..6a4107a --- /dev/null +++ b/Marco.Pms.Model/Dtos/Tenant/CreateTenantDto.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace Marco.Pms.Model.Dtos.Tenant +{ + public class CreateTenantDto + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [MinLength(6)] + [RegularExpression(@"^(?=.*[^a-zA-Z0-9])(?=.*\d)(?=.*[A-Z]).+$", ErrorMessage = "Passwords must have at least one non-alphanumeric character, at least one digit ('0'-'9'), and at least one uppercase ('A'-'Z').")] + public string Password { get; set; } + + + public string OrganizatioinName { get; set; } + public string? About { get; set; } + public string? OragnizationSize { get; set; } + public int IndustryId { get; set; } + public string Website { get; set; } + public string Name { get; set; } + public string ContactNumber { get; set; } + public DateTime OnBoardingDate { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/Tenant/UpdateTenantDto.cs b/Marco.Pms.Model/Dtos/Tenant/UpdateTenantDto.cs new file mode 100644 index 0000000..687d4d6 --- /dev/null +++ b/Marco.Pms.Model/Dtos/Tenant/UpdateTenantDto.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.Dtos.Tenant +{ + public class UpdateTenantDto + { + public string OrganizatioinName { get; set; } + public string About { get; set; } + public string Website { get; set; } + public string Name { get; set; } + public string ContactNumber { get; set; } + + public string? OragnizationSize { get; set; } + public int IndustryId { get; set; } + public DateTime OnBoardingDate { get; set; } + } +} diff --git a/Marco.Pms.Model/Mapper/TenantMapper.cs b/Marco.Pms.Model/Mapper/TenantMapper.cs new file mode 100644 index 0000000..c4a88f0 --- /dev/null +++ b/Marco.Pms.Model/Mapper/TenantMapper.cs @@ -0,0 +1,38 @@ +using Marco.Pms.Model.Dtos.Tenant; +using Marco.Pms.Model.Entitlements; +using Marco.Pms.Model.ViewModels.Tenant; + +namespace Marco.Pms.Model.Mapper +{ + public static class TenantMapper + { + public static Tenant CreateDtoToTenant(this CreateTenantDto createTenant) + { + return new Tenant + { + Name = createTenant.OrganizatioinName, + Description = createTenant.About, + DomainName = createTenant.Website, + ContactName = createTenant.Name, + OragnizationSize = createTenant.OragnizationSize, + IndustryId = createTenant.IndustryId, + ContactNumber = createTenant.ContactNumber, + OnBoardingDate = createTenant.OnBoardingDate, + }; + } + public static TenantVM ToTenantVMFromTenant(this Tenant tenant) + { + return new TenantVM + { + OrganizationName = tenant.Name ?? string.Empty, + About = tenant.Description ?? string.Empty, + Website = tenant.DomainName ?? string.Empty, + Name = tenant.ContactName ?? string.Empty, + OragnizationSize = tenant.OragnizationSize, + IndustryId = tenant.IndustryId, + ContactNumber = tenant.ContactNumber ?? string.Empty, + OnBoardingDate = tenant.OnBoardingDate, + }; + } + } +} diff --git a/Marco.Pms.Model/Utilities/RoleConfiguration.cs b/Marco.Pms.Model/Utilities/RoleConfiguration.cs new file mode 100644 index 0000000..9b45fe4 --- /dev/null +++ b/Marco.Pms.Model/Utilities/RoleConfiguration.cs @@ -0,0 +1,10 @@ +namespace Marco.Pms.Model.Utilities +{ + public class RoleConfiguration + { + public string JobRoleName { get; set; } + public string JobRoleDescription { get; set; } + public string RoleName { get; set; } + public string RoleDescription { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Tenant/TenantVM.cs b/Marco.Pms.Model/ViewModels/Tenant/TenantVM.cs new file mode 100644 index 0000000..dd797ec --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Tenant/TenantVM.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.ViewModels.Tenant +{ + public class TenantVM + { + public string OrganizationName { get; set; } + public string About { get; set; } + public string Website { get; set; } + public string Name { get; set; } + public string ContactNumber { get; set; } + + public string? OragnizationSize { get; set; } + public int? IndustryId { get; set; } + public DateTime OnBoardingDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/TenantController.cs b/Marco.Pms.Services/Controllers/TenantController.cs new file mode 100644 index 0000000..ef98176 --- /dev/null +++ b/Marco.Pms.Services/Controllers/TenantController.cs @@ -0,0 +1,251 @@ +using System.Net; +using System.Text.Json; +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.Mapper; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Tenant; +using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TenantController : ControllerBase + { + private readonly string jsonString = System.IO.File.ReadAllText("Data/RolesCofiguration.json"); + private readonly ApplicationDbContext _context; + private readonly UserManager _userManager; + private readonly ILoggingService _logger; + private readonly IEmailSender _emailSender; + private readonly IConfiguration _configuration; + public TenantController(ApplicationDbContext context, UserManager userManager, ILoggingService logger, IEmailSender emailSender, IConfiguration configuration) + { + _context = context; + _userManager = userManager; + _logger = logger; + _emailSender = emailSender; + _configuration = configuration; + } + [HttpPost] + public async Task CreateTenant([FromForm] CreateTenantDto createTenantDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + var existedUser = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Email == createTenantDto.Email && u.IsActive == true); + var existedTenanat = await _context.Tenants.FirstOrDefaultAsync(t => t.Name == createTenantDto.OrganizatioinName); + if (existedUser == null && existedTenanat == null) + { + var transaction = _context.Database.BeginTransaction(); + Tenant newTenant = createTenantDto.CreateDtoToTenant(); + _context.Tenants.Add(newTenant); + await _context.SaveChangesAsync(); + + //Tenant? tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Name == newTenant.Name); + + if (newTenant != null) + { + RoleConfiguration settings = JsonSerializer.Deserialize(jsonString) ?? new RoleConfiguration(); + var TenantId = newTenant.Id; + JobRole jobRole = new JobRole + { + Name = settings.JobRoleName, + Description = settings.JobRoleDescription, + TenantId = TenantId + }; + + ApplicationRole role = new ApplicationRole + { + Role = settings.RoleName, + Description = settings.RoleDescription, + TenantId = TenantId + }; + + _context.JobRoles.Add(jobRole); + _context.ApplicationRoles.Add(role); + await _context.SaveChangesAsync(); + + + List permissions = await _context.FeaturePermissions.AsNoTracking().ToListAsync(); + List rolePermissionMappings = new List(); + + foreach (var permission in permissions) + { + var item = new RolePermissionMappings() { ApplicationRoleId = role.Id, FeaturePermissionId = permission.Id }; + bool assigned = _context.RolePermissionMappings.Any(c => c.ApplicationRoleId == role.Id && c.FeaturePermissionId == permission.Id); + if (permission.IsEnabled && !assigned) + rolePermissionMappings.Add(item); + + } + _context.RolePermissionMappings.AddRange(rolePermissionMappings); + await _context.SaveChangesAsync(); + + var user = new ApplicationUser + { + UserName = createTenantDto.Email, + Email = createTenantDto.Email, + TenantId = TenantId, + IsRootUser = true, + EmailConfirmed = true // comment this line of code after implimenting proper email verification + }; + + var result = await _userManager.CreateAsync(user, createTenantDto.Password); + if (result.Succeeded) + { + try + { + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; + + if (user.Email == null) return NotFound(ApiResponse.ErrorResponse("Email Not found", "Email Not found", 404)); + + await _emailSender.SendTenanatRegistrationSuccessEmail(user.Email, newTenant.ContactName ?? "", resetLink); + + Employee newEmployee = CreateTenantDtoToEmployee(createTenantDto, TenantId, user.Id, jobRole.Id); + _context.Employees.Add(newEmployee); + await _context.SaveChangesAsync(); + + var employeeRoleMapping = new EmployeeRoleMapping + { + EmployeeId = newEmployee.Id, + RoleId = role.Id, + TenantId = TenantId, + IsEnabled = true + }; + _context.EmployeeRoleMappings.Add(employeeRoleMapping); + await _context.SaveChangesAsync(); + transaction.Commit(); + return Ok(ApiResponse.SuccessResponse(result.Succeeded, "Tenant created successfully.", 200)); + }catch(Exception ex) + { + transaction.Rollback(); + _logger.LogError("{Error}", ex); + } + } + else + { + // Log the errors for debugging + foreach (var error in result.Errors) + { + // Log error.Description + _logger.LogError("{Error}", error.Description); + } + transaction.Rollback(); + return BadRequest("Failed to create the root user."); + } + } + transaction.Rollback(); + return BadRequest("Falied to create Tenant"); + + + } + return BadRequest("Email Already Exists"); + + } + + [HttpGet("profile/{tenantId}")] + public async Task GetTenantProfile(int tenantId) + { + if (tenantId <= 0) + { + return BadRequest("Tenant Id is required and must be greater than zero."); + } + var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId && t.IsActive ==true); + if (tenant == null) + { + return NotFound("Tenant Not Found"); + } + TenantVM tenantVM = tenant.ToTenantVMFromTenant(); + return Ok(ApiResponse.SuccessResponse(tenantVM, "Tenant Profile.", 200)); + } + + [HttpPost("edit/{tenantId}")] + public async Task SuspendTenant(int tenantId, UpdateTenantDto model) + { + if (tenantId <= 0) + { + return BadRequest("Tenant Id is required and must be greater than zero."); + } + var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId && t.IsActive == true); + //var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.TenantId == tenantId && u.IsRootUser == true); + //var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == tenantId && e.ApplicationUserId == user.Id); + if (tenant == null) + { + return NotFound("Tenant Not Found"); + } + tenant.Name = model.OrganizatioinName; + tenant.DomainName = model.Website; + tenant.ContactName = model.Name; + tenant.Description = model.About; + tenant.ContactNumber = model.ContactNumber; + tenant.OnBoardingDate = model.OnBoardingDate; + + await _context.SaveChangesAsync(); + TenantVM tenantVM = tenant.ToTenantVMFromTenant(); + return Ok(ApiResponse.SuccessResponse(tenantVM, "Tenant Profile.", 200)); + } + + [HttpDelete("suspend/{tenantId}")] + public async Task SuspendTenant(int tenantId) + { + if (tenantId <= 0) + { + return BadRequest("Tenant Id is required and must be greater than zero."); + } + var tenant = await _context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId); + var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.TenantId == tenantId && u.IsRootUser == true); + if (tenant != null && user != null) + { + var employee = await _context.Employees.FirstOrDefaultAsync(e => e.TenantId == tenantId && e.ApplicationUserId == user.Id) ?? new Employee(); + tenant.IsActive = false; + user.IsActive = false; + user.IsRootUser = false; + employee.IsActive = false; + + + var result = await _userManager.UpdateAsync(user); + _context.Tenants.Update(tenant); + _context.Employees.Update(employee); + await _context.SaveChangesAsync(); + return Ok("Tenant is Suspended"); + } + return NotFound("Tenant Not Found"); + } + + private static Employee CreateTenantDtoToEmployee(CreateTenantDto model, int TenantId, string? ApplicationUserId, int jobRoleId) + { + return new Employee + { + ApplicationUserId = ApplicationUserId, + FirstName = model.Name, + LastName = "", + Email = model.Email, + TenantId = TenantId, + CurrentAddress = "", + BirthDate = DateTime.UtcNow, + EmergencyPhoneNumber = "", + EmergencyContactPerson = "", + AadharNumber = "", + Gender = "", + MiddleName = "", + PanNumber = "", + PermanentAddress = "", + PhoneNumber = model.ContactNumber, + Photo = null, // GetFileDetails(model.Photo).Result.FileData, + JobRoleId = jobRoleId, + JoiningDate = DateTime.UtcNow, + + }; + } + + + } + +} diff --git a/Marco.Pms.Services/Data/RolesCofiguration.json b/Marco.Pms.Services/Data/RolesCofiguration.json new file mode 100644 index 0000000..aa9034e --- /dev/null +++ b/Marco.Pms.Services/Data/RolesCofiguration.json @@ -0,0 +1,6 @@ +{ + "JobRoleName": "Admin", + "JobRoleDescription": "Admin", + "RoleName": "SuperUser", + "RoleDescription": "SuperUser" +} \ No newline at end of file diff --git a/Marco.Pms.Services/EmailTemplates/tenant-email-verification.html b/Marco.Pms.Services/EmailTemplates/tenant-email-verification.html new file mode 100644 index 0000000..5dc4b18 --- /dev/null +++ b/Marco.Pms.Services/EmailTemplates/tenant-email-verification.html @@ -0,0 +1,568 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+ + +
+
+
+ + + + + + + + +
+ + + + + + + +
+ Sita +
+ +
+ + +
+
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+
+
+ + + + + + + + +
+ + + + + +
+ + Image + +
+ +
+ + +
+
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+
+
+ + + + + + + + +
+ + + + + +
+ + Image + +
+ +
+ + + + + + + +
+ +
+

{{MAIL_TITLE}}

+
+ +
+ + +
+
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+
+
+ + + + + + + + +
+ +
+

Hello,{{CONTACT_PERSON}}

+

 

+

🎉 Your registration is successful! We're excited to have you on board.

+

 

+

Please set your password and log in using your registered email address.

+

 

+

To set your password, please follow the link below:

+
+ +
+ + + + + + + +
+ + + + +
+ + + + + + + +
+ +
+

Please ignore this email if you did not request a password change.
 

+
+ +
+ + +
+
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+
+
+ + + + + + + + +
+ +
+ + +

Contact Us: info@marcoaiot.com

+ + +
+ +
+ + +
+
+
+ + +
+
+
+ + + + + + + + +
+ +
+
+ + + + + + + + +
+ + Facebook + +
+ + + + + + + + +
+ + Twitter + +
+ + + + + + + + +
+ + Instagram + +
+ + + + + + + + +
+ + LinkedIn + +
+ + +
+
+ +
+ + + +
+
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+
+
+ + + + + + + + +
+

Marco AIoT Technologies Pvt. Ltd. ©  All Rights Reserved

+ + + +
+ + +
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+ + + + + + + + +
+ +
+ You're receiving this email because you have a MarcoPMS account. This email is not a marketing or promotional email. That is why this email does not contain an unsubscribe link. You will receive this email even if you have unsubscribed from MarcoPMS's marketing emails + +
+ +
+ + +
+
+
+ + +
+
+
+ +
+ + + + + diff --git a/Marco.Pms.Services/Marco.Pms.Services.csproj b/Marco.Pms.Services/Marco.Pms.Services.csproj index 4e70e23..0b5889e 100644 --- a/Marco.Pms.Services/Marco.Pms.Services.csproj +++ b/Marco.Pms.Services/Marco.Pms.Services.csproj @@ -44,6 +44,7 @@ + diff --git a/Marco.Pms.Services/Service/EmailSender.cs b/Marco.Pms.Services/Service/EmailSender.cs index 1baac55..f63f4be 100644 --- a/Marco.Pms.Services/Service/EmailSender.cs +++ b/Marco.Pms.Services/Service/EmailSender.cs @@ -1,4 +1,5 @@ -using MailKit.Net.Smtp; +using System.Xml.Linq; +using MailKit.Net.Smtp; using Marco.Pms.Model.Utilities; using Microsoft.Extensions.Options; using MimeKit; @@ -107,6 +108,24 @@ namespace MarcoBMS.Services.Service } + public async Task SendTenanatRegistrationSuccessEmail(string toEmail, string contactPerson, string resetLink) + { + var replacements = new Dictionary + { + { "MAIL_TITLE", "Registration Successful" }, + { "CONTACT_PERSON", contactPerson }, + {"RESET_PWD_URL",resetLink} + }; + + string emailBody = await GetEmailTemplate("tenant-email-verification", replacements); + + List toEmails = new List + { + toEmail + }; + await SendEmailAsync(toEmails, "Registration Successful", emailBody); + } + public async Task SendEmailAsync(List toEmails, string subject, string body) { var email = new MimeMessage(); @@ -126,6 +145,6 @@ namespace MarcoBMS.Services.Service await smtp.SendAsync(email); await smtp.DisconnectAsync(true); } - } + } } diff --git a/Marco.Pms.Services/Service/IEmailSender.cs b/Marco.Pms.Services/Service/IEmailSender.cs index e2e41be..a096ded 100644 --- a/Marco.Pms.Services/Service/IEmailSender.cs +++ b/Marco.Pms.Services/Service/IEmailSender.cs @@ -8,6 +8,7 @@ namespace MarcoBMS.Services.Service Task SendResetPasswordEmailOnRegister(string toEmail, string toName, string resetLink); Task SendResetPasswordSuccessEmail(string toEmail, string userName); Task SendRequestDemoEmail(List toEmails, InquiryEmailObject demoEmailObject); + Task SendTenanatRegistrationSuccessEmail(string toEmail, string contactPerson, string resetLink); Task SendEmailAsync(List toEmails, string subject, string body); } }