From ff288448b03d6be4c150a1837a3a62f7c726f234 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 23 Aug 2025 17:53:53 +0530 Subject: [PATCH] Optimized the the Appmenu controller --- .../SidebarMenuHelper.cs} | 52 ++- Marco.Pms.Model/AppMenu/MenuItem.cs | 23 + Marco.Pms.Model/AppMenu/MenuSection.cs | 21 + Marco.Pms.Model/AppMenu/SideBarMenu.cs | 51 -- Marco.Pms.Model/AppMenu/SubMenuItem.cs | 20 + .../Dtos/AppMenu/CreateMenuItemDto.cs | 16 + .../Dtos/AppMenu/CreateMenuSectionDto.cs | 9 + .../Dtos/AppMenu/CreateSubMenuItemDto.cs | 13 + .../Dtos/AppMenu/SideBarMenuDtco.cs | 42 -- .../Dtos/AppMenu/UpdateMenuItemDto.cs | 16 + .../Dtos/AppMenu/UpdateMenuSectionDto.cs | 9 + .../Dtos/AppMenu/UpdateSubMenuItemDto.cs | 15 + .../Entitlements/PermissionsMaster.cs | 6 + .../Controllers/AppMenuController.cs | 434 ++++++++++++------ Marco.Pms.Services/Helpers/RolesHelper.cs | 13 +- .../MappingProfiles/MappingProfile.cs | 15 +- Marco.Pms.Services/Program.cs | 2 +- .../Service/PermissionServices.cs | 12 + 18 files changed, 497 insertions(+), 272 deletions(-) rename Marco.Pms.Helpers/{CacheHelper/SidebarMenu.cs => Utility/SidebarMenuHelper.cs} (83%) create mode 100644 Marco.Pms.Model/AppMenu/MenuItem.cs create mode 100644 Marco.Pms.Model/AppMenu/MenuSection.cs delete mode 100644 Marco.Pms.Model/AppMenu/SideBarMenu.cs create mode 100644 Marco.Pms.Model/AppMenu/SubMenuItem.cs create mode 100644 Marco.Pms.Model/Dtos/AppMenu/CreateMenuItemDto.cs create mode 100644 Marco.Pms.Model/Dtos/AppMenu/CreateMenuSectionDto.cs create mode 100644 Marco.Pms.Model/Dtos/AppMenu/CreateSubMenuItemDto.cs delete mode 100644 Marco.Pms.Model/Dtos/AppMenu/SideBarMenuDtco.cs create mode 100644 Marco.Pms.Model/Dtos/AppMenu/UpdateMenuItemDto.cs create mode 100644 Marco.Pms.Model/Dtos/AppMenu/UpdateMenuSectionDto.cs create mode 100644 Marco.Pms.Model/Dtos/AppMenu/UpdateSubMenuItemDto.cs diff --git a/Marco.Pms.Helpers/CacheHelper/SidebarMenu.cs b/Marco.Pms.Helpers/Utility/SidebarMenuHelper.cs similarity index 83% rename from Marco.Pms.Helpers/CacheHelper/SidebarMenu.cs rename to Marco.Pms.Helpers/Utility/SidebarMenuHelper.cs index 8ee9ed4..5aa761d 100644 --- a/Marco.Pms.Helpers/CacheHelper/SidebarMenu.cs +++ b/Marco.Pms.Helpers/Utility/SidebarMenuHelper.cs @@ -1,26 +1,20 @@ using Marco.Pms.Model.AppMenu; -using Marco.Pms.Model.Dtos.AppMenu; -using Marco.Pms.Model.ViewModels.AppMenu; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Driver; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using static System.Collections.Specialized.BitVector32; namespace Marco.Pms.CacheHelper { - public class SideBarMenu + public class SidebarMenuHelper { private readonly IMongoCollection _collection; - private readonly ILogger _logger; + private readonly ILogger _logger; - public SideBarMenu(IConfiguration configuration, ILogger logger) + public SidebarMenuHelper(IConfiguration configuration, ILogger logger) { _logger = logger; - var connectionString = configuration["MongoDB:ConnectionMenu"]; + var connectionString = configuration["MongoDB:ModificationConnectionString"]; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); var database = client.GetDatabase(mongoUrl.DatabaseName); @@ -40,7 +34,7 @@ namespace Marco.Pms.CacheHelper return null; } } - + public async Task UpdateMenuSectionAsync(Guid sectionId, MenuSection updatedSection) { try @@ -108,7 +102,7 @@ namespace Marco.Pms.CacheHelper .Set("Items.$.Icon", updatedItem.Icon) .Set("Items.$.Available", updatedItem.Available) .Set("Items.$.Link", updatedItem.Link) - .Set("Items.$.PermissionKeys", updatedItem.PermissionKeys); // <-- updated + .Set("Items.$.PermissionIds", updatedItem.PermissionIds); var result = await _collection.UpdateOneAsync(filter, update); @@ -166,18 +160,18 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(s => s.Id, sectionId); var arrayFilters = new List - { - new BsonDocumentArrayFilterDefinition( - new BsonDocument("item._id", itemId.ToString())), - new BsonDocumentArrayFilterDefinition( - new BsonDocument("sub._id", subItemId.ToString())) - }; + { + new BsonDocumentArrayFilterDefinition( + new BsonDocument("item._id", itemId.ToString())), + new BsonDocumentArrayFilterDefinition( + new BsonDocument("sub._id", subItemId.ToString())) + }; var update = Builders.Update .Set("Items.$[item].Submenu.$[sub].Text", updatedSub.Text) .Set("Items.$[item].Submenu.$[sub].Available", updatedSub.Available) .Set("Items.$[item].Submenu.$[sub].Link", updatedSub.Link) - .Set("Items.$[item].Submenu.$[sub].PermissionKeys", updatedSub.PermissionKeys); // <-- updated + .Set("Items.$[item].Submenu.$[sub].PermissionKeys", updatedSub.PermissionIds); var options = new UpdateOptions { ArrayFilters = arrayFilters }; @@ -204,9 +198,25 @@ namespace Marco.Pms.CacheHelper - public async Task> GetAllMenuSectionsAsync() + public async Task> GetAllMenuSectionsAsync(Guid tenantId) { - return await _collection.Find(_ => true).ToListAsync(); + var filter = Builders.Filter.Eq(e => e.TenantId, tenantId); + + var result = await _collection + .Find(filter) + .ToListAsync(); + if (result.Any()) + { + return result; + } + + tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"); + filter = Builders.Filter.Eq(e => e.TenantId, tenantId); + + result = await _collection + .Find(filter) + .ToListAsync(); + return result; } diff --git a/Marco.Pms.Model/AppMenu/MenuItem.cs b/Marco.Pms.Model/AppMenu/MenuItem.cs new file mode 100644 index 0000000..f70c73d --- /dev/null +++ b/Marco.Pms.Model/AppMenu/MenuItem.cs @@ -0,0 +1,23 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.AppMenu +{ + public class MenuItem + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } = Guid.NewGuid(); + + public string? Text { get; set; } + public string? Icon { get; set; } + public bool Available { get; set; } = true; + + public string? Link { get; set; } + + // Changed from string → List + public List PermissionIds { get; set; } = new List(); + + public List Submenu { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/AppMenu/MenuSection.cs b/Marco.Pms.Model/AppMenu/MenuSection.cs new file mode 100644 index 0000000..df180cc --- /dev/null +++ b/Marco.Pms.Model/AppMenu/MenuSection.cs @@ -0,0 +1,21 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.AppMenu +{ + public class MenuSection + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } = Guid.NewGuid(); + + public string? Header { get; set; } + public string? Title { get; set; } + public List Items { get; set; } = new List(); + + [BsonRepresentation(BsonType.String)] + public Guid TenantId { get; set; } + } +} + + diff --git a/Marco.Pms.Model/AppMenu/SideBarMenu.cs b/Marco.Pms.Model/AppMenu/SideBarMenu.cs deleted file mode 100644 index cd826f0..0000000 --- a/Marco.Pms.Model/AppMenu/SideBarMenu.cs +++ /dev/null @@ -1,51 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace Marco.Pms.Model.AppMenu -{ - public class MenuSection - { - [BsonId] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } = Guid.NewGuid(); - - public string? Header { get; set; } - public string? Title { get; set; } - public List Items { get; set; } = new List(); - } - - public class MenuItem - { - [BsonId] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } = Guid.NewGuid(); - - public string? Text { get; set; } - public string? Icon { get; set; } - public bool Available { get; set; } = true; - - public string? Link { get; set; } - - // Changed from string → List - public List PermissionKeys { get; set; } = new List(); - - public List Submenu { get; set; } = new List(); - } - - public class SubMenuItem - { - [BsonId] - [BsonRepresentation(BsonType.String)] - public Guid Id { get; set; } = Guid.NewGuid(); - - public string? Text { get; set; } - public bool Available { get; set; } = true; - - public string Link { get; set; } = string.Empty; - - // Changed from string → List - public List PermissionKeys { get; set; } = new List(); - } -} - - diff --git a/Marco.Pms.Model/AppMenu/SubMenuItem.cs b/Marco.Pms.Model/AppMenu/SubMenuItem.cs new file mode 100644 index 0000000..26e4eca --- /dev/null +++ b/Marco.Pms.Model/AppMenu/SubMenuItem.cs @@ -0,0 +1,20 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.AppMenu +{ + public class SubMenuItem + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } = Guid.NewGuid(); + + public string? Text { get; set; } + public bool Available { get; set; } = true; + + public string Link { get; set; } = string.Empty; + + // Changed from string → List + public List PermissionIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/Dtos/AppMenu/CreateMenuItemDto.cs b/Marco.Pms.Model/Dtos/AppMenu/CreateMenuItemDto.cs new file mode 100644 index 0000000..0e1ddd3 --- /dev/null +++ b/Marco.Pms.Model/Dtos/AppMenu/CreateMenuItemDto.cs @@ -0,0 +1,16 @@ +namespace Marco.Pms.Model.Dtos.AppMenu +{ + public class CreateMenuItemDto + { + public required string Text { get; set; } + public required string Icon { get; set; } + public bool Available { get; set; } = true; + + public required string Link { get; set; } + + // Changed from string → List + public List PermissionIds { get; set; } = new List(); + + public List Submenu { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/Dtos/AppMenu/CreateMenuSectionDto.cs b/Marco.Pms.Model/Dtos/AppMenu/CreateMenuSectionDto.cs new file mode 100644 index 0000000..e64c137 --- /dev/null +++ b/Marco.Pms.Model/Dtos/AppMenu/CreateMenuSectionDto.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Dtos.AppMenu +{ + public class CreateMenuSectionDto + { + public required string Header { get; set; } + public required string Title { get; set; } + public List Items { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Marco.Pms.Model/Dtos/AppMenu/CreateSubMenuItemDto.cs b/Marco.Pms.Model/Dtos/AppMenu/CreateSubMenuItemDto.cs new file mode 100644 index 0000000..1ebed17 --- /dev/null +++ b/Marco.Pms.Model/Dtos/AppMenu/CreateSubMenuItemDto.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.Dtos.AppMenu +{ + public class CreateSubMenuItemDto + { + public required string Text { get; set; } + public bool Available { get; set; } = true; + + public required string Link { get; set; } = string.Empty; + + // Changed from string → List + public List PermissionIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/Dtos/AppMenu/SideBarMenuDtco.cs b/Marco.Pms.Model/Dtos/AppMenu/SideBarMenuDtco.cs deleted file mode 100644 index e31759a..0000000 --- a/Marco.Pms.Model/Dtos/AppMenu/SideBarMenuDtco.cs +++ /dev/null @@ -1,42 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Marco.Pms.Model.Dtos.AppMenu -{ - public class MenuSectionDto - { - public string? Header { get; set; } - public string? Title { get; set; } - public List Items { get; set; } = new List(); - } - - public class MenuItemDto - { - public string? Text { get; set; } - public string? Icon { get; set; } - public bool Available { get; set; } = true; - - public string? Link { get; set; } - - // Changed from string → List - public List PermissionKeys { get; set; } = new List(); - - public List Submenu { get; set; } = new List(); - } - - public class SubMenuItemDto - { - public string? Text { get; set; } - public bool Available { get; set; } = true; - - public string Link { get; set; } = string.Empty; - - // Changed from string → List - public List PermissionKeys { get; set; } = new List(); - } -} \ No newline at end of file diff --git a/Marco.Pms.Model/Dtos/AppMenu/UpdateMenuItemDto.cs b/Marco.Pms.Model/Dtos/AppMenu/UpdateMenuItemDto.cs new file mode 100644 index 0000000..dd56d4d --- /dev/null +++ b/Marco.Pms.Model/Dtos/AppMenu/UpdateMenuItemDto.cs @@ -0,0 +1,16 @@ +namespace Marco.Pms.Model.Dtos.AppMenu +{ + public class UpdateMenuItemDto + { + public required Guid Id { get; set; } + + public required string Text { get; set; } + public required string Icon { get; set; } + public bool Available { get; set; } = true; + + public required string Link { get; set; } + + // Changed from string → List + public List PermissionIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/Dtos/AppMenu/UpdateMenuSectionDto.cs b/Marco.Pms.Model/Dtos/AppMenu/UpdateMenuSectionDto.cs new file mode 100644 index 0000000..f42794e --- /dev/null +++ b/Marco.Pms.Model/Dtos/AppMenu/UpdateMenuSectionDto.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Dtos.AppMenu +{ + public class UpdateMenuSectionDto + { + public required Guid Id { get; set; } + public required string Header { get; set; } + public required string Title { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/AppMenu/UpdateSubMenuItemDto.cs b/Marco.Pms.Model/Dtos/AppMenu/UpdateSubMenuItemDto.cs new file mode 100644 index 0000000..7aed3fb --- /dev/null +++ b/Marco.Pms.Model/Dtos/AppMenu/UpdateSubMenuItemDto.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.Dtos.AppMenu +{ + public class UpdateSubMenuItemDto + { + public Guid Id { get; set; } + + public string? Text { get; set; } + public bool Available { get; set; } = true; + + public string Link { get; set; } = string.Empty; + + // Changed from string → List + public List PermissionIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/Entitlements/PermissionsMaster.cs b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs index 7951f0f..262d34b 100644 --- a/Marco.Pms.Model/Entitlements/PermissionsMaster.cs +++ b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs @@ -5,9 +5,11 @@ 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 DirectoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5"); public static readonly Guid DirectoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb"); + public static readonly Guid ViewProject = Guid.Parse("6ea44136-987e-44ba-9e5d-1cf8f5837ebc"); public static readonly Guid ManageProject = Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614"); public static readonly Guid ManageTeam = Guid.Parse("b94802ce-0689-4643-9e1d-11c86950c35b"); @@ -17,15 +19,19 @@ public static readonly Guid AddAndEditTask = Guid.Parse("08752f33-3b29-4816-b76b-ea8a968ed3c5"); public static readonly Guid AssignAndReportProgress = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); public static readonly Guid ApproveTask = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); + public static readonly Guid ViewAllEmployees = Guid.Parse("60611762-7f8a-4fb5-b53f-b1139918796b"); public static readonly Guid ViewTeamMembers = Guid.Parse("b82d2b7e-0d52-45f3-997b-c008ea460e7f"); public static readonly Guid AddAndEditEmployee = Guid.Parse("a97d366a-c2bb-448d-be93-402bd2324566"); public static readonly Guid AssignRoles = Guid.Parse("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"); + public static readonly Guid TeamAttendance = Guid.Parse("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"); public static readonly Guid RegularizeAttendance = Guid.Parse("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"); public static readonly Guid SelfAttendance = Guid.Parse("ccb0589f-712b-43de-92ed-5b6088e7dc4e"); + public static readonly Guid ViewMasters = Guid.Parse("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"); public static readonly Guid ManageMasters = Guid.Parse("588a8824-f924-4955-82d8-fc51956cf323"); + public static readonly Guid ExpenseViewSelf = Guid.Parse("385be49f-8fde-440e-bdbc-3dffeb8dd116"); public static readonly Guid ExpenseViewAll = Guid.Parse("01e06444-9ca7-4df4-b900-8c3fa051b92f"); public static readonly Guid ExpenseUpload = Guid.Parse("0f57885d-bcb2-4711-ac95-d841ace6d5a7"); diff --git a/Marco.Pms.Services/Controllers/AppMenuController.cs b/Marco.Pms.Services/Controllers/AppMenuController.cs index 12dc7ec..7d856a3 100644 --- a/Marco.Pms.Services/Controllers/AppMenuController.cs +++ b/Marco.Pms.Services/Controllers/AppMenuController.cs @@ -1,24 +1,13 @@ using AutoMapper; -using Azure; using Marco.Pms.CacheHelper; using Marco.Pms.Model.AppMenu; using Marco.Pms.Model.Dtos.AppMenu; -using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.AppMenu; using Marco.Pms.Services.Service; -using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; -using MongoDB.Driver; -using Org.BouncyCastle.Asn1.Ocsp; -using System.Linq; -using System.Threading.Tasks; -using static System.Collections.Specialized.BitVector32; namespace Marco.Pms.Services.Controllers { @@ -29,151 +18,290 @@ namespace Marco.Pms.Services.Controllers { private readonly UserHelper _userHelper; - private readonly EmployeeHelper _employeeHelper; - private readonly RolesHelper _rolesHelper; - private readonly SideBarMenu _sideBarMenuHelper; + private readonly SidebarMenuHelper _sideBarMenuHelper; private readonly IMapper _mapper; private readonly ILoggingService _logger; private readonly PermissionServices _permissions; + private readonly Guid tenantId; + private static readonly Guid superTenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"); - public AppMenuController(EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, RolesHelper rolesHelper, SideBarMenu sideBarMenuHelper, IMapper mapper, ILoggingService logger, PermissionServices permissions) + public AppMenuController(UserHelper userHelper, + SidebarMenuHelper sideBarMenuHelper, + IMapper mapper, + ILoggingService logger, + PermissionServices permissions) { _userHelper = userHelper; - _employeeHelper = employeeHelper; - _rolesHelper = rolesHelper; _sideBarMenuHelper = sideBarMenuHelper; _mapper = mapper; _logger = logger; _permissions = permissions; + tenantId = userHelper.GetTenantId(); } - [HttpPost("sidebar/menu-section")] - public async Task CreateAppSideBarMenu([FromBody] MenuSectionDto MenuSecetion) + /// + /// Creates a new sidebar menu section for the tenant. + /// Only accessible by root users or for the super tenant. + /// + /// The data for the new menu section. + /// HTTP response with result of the operation. + + [HttpPost("add/sidebar/menu-section")] + public async Task CreateAppSideBarMenu([FromBody] CreateMenuSectionDto menuSectionDto) { + // Step 1: Fetch logged-in user + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; - - var user = await _userHelper.GetCurrentEmployeeAsync(); - - if (!(user.ApplicationUser?.IsRootUser ?? false)) + // Step 2: Authorization check + if (!isRootUser || tenantId != superTenantId) { - _logger.LogWarning("Access Denied while creating side menu"); - return StatusCode(403, ApiResponse.ErrorResponse("access denied", "User haven't permission", 403)); + _logger.LogWarning("Access denied: Employee {EmployeeId} attempted to create sidebar menu in Tenant {TenantId}", loggedInEmployee.Id, tenantId); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "User does not have permission.", 403)); } - var sideMenuSection = _mapper.Map(MenuSecetion); + // Step 3: Map DTO to entity + var sideMenuSection = _mapper.Map(menuSectionDto); + sideMenuSection.TenantId = tenantId; + try { + // Step 4: Save entity using helper sideMenuSection = await _sideBarMenuHelper.CreateMenuSectionAsync(sideMenuSection); + + if (sideMenuSection == null) + { + _logger.LogWarning("Failed to create sidebar menu section. Tenant: {TenantId}, Request: {@MenuSectionDto}", tenantId, menuSectionDto); + return BadRequest(ApiResponse.ErrorResponse("Invalid MenuSection", 400)); + } + + // Step 5: Log success + _logger.LogInfo("Sidebar menu created successfully. SectionId: {SectionId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", + sideMenuSection.Id, tenantId, loggedInEmployee.Id); + + return Ok(ApiResponse.SuccessResponse(sideMenuSection, "Sidebar menu created successfully.", 201)); } catch (Exception ex) { - _logger.LogError(ex, "Error Occurred while creating Menu"); - return StatusCode(500, ApiResponse.ErrorResponse("Server Error", ex, 500)); - } + // Step 6: Handle and log unexpected server errors + _logger.LogError(ex, "Unexpected error occurred while creating sidebar menu. Tenant: {TenantId}, EmployeeId: {EmployeeId}, Request: {@MenuSectionDto}", + tenantId, loggedInEmployee.Id, menuSectionDto); - if (sideMenuSection == null) { - _logger.LogWarning("Error Occurred while creating Menu"); - return BadRequest(ApiResponse.ErrorResponse("Invalid MenuSection", 400)); - } - - _logger.LogInfo("Error Occurred while creating Menu"); - return Ok(ApiResponse.SuccessResponse(sideMenuSection, "Sidebar menu created successfully.", 201)); - - } - - [HttpPut("sidebar/menu-section/{sectionId}")] - public async Task UpdateMenuSection(Guid sectionId, [FromBody] MenuSection updatedSection) - { - if (sectionId == Guid.Empty || updatedSection == null) - { - _logger.LogWarning("Error Occurred while Updating Menu Item"); - return BadRequest(ApiResponse.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400)); - } - var UpdatedMenuSection = _mapper.Map(updatedSection); - try - { - UpdatedMenuSection = await _sideBarMenuHelper.UpdateMenuSectionAsync(sectionId, UpdatedMenuSection); - - if (UpdatedMenuSection == null) - return NotFound(ApiResponse.ErrorResponse("Menu section not found", 404)); - - return Ok(ApiResponse.SuccessResponse(UpdatedMenuSection, "Menu section updated successfully")); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update menu section"); - return StatusCode(500, ApiResponse.ErrorResponse("Server error", ex, 500)); + return StatusCode(500, ApiResponse.ErrorResponse("Server Error", "An unexpected error occurred.", 500)); } } - [HttpPost("sidebar/menus/{sectionId}/items")] - public async Task AddMenuItem(Guid sectionId, [FromBody] MenuItemDto newItemDto) + /// + /// Updates an existing sidebar menu section for the tenant. + /// Only accessible by root users or for the super tenant. + /// + /// The unique identifier of the section to update. + /// The updated data for the sidebar menu section. + /// HTTP response with the result of the operation. + + [HttpPut("edit/sidebar/menu-section/{sectionId}")] + public async Task UpdateMenuSection(Guid sectionId, [FromBody] UpdateMenuSectionDto updatedSection) { - if (sectionId == Guid.Empty || newItemDto == null) - return BadRequest(ApiResponse.ErrorResponse("Invalid input", 400)); + // Step 1: Fetch logged-in user + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; + + // Step 2: Authorization check + if (!isRootUser && tenantId != superTenantId) + { + _logger.LogWarning("Access denied: User {UserId} attempted to update sidebar menu section {SectionId} in Tenant {TenantId}", + loggedInEmployee.Id, sectionId, tenantId); + + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "User does not have permission.", 403)); + } + + // Step 3: Validate request + if (sectionId == Guid.Empty || sectionId != updatedSection.Id) + { + _logger.LogWarning("Invalid update request. Tenant: {TenantId}, SectionId: {SectionId}, PayloadId: {PayloadId}, UserId: {UserId}", + tenantId, sectionId, updatedSection.Id, loggedInEmployee.Id); + + return BadRequest(ApiResponse.ErrorResponse("Invalid section ID or mismatched payload.", 400)); + } + + // Step 4: Map DTO to entity + var menuSectionEntity = _mapper.Map(updatedSection); try { - var menuItem = _mapper.Map(newItemDto); - - var result = await _sideBarMenuHelper.AddMenuItemAsync(sectionId, menuItem); + // Step 5: Perform update operation + var result = await _sideBarMenuHelper.UpdateMenuSectionAsync(sectionId, menuSectionEntity); if (result == null) + { + _logger.LogWarning("Menu section not found for update. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}", + sectionId, tenantId, loggedInEmployee.Id); return NotFound(ApiResponse.ErrorResponse("Menu section not found", 404)); + } - _logger.LogInfo("Added MenuItem in Section: {SectionId}"); + // Step 6: Successful update + _logger.LogInfo("Menu section updated successfully. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}", + sectionId, tenantId, loggedInEmployee.Id); + + return Ok(ApiResponse.SuccessResponse(result, "Menu section updated successfully")); + } + catch (Exception ex) + { + // Step 7: Unexpected server error + _logger.LogError(ex, "Failed to update menu section. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}, Payload: {@UpdatedSection}", + sectionId, tenantId, loggedInEmployee.Id, updatedSection); + + return StatusCode(500, ApiResponse.ErrorResponse("Server error", "An unexpected error occurred while updating the menu section.", 500)); + } + } + + /// + /// Adds a new menu item to an existing sidebar menu section. + /// Only accessible by root users or for the super tenant. + /// + /// The unique identifier of the section the item will be added to. + /// The details of the new menu item. + /// HTTP response with the result of the operation. + + [HttpPost("add/sidebar/menus/{sectionId}/items")] + public async Task AddMenuItem(Guid sectionId, [FromBody] CreateMenuItemDto newItemDto) + { + // Step 1: Fetch logged-in user + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; + + // Step 2: Authorization check + if (!isRootUser && tenantId != superTenantId) + { + _logger.LogWarning("Access denied: User {UserId} attempted to add menu item to section {SectionId} in Tenant {TenantId}", + loggedInEmployee.Id, sectionId, tenantId); + + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "User does not have permission.", 403)); + } + + // Step 3: Input validation + if (sectionId == Guid.Empty || newItemDto == null) + { + _logger.LogWarning("Invalid AddMenuItem request. Tenant: {TenantId}, SectionId: {SectionId}, UserId: {UserId}", + tenantId, sectionId, loggedInEmployee.Id); + + return BadRequest(ApiResponse.ErrorResponse("Invalid section ID or menu item payload.", 400)); + } + + try + { + // Step 4: Map DTO to entity + var menuItemEntity = _mapper.Map(newItemDto); + + // Step 5: Perform Add operation + var result = await _sideBarMenuHelper.AddMenuItemAsync(sectionId, menuItemEntity); + + if (result == null) + { + _logger.LogWarning("Menu section not found. Unable to add menu item. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}", + sectionId, tenantId, loggedInEmployee.Id); + + return NotFound(ApiResponse.ErrorResponse("Menu section not found", 404)); + } + + // Step 6: Successful addition + _logger.LogInfo("Menu item added successfully. SectionId: {SectionId}, MenuItemId: {MenuItemId}, TenantId: {TenantId}, UserId: {UserId}", + sectionId, result.Id, tenantId, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(result, "Menu item added successfully")); } catch (Exception ex) { - _logger.LogError(ex, "Error occurred while adding MenuItem inside MenuSection: {SectionId}", sectionId); - return StatusCode(500, ApiResponse.ErrorResponse("Server error", ex, 500)); + // Step 7: Handle unexpected errors + _logger.LogError(ex, "Error occurred while adding menu item. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}, Payload: {@NewItemDto}", + sectionId, tenantId, loggedInEmployee.Id, newItemDto); + + return StatusCode(500, ApiResponse.ErrorResponse("Server error", "An unexpected error occurred while adding the menu item.", 500)); } } - [HttpPut("sidebar/{sectionId}/items/{itemId}")] - public async Task UpdateMenuItem(Guid sectionId, Guid itemId, [FromBody] MenuItemDto updatedMenuItem) + /// + /// Updates an existing menu item inside a sidebar menu section. + /// Only accessible by root users or within the super tenant. + /// + /// The ID of the sidebar menu section. + /// The ID of the menu item to update. + /// The updated menu item details. + /// HTTP response with the result of the update operation. + + [HttpPut("edit/sidebar/{sectionId}/items/{itemId}")] + public async Task UpdateMenuItem(Guid sectionId, Guid itemId, [FromBody] UpdateMenuItemDto updatedMenuItem) { + // Step 1: Fetch logged-in user + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; - if (sectionId == Guid.Empty || itemId == Guid.Empty || updatedMenuItem == null) + // Step 2: Authorization check + if (!isRootUser && tenantId != superTenantId) { - _logger.LogWarning("Error Occurred while Updating Menu Item"); - return BadRequest(ApiResponse.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400)); + _logger.LogWarning("Access denied: User {UserId} attempted to update menu item {ItemId} in Section {SectionId}, Tenant {TenantId}", + loggedInEmployee.Id, itemId, sectionId, tenantId); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "User does not have permission.", 403)); } - var sideMenuItem = _mapper.Map(updatedMenuItem); + // Step 3: Input validation + if (sectionId == Guid.Empty || itemId == Guid.Empty || updatedMenuItem == null || updatedMenuItem.Id != itemId) + { + _logger.LogWarning("Invalid UpdateMenuItem request. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}", + tenantId, sectionId, itemId, loggedInEmployee.Id); + + return BadRequest(ApiResponse.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400)); + } + + // Step 4: Map DTO to entity + var menuItemEntity = _mapper.Map(updatedMenuItem); try { + // Step 5: Perform update operation + var result = await _sideBarMenuHelper.UpdateMenuItemAsync(sectionId, itemId, menuItemEntity); - sideMenuItem = await _sideBarMenuHelper.UpdateMenuItemAsync(sectionId, itemId, sideMenuItem); - - if (sideMenuItem == null) + if (result == null) { - _logger.LogWarning("Error Occurred while Updating SidBar Section:{SectionId} MenuItem:{itemId} "); - return BadRequest(ApiResponse.ErrorResponse("Menu creation failed", 400)); + _logger.LogWarning("Menu item not found or update failed. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}", + tenantId, sectionId, itemId, loggedInEmployee.Id); + return NotFound(ApiResponse.ErrorResponse("Menu item not found or update failed.", 404)); } - _logger.LogInfo("SidBar Section{SectionId} MenuItem {itemId} Updated "); - return Ok(ApiResponse.SuccessResponse(sideMenuItem, "Sidebar MenuItem Updated successfully.", 201)); + // Step 6: Success log + _logger.LogInfo("Menu item updated successfully. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}", + tenantId, sectionId, itemId, loggedInEmployee.Id); + return Ok(ApiResponse.SuccessResponse(result, "Sidebar menu item updated successfully.")); } - catch (Exception ex) { - _logger.LogError(ex, "Error Occurred while creating MenuItem"); - return StatusCode(500, ApiResponse.ErrorResponse("Server Error", ex, 500)); + catch (Exception ex) + { + // ✅ Step 7: Handle server errors + _logger.LogError(ex, "Error occurred while updating menu item. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}, Payload: {@UpdatedMenuItem}", + tenantId, sectionId, itemId, loggedInEmployee.Id, updatedMenuItem); + + return StatusCode( + 500, + ApiResponse.ErrorResponse("Server Error", "An unexpected error occurred while updating the menu item.", 500) + ); } - - } - [HttpPost("sidebar/menus/{sectionId}/items/{itemId}/subitems")] - public async Task AddSubMenuItem(Guid sectionId, Guid itemId, [FromBody] SubMenuItemDto newSubItem) + + [HttpPost("add/sidebar/menus/{sectionId}/items/{itemId}/subitems")] + public async Task AddSubMenuItem(Guid sectionId, Guid itemId, [FromBody] CreateSubMenuItemDto newSubItem) { - if (sectionId == Guid.Empty || itemId == Guid.Empty || newSubItem == null) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; + if (!isRootUser && tenantId != superTenantId) + { + _logger.LogWarning("Access Denied while adding sub menu item"); + return StatusCode(403, ApiResponse.ErrorResponse("access denied", "User haven't permission", 403)); + } + if (sectionId == Guid.Empty || itemId == Guid.Empty) return BadRequest(ApiResponse.ErrorResponse("Invalid input", 400)); try @@ -199,10 +327,17 @@ namespace Marco.Pms.Services.Controllers } - [HttpPut("sidebar/{sectionId}/items/{itemId}/subitems/{subItemId}")] - public async Task UpdateSubmenuItem(Guid sectionId, Guid itemId, Guid subItemId, [FromBody] SubMenuItemDto updatedSubMenuItem) + [HttpPut("edit/sidebar/{sectionId}/items/{itemId}/subitems/{subItemId}")] + public async Task UpdateSubmenuItem(Guid sectionId, Guid itemId, Guid subItemId, [FromBody] UpdateSubMenuItemDto updatedSubMenuItem) { - if (sectionId == Guid.Empty || itemId == Guid.Empty || subItemId == Guid.Empty || updatedSubMenuItem == null) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; + if (!isRootUser && tenantId != superTenantId) + { + _logger.LogWarning("Access Denied while updating sub menu item"); + return StatusCode(403, ApiResponse.ErrorResponse("access denied", "User haven't permission", 403)); + } + if (sectionId == Guid.Empty || itemId == Guid.Empty || subItemId == Guid.Empty || updatedSubMenuItem.Id != subItemId) return BadRequest(ApiResponse.ErrorResponse("Invalid input", 400)); try @@ -223,16 +358,22 @@ namespace Marco.Pms.Services.Controllers } } - [HttpGet("sidebar/menu-section")] + /// + /// Fetches the sidebar menu for the current tenant and filters items based on employee permissions. + /// + /// The sidebar menu with only the items/sub-items the employee has access to. + + [HttpGet("get/menu")] public async Task GetAppSideBarMenu() { - var loggedUser = await _userHelper.GetCurrentEmployeeAsync(); - var employeeId = loggedUser.Id; + // Step 1: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var employeeId = loggedInEmployee.Id; try { - - var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(); + // Step 2: Fetch all menu sections for the tenant + var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(tenantId); foreach (var menu in menus) { @@ -240,77 +381,78 @@ namespace Marco.Pms.Services.Controllers foreach (var item in menu.Items) { - bool isAllowed = false; - - if (item.PermissionKeys == null || !item.PermissionKeys.Any()) + // --- Item permission check --- + if (!item.PermissionIds.Any()) { - isAllowed = true; + allowedItems.Add(item); } else { - foreach (var pk in item.PermissionKeys) - { - if (Guid.TryParse(pk, out var permissionId)) - { - if (await _permissions.HasPermission(permissionId, employeeId)) - { - isAllowed = true; - break; - } - } - } - } + // Convert permission string IDs to GUIDs + var menuPermissionIds = item.PermissionIds + .Select(Guid.Parse) + .ToList(); - if (isAllowed) - { + bool isAllowed = await _permissions.HasPermissionAny(menuPermissionIds, employeeId); - if (item.Submenu != null && item.Submenu.Any()) + // If allowed, filter its submenus as well + if (isAllowed) { - var allowedSubmenus = new List(); - foreach (var sm in item.Submenu) + if (item.Submenu?.Any() == true) { - if (sm.PermissionKeys == null || !sm.PermissionKeys.Any()) + var allowedSubmenus = new List(); + + foreach (var subItem in item.Submenu) { - allowedSubmenus.Add(sm); - } - else - { - foreach (var pk in sm.PermissionKeys) + if (!subItem.PermissionIds.Any()) { - if (Guid.TryParse(pk, out var permissionId)) - { - if (await _permissions.HasPermission(permissionId, employeeId)) - { - allowedSubmenus.Add(sm); - break; - } - } + allowedSubmenus.Add(subItem); + continue; + } + + var subMenuPermissionIds = subItem.PermissionIds + .Select(Guid.Parse) + .ToList(); + + bool isSubItemAllowed = await _permissions.HasPermissionAny(subMenuPermissionIds, employeeId); + + if (isSubItemAllowed) + { + allowedSubmenus.Add(subItem); } } - } - item.Submenu = allowedSubmenus; - } - allowedItems.Add(item); + // Replace with filtered submenus + item.Submenu = allowedSubmenus; + } + + allowedItems.Add(item); + } } } + // Replace with filtered items menu.Items = allowedItems; } - _logger.LogInfo("Fetched Sidebar Menu"); - return Ok(ApiResponse.SuccessResponse(menus, "SideBar Menu Fetched successfully")); - } - catch (Exception ex) { + // Step 3: Log success + _logger.LogInfo("Fetched sidebar menu successfully. Tenant: {TenantId}, EmployeeId: {EmployeeId}, SectionsReturned: {Count}", + tenantId, employeeId, menus.Count); - _logger.LogError(ex, "Error Occurred while Updating Fetching Menu"); - return StatusCode(500, ApiResponse.ErrorResponse("Server Error", ex, 500)); + return Ok(ApiResponse.SuccessResponse(menus, "Sidebar menu fetched successfully")); } + catch (Exception ex) + { + // Step 4: Handle unexpected errors + _logger.LogError(ex, "Error occurred while fetching sidebar menu. Tenant: {TenantId}, EmployeeId: {EmployeeId}", + tenantId, employeeId); - + return StatusCode(500, ApiResponse.ErrorResponse("Server Error", "An unexpected error occurred while fetching the sidebar menu.", 500)); + } } + } diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index ef9f824..4fb1c49 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -70,12 +70,13 @@ namespace MarcoBMS.Services.Helpers // --- Step 3: Execute the main query on the main thread using its original context --- // This is now safe because the background task is using a different DbContext instance. - var permissions = await ( - from rpm in _context.RolePermissionMappings - join fp in _context.FeaturePermissions.Include(f => f.Feature) - on rpm.FeaturePermissionId equals fp.Id - where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true - select fp) + var roleIds = await employeeRoleIdsQuery.ToListAsync(); + + var permissionIds = await _context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).ToListAsync(); + + var permissions = await _context.FeaturePermissions.Include(f => f.Feature) + .Where(fp => permissionIds.Contains(fp.Id)) .Distinct() .ToListAsync(); diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 454ae5e..76abb2f 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,8 +1,8 @@ using AutoMapper; -using Marco.Pms.Model.Dtos.Expenses; -using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.AppMenu; using Marco.Pms.Model.Dtos.AppMenu; +using Marco.Pms.Model.Dtos.Expenses; +using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Employees; @@ -309,9 +309,14 @@ namespace Marco.Pms.Services.MappingProfiles #endregion #region ======================================================= AppMenu ======================================================= - CreateMap(); - CreateMap(); - CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); #endregion } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 5e22d86..0b06de0 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -198,7 +198,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); #endregion // Singleton services (one instance for the app's lifetime) diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index 9758a5f..e3374f5 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -30,6 +30,18 @@ namespace Marco.Pms.Services.Service var hasPermission = featurePermissionIds.Contains(featurePermissionId); return hasPermission; } + public async Task HasPermissionAny(List featurePermissionIds, Guid employeeId) + { + var allFeaturePermissionIds = await _cache.GetPermissions(employeeId); + if (allFeaturePermissionIds == null) + { + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId); + allFeaturePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); + } + var hasPermission = featurePermissionIds.Any(f => allFeaturePermissionIds.Contains(f)); + + return hasPermission; + } public async Task HasProjectPermission(Employee LoggedInEmployee, Guid projectId) { var employeeId = LoggedInEmployee.Id;