Optimized the the Appmenu controller

This commit is contained in:
ashutosh.nehete 2025-08-23 17:53:53 +05:30
parent 7cfebd764c
commit ff288448b0
18 changed files with 497 additions and 272 deletions

View File

@ -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<MenuSection> _collection;
private readonly ILogger<SideBarMenu> _logger;
private readonly ILogger<SidebarMenuHelper> _logger;
public SideBarMenu(IConfiguration configuration, ILogger<SideBarMenu> logger)
public SidebarMenuHelper(IConfiguration configuration, ILogger<SidebarMenuHelper> 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<MenuSection?> 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<MenuSection>.Filter.Eq(s => s.Id, sectionId);
var arrayFilters = new List<ArrayFilterDefinition>
{
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("item._id", itemId.ToString())),
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("sub._id", subItemId.ToString()))
};
{
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("item._id", itemId.ToString())),
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("sub._id", subItemId.ToString()))
};
var update = Builders<MenuSection>.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<List<MenuSection>> GetAllMenuSectionsAsync()
public async Task<List<MenuSection>> GetAllMenuSectionsAsync(Guid tenantId)
{
return await _collection.Find(_ => true).ToListAsync();
var filter = Builders<MenuSection>.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<MenuSection>.Filter.Eq(e => e.TenantId, tenantId);
result = await _collection
.Find(filter)
.ToListAsync();
return result;
}

View File

@ -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<string>
public List<string> PermissionIds { get; set; } = new List<string>();
public List<SubMenuItem> Submenu { get; set; } = new List<SubMenuItem>();
}
}

View File

@ -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<MenuItem> Items { get; set; } = new List<MenuItem>();
[BsonRepresentation(BsonType.String)]
public Guid TenantId { get; set; }
}
}

View File

@ -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<MenuItem> Items { get; set; } = new List<MenuItem>();
}
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<string>
public List<string> PermissionKeys { get; set; } = new List<string>();
public List<SubMenuItem> Submenu { get; set; } = new List<SubMenuItem>();
}
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<string>
public List<string> PermissionKeys { get; set; } = new List<string>();
}
}

View File

@ -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<string>
public List<string> PermissionIds { get; set; } = new List<string>();
}
}

View File

@ -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<string>
public List<string> PermissionIds { get; set; } = new List<string>();
public List<CreateSubMenuItemDto> Submenu { get; set; } = new List<CreateSubMenuItemDto>();
}
}

View File

@ -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<CreateMenuItemDto> Items { get; set; } = new List<CreateMenuItemDto>();
}
}

View File

@ -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<string>
public List<string> PermissionIds { get; set; } = new List<string>();
}
}

View File

@ -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<MenuItemDto> Items { get; set; } = new List<MenuItemDto>();
}
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<string>
public List<string> PermissionKeys { get; set; } = new List<string>();
public List<SubMenuItemDto> Submenu { get; set; } = new List<SubMenuItemDto>();
}
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<string>
public List<string> PermissionKeys { get; set; } = new List<string>();
}
}

View File

@ -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<string>
public List<string> PermissionIds { get; set; } = new List<string>();
}
}

View File

@ -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; }
}
}

View File

@ -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<string>
public List<string> PermissionIds { get; set; } = new List<string>();
}
}

View File

@ -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");

View File

@ -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<IActionResult> CreateAppSideBarMenu([FromBody] MenuSectionDto MenuSecetion)
/// <summary>
/// Creates a new sidebar menu section for the tenant.
/// Only accessible by root users or for the super tenant.
/// </summary>
/// <param name="menuSectionDto">The data for the new menu section.</param>
/// <returns>HTTP response with result of the operation.</returns>
[HttpPost("add/sidebar/menu-section")]
public async Task<IActionResult> 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<object>.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<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
}
var sideMenuSection = _mapper.Map<MenuSection>(MenuSecetion);
// Step 3: Map DTO to entity
var sideMenuSection = _mapper.Map<MenuSection>(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<object>.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<object>.SuccessResponse(sideMenuSection, "Sidebar menu created successfully.", 201));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Occurred while creating Menu");
return StatusCode(500, ApiResponse<object>.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<object>.ErrorResponse("Invalid MenuSection", 400));
}
_logger.LogInfo("Error Occurred while creating Menu");
return Ok(ApiResponse<object>.SuccessResponse(sideMenuSection, "Sidebar menu created successfully.", 201));
}
[HttpPut("sidebar/menu-section/{sectionId}")]
public async Task<IActionResult> UpdateMenuSection(Guid sectionId, [FromBody] MenuSection updatedSection)
{
if (sectionId == Guid.Empty || updatedSection == null)
{
_logger.LogWarning("Error Occurred while Updating Menu Item");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400));
}
var UpdatedMenuSection = _mapper.Map<MenuSection>(updatedSection);
try
{
UpdatedMenuSection = await _sideBarMenuHelper.UpdateMenuSectionAsync(sectionId, UpdatedMenuSection);
if (UpdatedMenuSection == null)
return NotFound(ApiResponse<object>.ErrorResponse("Menu section not found", 404));
return Ok(ApiResponse<object>.SuccessResponse(UpdatedMenuSection, "Menu section updated successfully"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update menu section");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server error", ex, 500));
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server Error", "An unexpected error occurred.", 500));
}
}
[HttpPost("sidebar/menus/{sectionId}/items")]
public async Task<IActionResult> AddMenuItem(Guid sectionId, [FromBody] MenuItemDto newItemDto)
/// <summary>
/// Updates an existing sidebar menu section for the tenant.
/// Only accessible by root users or for the super tenant.
/// </summary>
/// <param name="sectionId">The unique identifier of the section to update.</param>
/// <param name="updatedSection">The updated data for the sidebar menu section.</param>
/// <returns>HTTP response with the result of the operation.</returns>
[HttpPut("edit/sidebar/menu-section/{sectionId}")]
public async Task<IActionResult> UpdateMenuSection(Guid sectionId, [FromBody] UpdateMenuSectionDto updatedSection)
{
if (sectionId == Guid.Empty || newItemDto == null)
return BadRequest(ApiResponse<object>.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<object>.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<object>.ErrorResponse("Invalid section ID or mismatched payload.", 400));
}
// Step 4: Map DTO to entity
var menuSectionEntity = _mapper.Map<MenuSection>(updatedSection);
try
{
var menuItem = _mapper.Map<MenuItem>(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<object>.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<object>.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<object>.ErrorResponse("Server error", "An unexpected error occurred while updating the menu section.", 500));
}
}
/// <summary>
/// Adds a new menu item to an existing sidebar menu section.
/// Only accessible by root users or for the super tenant.
/// </summary>
/// <param name="sectionId">The unique identifier of the section the item will be added to.</param>
/// <param name="newItemDto">The details of the new menu item.</param>
/// <returns>HTTP response with the result of the operation.</returns>
[HttpPost("add/sidebar/menus/{sectionId}/items")]
public async Task<IActionResult> 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<object>.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<object>.ErrorResponse("Invalid section ID or menu item payload.", 400));
}
try
{
// Step 4: Map DTO to entity
var menuItemEntity = _mapper.Map<MenuItem>(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<object>.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<object>.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<object>.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<object>.ErrorResponse("Server error", "An unexpected error occurred while adding the menu item.", 500));
}
}
[HttpPut("sidebar/{sectionId}/items/{itemId}")]
public async Task<IActionResult> UpdateMenuItem(Guid sectionId, Guid itemId, [FromBody] MenuItemDto updatedMenuItem)
/// <summary>
/// Updates an existing menu item inside a sidebar menu section.
/// Only accessible by root users or within the super tenant.
/// </summary>
/// <param name="sectionId">The ID of the sidebar menu section.</param>
/// <param name="itemId">The ID of the menu item to update.</param>
/// <param name="updatedMenuItem">The updated menu item details.</param>
/// <returns>HTTP response with the result of the update operation.</returns>
[HttpPut("edit/sidebar/{sectionId}/items/{itemId}")]
public async Task<IActionResult> 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<object>.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<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
}
var sideMenuItem = _mapper.Map<MenuItem>(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<object>.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400));
}
// Step 4: Map DTO to entity
var menuItemEntity = _mapper.Map<MenuItem>(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<object>.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<object>.ErrorResponse("Menu item not found or update failed.", 404));
}
_logger.LogInfo("SidBar Section{SectionId} MenuItem {itemId} Updated ");
return Ok(ApiResponse<object>.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<object>.SuccessResponse(result, "Sidebar menu item updated successfully."));
}
catch (Exception ex) {
_logger.LogError(ex, "Error Occurred while creating MenuItem");
return StatusCode(500, ApiResponse<object>.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<object>.ErrorResponse("Server Error", "An unexpected error occurred while updating the menu item.", 500)
);
}
}
[HttpPost("sidebar/menus/{sectionId}/items/{itemId}/subitems")]
public async Task<IActionResult> AddSubMenuItem(Guid sectionId, Guid itemId, [FromBody] SubMenuItemDto newSubItem)
[HttpPost("add/sidebar/menus/{sectionId}/items/{itemId}/subitems")]
public async Task<IActionResult> 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<object>.ErrorResponse("access denied", "User haven't permission", 403));
}
if (sectionId == Guid.Empty || itemId == Guid.Empty)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid input", 400));
try
@ -199,10 +327,17 @@ namespace Marco.Pms.Services.Controllers
}
[HttpPut("sidebar/{sectionId}/items/{itemId}/subitems/{subItemId}")]
public async Task<IActionResult> UpdateSubmenuItem(Guid sectionId, Guid itemId, Guid subItemId, [FromBody] SubMenuItemDto updatedSubMenuItem)
[HttpPut("edit/sidebar/{sectionId}/items/{itemId}/subitems/{subItemId}")]
public async Task<IActionResult> 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<object>.ErrorResponse("access denied", "User haven't permission", 403));
}
if (sectionId == Guid.Empty || itemId == Guid.Empty || subItemId == Guid.Empty || updatedSubMenuItem.Id != subItemId)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid input", 400));
try
@ -223,16 +358,22 @@ namespace Marco.Pms.Services.Controllers
}
}
[HttpGet("sidebar/menu-section")]
/// <summary>
/// Fetches the sidebar menu for the current tenant and filters items based on employee permissions.
/// </summary>
/// <returns>The sidebar menu with only the items/sub-items the employee has access to.</returns>
[HttpGet("get/menu")]
public async Task<IActionResult> 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<SubMenuItem>();
foreach (var sm in item.Submenu)
if (item.Submenu?.Any() == true)
{
if (sm.PermissionKeys == null || !sm.PermissionKeys.Any())
var allowedSubmenus = new List<SubMenuItem>();
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<object>.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<object>.ErrorResponse("Server Error", ex, 500));
return Ok(ApiResponse<object>.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<object>.ErrorResponse("Server Error", "An unexpected error occurred while fetching the sidebar menu.", 500));
}
}
}

View File

@ -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();

View File

@ -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<MenuSectionDto, MenuSection>();
CreateMap<MenuItemDto, MenuItem>();
CreateMap<SubMenuItemDto, SubMenuItem>();
CreateMap<CreateMenuSectionDto, MenuSection>();
CreateMap<UpdateMenuSectionDto, MenuSection>();
CreateMap<CreateMenuItemDto, MenuItem>();
CreateMap<UpdateMenuItemDto, MenuItem>();
CreateMap<CreateSubMenuItemDto, SubMenuItem>();
CreateMap<UpdateSubMenuItemDto, SubMenuItem>();
#endregion
}
}

View File

@ -198,7 +198,7 @@ builder.Services.AddScoped<ProjectCache>();
builder.Services.AddScoped<EmployeeCache>();
builder.Services.AddScoped<ReportCache>();
builder.Services.AddScoped<ExpenseCache>();
builder.Services.AddScoped<SideBarMenu>();
builder.Services.AddScoped<SidebarMenuHelper>();
#endregion
// Singleton services (one instance for the app's lifetime)

View File

@ -30,6 +30,18 @@ namespace Marco.Pms.Services.Service
var hasPermission = featurePermissionIds.Contains(featurePermissionId);
return hasPermission;
}
public async Task<bool> HasPermissionAny(List<Guid> featurePermissionIds, Guid employeeId)
{
var allFeaturePermissionIds = await _cache.GetPermissions(employeeId);
if (allFeaturePermissionIds == null)
{
List<FeaturePermission> 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<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
{
var employeeId = LoggedInEmployee.Id;