Modified side menu APIs
This commit is contained in:
parent
852ddc7e02
commit
4c284f9904
@ -1,15 +1,14 @@
|
|||||||
using Marco.Pms.Model.AppMenu;
|
using Marco.Pms.Model.AppMenu;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
|
||||||
namespace Marco.Pms.CacheHelper
|
namespace Marco.Pms.CacheHelper
|
||||||
{
|
{
|
||||||
public class SidebarMenuHelper
|
public class SidebarMenuHelper
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<MenuSection> _collection;
|
private readonly IMongoCollection<MenuSection> _oldCollection;
|
||||||
private readonly IMongoCollection<WebMenuSection> _webCollection;
|
private readonly IMongoCollection<WebSideMenuItem> _collection;
|
||||||
private readonly ILogger<SidebarMenuHelper> _logger;
|
private readonly ILogger<SidebarMenuHelper> _logger;
|
||||||
|
|
||||||
public SidebarMenuHelper(IConfiguration configuration, ILogger<SidebarMenuHelper> logger)
|
public SidebarMenuHelper(IConfiguration configuration, ILogger<SidebarMenuHelper> logger)
|
||||||
@ -19,17 +18,17 @@ namespace Marco.Pms.CacheHelper
|
|||||||
var mongoUrl = new MongoUrl(connectionString);
|
var mongoUrl = new MongoUrl(connectionString);
|
||||||
var client = new MongoClient(mongoUrl);
|
var client = new MongoClient(mongoUrl);
|
||||||
var database = client.GetDatabase(mongoUrl.DatabaseName);
|
var database = client.GetDatabase(mongoUrl.DatabaseName);
|
||||||
_collection = database.GetCollection<MenuSection>("Menus");
|
_oldCollection = database.GetCollection<MenuSection>("Menus");
|
||||||
_webCollection = database.GetCollection<WebMenuSection>("WebSideMenus");
|
_collection = database.GetCollection<WebSideMenuItem>("WebSideMenus");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<WebMenuSection>> GetAllWebMenuSectionsAsync(Guid tenantId)
|
public async Task<List<WebSideMenuItem>> GetAllWebMenuSectionsAsync(Guid tenantId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var filter = Builders<WebMenuSection>.Filter.Eq(e => e.TenantId, tenantId);
|
var filter = Builders<WebSideMenuItem>.Filter.Eq(e => e.TenantId, tenantId);
|
||||||
|
|
||||||
var result = await _webCollection
|
var result = await _collection
|
||||||
.Find(filter)
|
.Find(filter)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
if (result.Any())
|
if (result.Any())
|
||||||
@ -38,9 +37,9 @@ namespace Marco.Pms.CacheHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
|
tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
|
||||||
filter = Builders<WebMenuSection>.Filter.Eq(e => e.TenantId, tenantId);
|
filter = Builders<WebSideMenuItem>.Filter.Eq(e => e.TenantId, tenantId);
|
||||||
|
|
||||||
result = await _webCollection
|
result = await _collection
|
||||||
.Find(filter)
|
.Find(filter)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return result;
|
return result;
|
||||||
@ -48,229 +47,29 @@ namespace Marco.Pms.CacheHelper
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error occurred while fetching Web Menu Sections.");
|
_logger.LogError(ex, "Error occurred while fetching Web Menu Sections.");
|
||||||
return new List<WebMenuSection>();
|
return new List<WebSideMenuItem>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WebMenuSection?> CreateWebMenuSectionAsync(WebMenuSection section)
|
public async Task<List<WebSideMenuItem>> AddWebMenuItemAsync(List<WebSideMenuItem> newItems)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _webCollection.InsertOneAsync(section);
|
await _collection.InsertManyAsync(newItems);
|
||||||
return section;
|
return newItems;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error occurred while adding Web Menu Section.");
|
_logger.LogError(ex, "Error occurred while adding Web Menu Section.");
|
||||||
return null;
|
return new List<WebSideMenuItem>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task<WebMenuSection?> AddWebMenuItemAsync(Guid sectionId, List<WebSideMenuItem> newItems)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
var filter = Builders<WebMenuSection>.Filter.Eq(s => s.Id, sectionId);
|
|
||||||
|
|
||||||
var update = Builders<WebMenuSection>.Update.PushEach(s => s.Items, newItems);
|
|
||||||
|
|
||||||
var result = await _webCollection.UpdateOneAsync(filter, update);
|
|
||||||
|
|
||||||
if (result.ModifiedCount > 0)
|
|
||||||
{
|
|
||||||
return await _webCollection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error adding menu item.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public async Task<MenuSection?> CreateMenuSectionAsync(MenuSection section)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _collection.InsertOneAsync(section);
|
|
||||||
return section;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error occurred while adding MenuSection.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MenuSection?> UpdateMenuSectionAsync(Guid sectionId, MenuSection updatedSection)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var filter = Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId);
|
|
||||||
|
|
||||||
var update = Builders<MenuSection>.Update
|
|
||||||
.Set(s => s.Header, updatedSection.Header)
|
|
||||||
.Set(s => s.Title, updatedSection.Title)
|
|
||||||
.Set(s => s.Items, updatedSection.Items);
|
|
||||||
|
|
||||||
var result = await _collection.UpdateOneAsync(filter, update);
|
|
||||||
|
|
||||||
if (result.ModifiedCount > 0)
|
|
||||||
{
|
|
||||||
return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error updating MenuSection.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MenuSection?> AddMenuItemAsync(Guid sectionId, MenuItem newItem)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
newItem.Id = Guid.NewGuid();
|
|
||||||
|
|
||||||
var filter = Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId);
|
|
||||||
|
|
||||||
var update = Builders<MenuSection>.Update.Push(s => s.Items, newItem);
|
|
||||||
|
|
||||||
var result = await _collection.UpdateOneAsync(filter, update);
|
|
||||||
|
|
||||||
if (result.ModifiedCount > 0)
|
|
||||||
{
|
|
||||||
return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error adding menu item.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MenuItem?> UpdateMenuItemAsync(Guid sectionId, Guid itemId, MenuItem updatedItem)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var filter = Builders<MenuSection>.Filter.And(
|
|
||||||
Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId),
|
|
||||||
Builders<MenuSection>.Filter.ElemMatch(s => s.Items, i => i.Id == itemId)
|
|
||||||
);
|
|
||||||
|
|
||||||
var update = Builders<MenuSection>.Update
|
|
||||||
.Set("Items.$.Text", updatedItem.Text)
|
|
||||||
.Set("Items.$.Icon", updatedItem.Icon)
|
|
||||||
.Set("Items.$.Available", updatedItem.Available)
|
|
||||||
.Set("Items.$.Link", updatedItem.Link)
|
|
||||||
.Set("Items.$.PermissionIds", updatedItem.PermissionIds);
|
|
||||||
|
|
||||||
var result = await _collection.UpdateOneAsync(filter, update);
|
|
||||||
|
|
||||||
if (result.ModifiedCount > 0)
|
|
||||||
{
|
|
||||||
// Re-fetch section and return the updated item
|
|
||||||
var section = await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
|
|
||||||
return section?.Items.FirstOrDefault(i => i.Id == itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error updating MenuItem.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MenuSection?> AddSubMenuItemAsync(Guid sectionId, Guid itemId, SubMenuItem newSubItem)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
newSubItem.Id = Guid.NewGuid();
|
|
||||||
|
|
||||||
// Match the MenuSection and the specific MenuItem inside it
|
|
||||||
var filter = Builders<MenuSection>.Filter.And(
|
|
||||||
Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId),
|
|
||||||
Builders<MenuSection>.Filter.ElemMatch(s => s.Items, i => i.Id == itemId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use positional operator `$` to target matched MenuItem and push into its Submenu
|
|
||||||
var update = Builders<MenuSection>.Update.Push("Items.$.Submenu", newSubItem);
|
|
||||||
|
|
||||||
var result = await _collection.UpdateOneAsync(filter, update);
|
|
||||||
|
|
||||||
if (result.ModifiedCount > 0)
|
|
||||||
{
|
|
||||||
return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error adding submenu item.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SubMenuItem?> UpdateSubmenuItemAsync(Guid sectionId, Guid itemId, Guid subItemId, SubMenuItem updatedSub)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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()))
|
|
||||||
};
|
|
||||||
|
|
||||||
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.PermissionIds);
|
|
||||||
|
|
||||||
var options = new UpdateOptions { ArrayFilters = arrayFilters };
|
|
||||||
|
|
||||||
var result = await _collection.UpdateOneAsync(filter, update, options);
|
|
||||||
|
|
||||||
if (result.ModifiedCount == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var updatedSection = await _collection.Find(x => x.Id == sectionId).FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
var subItem = updatedSection?.Items
|
|
||||||
.FirstOrDefault(i => i.Id == itemId)?
|
|
||||||
.Submenu
|
|
||||||
.FirstOrDefault(s => s.Id == subItemId);
|
|
||||||
|
|
||||||
return subItem;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error updating SubMenuItem.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<MenuSection>> GetAllMenuSectionsAsync(Guid tenantId)
|
public async Task<List<MenuSection>> GetAllMenuSectionsAsync(Guid tenantId)
|
||||||
{
|
{
|
||||||
var filter = Builders<MenuSection>.Filter.Eq(e => e.TenantId, tenantId);
|
var filter = Builders<MenuSection>.Filter.Eq(e => e.TenantId, tenantId);
|
||||||
|
|
||||||
var result = await _collection
|
var result = await _oldCollection
|
||||||
.Find(filter)
|
.Find(filter)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
if (result.Any())
|
if (result.Any())
|
||||||
@ -281,7 +80,7 @@ namespace Marco.Pms.CacheHelper
|
|||||||
tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
|
tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
|
||||||
filter = Builders<MenuSection>.Filter.Eq(e => e.TenantId, tenantId);
|
filter = Builders<MenuSection>.Filter.Eq(e => e.TenantId, tenantId);
|
||||||
|
|
||||||
result = await _collection
|
result = await _oldCollection
|
||||||
.Find(filter)
|
.Find(filter)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -18,5 +18,8 @@ namespace Marco.Pms.Model.AppMenu
|
|||||||
|
|
||||||
[BsonRepresentation(BsonType.String)]
|
[BsonRepresentation(BsonType.String)]
|
||||||
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
|
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
|
||||||
|
|
||||||
|
[BsonRepresentation(BsonType.String)]
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,5 @@
|
|||||||
public string? Header { get; set; }
|
public string? Header { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public List<WebSideMenuItemVM> Items { get; set; } = new List<WebSideMenuItemVM>();
|
public List<WebSideMenuItemVM> Items { get; set; } = new List<WebSideMenuItemVM>();
|
||||||
public Guid TenantId { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,218 +53,254 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the sidebar menu for the current tenant and logged-in employee,
|
||||||
|
/// filtered by permission and structured for the web application UI.
|
||||||
|
/// </summary>
|
||||||
[HttpGet("get/menu")]
|
[HttpGet("get/menu")]
|
||||||
public async Task<IActionResult> GetAppSideBarMenuAsync()
|
public async Task<IActionResult> GetAppSideBarMenuAsync()
|
||||||
{
|
{
|
||||||
// Correlation ID for distributed tracing across services and logs.
|
// Correlation ID enables tracing this request across services and logs.
|
||||||
var correlationId = HttpContext.TraceIdentifier;
|
var correlationId = HttpContext.TraceIdentifier;
|
||||||
|
|
||||||
_logger.LogInfo("GetAppSideBarMenuAsync started. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId);
|
// Log the high-level intent and core context up front (no PII, no secrets).
|
||||||
|
_logger.LogInfo("GetAppSideBarMenuAsync started. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
|
tenantId, correlationId);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Step 1: Validate tenant context
|
// 1. Validate tenant context
|
||||||
if (tenantId == Guid.Empty)
|
if (tenantId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("GetAppSideBarMenuAsync rejected: Invalid TenantId. CorrelationId: {CorrelationId}", correlationId);
|
_logger.LogWarning("GetAppSideBarMenuAsync rejected due to invalid tenant. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
|
tenantId, correlationId);
|
||||||
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "The tenant identifier provided is invalid or missing.", 400));
|
var error = ApiResponse<object>.ErrorResponse("Invalid Tenant Identifier", "The tenant identifier is missing or invalid. Please verify the tenant context and try again.", 400);
|
||||||
|
|
||||||
|
return BadRequest(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Resolve current employee context for permission checks
|
// 2. Resolve current employee context
|
||||||
|
// - This call should throw or return a known result if the user is not authenticated.
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
var employeeId = loggedInEmployee.Id;
|
if (loggedInEmployee is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetAppSideBarMenuAsync failed: current employee not resolved. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
|
tenantId, correlationId);
|
||||||
|
|
||||||
// Step 3: Create scoped permission service (avoid capturing scoped services in controller)
|
var error = ApiResponse<object>.ErrorResponse("User Context Not Found", "The current user context could not be resolved. Please re-authenticate and try again.", 403);
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
|
||||||
var permissions = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
return StatusCode(403, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
var employeeId = loggedInEmployee.Id;
|
||||||
|
|
||||||
_logger.LogDebug("GetAppSideBarMenuAsync resolved employee context. EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
_logger.LogDebug("GetAppSideBarMenuAsync resolved employee context. EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
employeeId, tenantId, correlationId);
|
employeeId, tenantId, correlationId);
|
||||||
|
|
||||||
// Step 4: Fetch all menu sections for tenant (null-safe check)
|
// 3. Create scoped permission service
|
||||||
|
// - Avoid capturing scoped services directly in controller ctor when they depend on per-request state.
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
|
||||||
|
// 4. Preload all permission ids for the employee for efficient in-memory checks
|
||||||
|
var permissionIds = await permissionService.GetPermissionIdsByEmployeeId(employeeId);
|
||||||
|
|
||||||
|
_logger.LogDebug("GetAppSideBarMenuAsync loaded {PermissionCount} permissions for EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}",
|
||||||
|
permissionIds.Count, employeeId, correlationId);
|
||||||
|
|
||||||
|
// 5. Fetch all menu entries configured for this tenant
|
||||||
var menus = await _sideBarMenuHelper.GetAllWebMenuSectionsAsync(tenantId);
|
var menus = await _sideBarMenuHelper.GetAllWebMenuSectionsAsync(tenantId);
|
||||||
|
|
||||||
if (menus == null || !menus.Any())
|
if (menus == null || !menus.Any())
|
||||||
{
|
{
|
||||||
_logger.LogInfo("GetAppSideBarMenuAsync: No menu sections found. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
_logger.LogInfo("GetAppSideBarMenuAsync: No menu sections found for tenant. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
tenantId, correlationId);
|
tenantId, correlationId);
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(new List<WebMenuSectionVM>(), "No sidebar menu sections configured for this tenant.", 200));
|
|
||||||
|
var emptyResponse = new List<WebMenuSectionVM>();
|
||||||
|
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(emptyResponse, "No sidebar menu sections are configured for the current tenant.", 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("GetAppSideBarMenuAsync loaded {MenuSectionCount} menu sections. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
_logger.LogDebug("GetAppSideBarMenuAsync loaded {MenuSectionCount} raw menu records. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
menus.Count, tenantId, correlationId);
|
menus.Count, tenantId, correlationId);
|
||||||
|
|
||||||
// Step 5: Filter and build menu response with permission checks
|
// 6. Build logical menu sections (root + children) and apply permission filtering
|
||||||
var response = new List<WebMenuSectionVM>();
|
var responseSections = new List<WebMenuSectionVM>();
|
||||||
|
|
||||||
foreach (var menuSection in menus)
|
// Root container section for the web UI.
|
||||||
|
var rootSection = new WebMenuSectionVM
|
||||||
{
|
{
|
||||||
var sectionVM = _mapper.Map<WebMenuSectionVM>(menuSection);
|
Id = Guid.Parse("4885d9f4-89b8-447d-9a95-7434b343dfda"),
|
||||||
if (menuSection.Items == null)
|
Header = "Main Navigation",
|
||||||
|
Name = "Main Menu"
|
||||||
|
};
|
||||||
|
|
||||||
|
// To avoid multiple enumerations and improve readability, materialize once.
|
||||||
|
var menusList = menus.ToList();
|
||||||
|
|
||||||
|
foreach (var menu in menusList)
|
||||||
|
{
|
||||||
|
// Skip any non-root menu entry; these will be attached as children.
|
||||||
|
if (menu.ParentMenuId.HasValue)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Skipping menu section with null items. SectionId: {SectionId}, TenantId: {TenantId}", menuSection.Id, tenantId);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var itemVm = _mapper.Map<WebSideMenuItemVM>(menu);
|
||||||
|
|
||||||
foreach (var menuItem in menuSection.Items)
|
// If the menu requires any permission, check now.
|
||||||
|
if (menu.PermissionIds.Any())
|
||||||
{
|
{
|
||||||
// Skip items without permission check if required
|
var hasMenuPermission = permissionService.HasPermissionAny(permissionIds, menu.PermissionIds, employeeId);
|
||||||
if (menuItem.PermissionIds.Any())
|
|
||||||
|
if (!hasMenuPermission)
|
||||||
{
|
{
|
||||||
var hasPermission = await permissions.HasPermissionAny(menuItem.PermissionIds, employeeId);
|
_logger.LogDebug("Menu item skipped due to insufficient permissions. MenuId: {MenuId}, EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}",
|
||||||
if (!hasPermission)
|
menu.Id, employeeId, correlationId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve submenus only for eligible root menu entries.
|
||||||
|
var subMenus = menusList
|
||||||
|
.Where(m => m.ParentMenuId.HasValue && m.ParentMenuId == menu.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var subMenu in subMenus)
|
||||||
|
{
|
||||||
|
var subMenuVm = _mapper.Map<WebSideMenuItemVM>(subMenu);
|
||||||
|
|
||||||
|
// If the submenu requires permissions, validate before adding.
|
||||||
|
if (subMenu.PermissionIds.Any())
|
||||||
|
{
|
||||||
|
var hasSubPermission = permissionService.HasPermissionAny(permissionIds, subMenu.PermissionIds, employeeId);
|
||||||
|
|
||||||
|
if (!hasSubPermission)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Menu item access denied due to missing permissions. ItemId: {ItemId}, EmployeeId: {EmployeeId}, TenantId: {TenantId}",
|
_logger.LogDebug(
|
||||||
menuItem.Id, employeeId, tenantId);
|
"Submenu item skipped due to insufficient permissions. MenuId: {MenuId}, ParentMenuId: {ParentMenuId}, EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}",
|
||||||
|
subMenu.Id, subMenu.ParentMenuId!, employeeId, correlationId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemVM = _mapper.Map<WebSideMenuItemVM>(menuItem);
|
// Add the submenu to the root section
|
||||||
if (menuItem.ParentMenuId.HasValue)
|
itemVm.Submenu.Add(subMenuVm);
|
||||||
{
|
|
||||||
sectionVM.Items.Where(i => i.Id == menuItem.ParentMenuId.Value).FirstOrDefault()?.Submenu.Add(itemVM);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sectionVM.Items.Add(itemVM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include sections with at least one accessible item
|
|
||||||
if (sectionVM.Items.Any())
|
|
||||||
{
|
|
||||||
response.Add(sectionVM);
|
|
||||||
}
|
}
|
||||||
|
// Add the root menu item
|
||||||
|
rootSection.Items.Add(itemVm);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInfo("GetAppSideBarMenuAsync completed successfully. TenantId: {TenantId}, EmployeeId: {EmployeeId}, OriginalSections: {OriginalCount}, FilteredSections: {FilteredCount}, CorrelationId: {CorrelationId}",
|
if (rootSection.Items.Any())
|
||||||
tenantId, employeeId, menus.Count, response.Count, correlationId);
|
{
|
||||||
|
responseSections.Add(rootSection);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(response, $"Sidebar menu fetched successfully. {response.Count} sections returned.", 200));
|
_logger.LogInfo(
|
||||||
|
"GetAppSideBarMenuAsync completed successfully. TenantId: {TenantId}, EmployeeId: {EmployeeId}, OriginalMenuCount: {OriginalCount}, ReturnedSectionCount: {SectionCount}, CorrelationId: {CorrelationId}",
|
||||||
|
tenantId, employeeId, menusList.Count, responseSections.Count, correlationId);
|
||||||
|
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(responseSections,
|
||||||
|
responseSections.Any()
|
||||||
|
? $"Sidebar menu fetched successfully. {responseSections.Count} section(s) returned."
|
||||||
|
: "No accessible sidebar menu items were found for the current user.", 200));
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("GetAppSideBarMenuAsync cancelled. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
// This typically indicates client disconnected or explicit cancellation.
|
||||||
|
_logger.LogWarning("GetAppSideBarMenuAsync was cancelled. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
tenantId, correlationId);
|
tenantId, correlationId);
|
||||||
|
|
||||||
return StatusCode(499, ApiResponse<object>.ErrorResponse("Request Cancelled", "The request was cancelled by the client.", 499));
|
var error = ApiResponse<object>.ErrorResponse("Request Cancelled", "The operation was cancelled, likely due to a client disconnection or explicit cancellation request.", 499); // Non-standard but commonly used in APIs for client closed request.
|
||||||
|
|
||||||
|
return StatusCode(499, error);
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "GetAppSideBarMenuAsync authorization failed. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
// Handle authorization-related errors explicitly to avoid leaking details.
|
||||||
|
_logger.LogError(ex, "GetAppSideBarMenuAsync authorization failure. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
tenantId, correlationId);
|
tenantId, correlationId);
|
||||||
|
|
||||||
return StatusCode(403, (ApiResponse<object>.ErrorResponse("Access Denied", "Insufficient permissions to access menu sections.", 403)));
|
var error = ApiResponse<object>.ErrorResponse("Access Denied", "You do not have sufficient permissions to access the sidebar menu.", 403);
|
||||||
|
|
||||||
|
return StatusCode(403, error);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Unexpected error in GetAppSideBarMenuAsync. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
// Fallback handler for unexpected failures. Keep log detailed, response generic.
|
||||||
|
_logger.LogError(ex, "Unhandled exception in GetAppSideBarMenuAsync. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
|
||||||
tenantId, correlationId);
|
tenantId, correlationId);
|
||||||
|
|
||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred while fetching the sidebar menu.", 500));
|
var error = ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing the sidebar menu request. Please try again later.", 500);
|
||||||
|
|
||||||
|
return StatusCode(500, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("add/sidebar/menu-section")]
|
|
||||||
public async Task<IActionResult> CreateAppSideBarMenuAsync([FromBody] CreateWebMenuSectionDto model)
|
[HttpPost("add/side-menu")]
|
||||||
|
public async Task<IActionResult> AddMenuItemAsync([FromBody] List<CreateWebSideMenuItemDto> model)
|
||||||
{
|
{
|
||||||
// Step 1: Fetch logged-in user
|
// Step 1: Validate tenant context
|
||||||
|
if (tenantId == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetAppSideBarMenuAsync rejected: Invalid TenantId.");
|
||||||
|
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "The tenant identifier provided is invalid or missing.", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Fetch logged-in user
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
|
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
|
||||||
|
|
||||||
// Step 2: Authorization check
|
// Step 3: Authorization check
|
||||||
if (!isRootUser || tenantId != superTenantId)
|
|
||||||
{
|
|
||||||
_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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Map DTO to entity
|
|
||||||
var sideMenuSection = _mapper.Map<WebMenuSection>(model);
|
|
||||||
sideMenuSection.TenantId = tenantId;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Step 4: Save entity using helper
|
|
||||||
sideMenuSection = await _sideBarMenuHelper.CreateWebMenuSectionAsync(sideMenuSection);
|
|
||||||
|
|
||||||
if (sideMenuSection == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Failed to create sidebar menu section. Tenant: {TenantId}, Request: {@MenuSectionDto}", tenantId, model);
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
// 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, model);
|
|
||||||
|
|
||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server Error", "An unexpected error occurred.", 500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("add/sidebar/section/{sectionId}/items")]
|
|
||||||
public async Task<IActionResult> AddMenuItemAsync(Guid sectionId, [FromBody] List<CreateWebSideMenuItemDto> model)
|
|
||||||
{
|
|
||||||
// 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)
|
if (!isRootUser && tenantId != superTenantId)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Access denied: User {UserId} attempted to add menu item to section {SectionId} in Tenant {TenantId}",
|
_logger.LogWarning("Access denied: User {UserId} attempted to add menu item in Tenant {TenantId}",
|
||||||
loggedInEmployee.Id, sectionId, tenantId);
|
loggedInEmployee.Id, tenantId);
|
||||||
|
|
||||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
|
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Input validation
|
// Step 4: Input validation
|
||||||
if (sectionId == Guid.Empty || model == null)
|
if (model == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Invalid AddMenuItem request. Tenant: {TenantId}, SectionId: {SectionId}, UserId: {UserId}",
|
_logger.LogWarning("Invalid AddMenuItem request. Tenant: {TenantId}, UserId: {UserId}",
|
||||||
tenantId, sectionId, loggedInEmployee.Id);
|
tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid section ID or menu item payload.", 400));
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid section ID or menu item payload.", 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Step 4: Map DTO to entity
|
// Step 5: Map DTO to entity
|
||||||
var menuItemEntity = _mapper.Map<List<WebSideMenuItem>>(model);
|
var menuItemEntity = _mapper.Map<List<WebSideMenuItem>>(model);
|
||||||
|
|
||||||
// Step 5: Perform Add operation
|
menuItemEntity.ForEach(m => m.TenantId = tenantId);
|
||||||
var result = await _sideBarMenuHelper.AddWebMenuItemAsync(sectionId, menuItemEntity);
|
|
||||||
|
// Step 6: Perform Add operation
|
||||||
|
var result = await _sideBarMenuHelper.AddWebMenuItemAsync(menuItemEntity);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Menu section not found. Unable to add menu item. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}",
|
_logger.LogWarning("Menu section not found. Unable to add menu item. TenantId: {TenantId}, UserId: {UserId}", tenantId, loggedInEmployee.Id);
|
||||||
sectionId, tenantId, loggedInEmployee.Id);
|
|
||||||
|
|
||||||
return NotFound(ApiResponse<object>.ErrorResponse("Menu section not found", 404));
|
return NotFound(ApiResponse<object>.ErrorResponse("Menu section not found", 404));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Successful addition
|
// Step 7: Successful addition
|
||||||
_logger.LogInfo("Menu item added successfully. SectionId: {SectionId}, MenuItemId: {MenuItemId}, TenantId: {TenantId}, UserId: {UserId}",
|
_logger.LogInfo("Menu items added successfully TenantId: {TenantId}, UserId: {UserId}",
|
||||||
sectionId, result.Id, tenantId, loggedInEmployee.Id);
|
tenantId, loggedInEmployee.Id);
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(result, "Menu item added successfully"));
|
return Ok(ApiResponse<object>.SuccessResponse(result, "Menu item added successfully"));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Step 7: Handle unexpected errors
|
// Step 8: Handle unexpected errors
|
||||||
_logger.LogError(ex, "Error occurred while adding menu item. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}, Payload: {@NewItemDto}",
|
_logger.LogError(ex, "Error occurred while adding menu item. TenantId: {TenantId}, UserId: {UserId}, Payload: {@NewItemDto}",
|
||||||
sectionId, tenantId, loggedInEmployee.Id, model);
|
tenantId, loggedInEmployee.Id, model);
|
||||||
|
|
||||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server error", "An unexpected error occurred while adding the menu item.", 500));
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server error", "An unexpected error occurred while adding the menu item.", 500));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -384,7 +384,10 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
response.CreatedBy = createdBy;
|
response.CreatedBy = createdBy;
|
||||||
|
|
||||||
response.CurrentPlan = _mapper.Map<SubscriptionPlanDetailsVM>(currentPlan);
|
response.CurrentPlan = _mapper.Map<SubscriptionPlanDetailsVM>(currentPlan);
|
||||||
response.CurrentPlan.PaymentDetail = paymentsDetails.FirstOrDefault(pd => currentPlan != null && pd.Id == currentPlan.PaymentDetailId);
|
if (currentPlan != null)
|
||||||
|
{
|
||||||
|
response.CurrentPlan.PaymentDetail = paymentsDetails.FirstOrDefault(pd => pd.Id == currentPlan.PaymentDetailId);
|
||||||
|
}
|
||||||
|
|
||||||
response.CurrentPlanFeatures = await _featureDetailsHelper.GetFeatureDetails(currentPlan?.Plan?.FeaturesId ?? Guid.Empty);
|
response.CurrentPlanFeatures = await _featureDetailsHelper.GetFeatureDetails(currentPlan?.Plan?.FeaturesId ?? Guid.Empty);
|
||||||
// Map subscription history plans to DTO
|
// Map subscription history plans to DTO
|
||||||
|
|||||||
@ -809,7 +809,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
}
|
}
|
||||||
Task<List<string>> getPermissionIdsTask = Task.Run(async () =>
|
Task<List<string>> getPermissionIdsTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
using var context = _dbContextFactory.CreateDbContext();
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
return await context.RolePermissionMappings
|
return await context.RolePermissionMappings
|
||||||
.Where(rp => roleIds.Contains(rp.ApplicationRoleId))
|
.Where(rp => roleIds.Contains(rp.ApplicationRoleId))
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using Marco.Pms.Model.Entitlements;
|
|||||||
using Marco.Pms.Services.Helpers;
|
using Marco.Pms.Services.Helpers;
|
||||||
using MarcoBMS.Services.Helpers;
|
using MarcoBMS.Services.Helpers;
|
||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Service
|
namespace Marco.Pms.Services.Service
|
||||||
@ -11,15 +12,13 @@ namespace Marco.Pms.Services.Service
|
|||||||
public class PermissionServices
|
public class PermissionServices
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
private readonly RolesHelper _rolesHelper;
|
|
||||||
private readonly CacheUpdateHelper _cache;
|
private readonly CacheUpdateHelper _cache;
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly Guid tenantId;
|
private readonly Guid tenantId;
|
||||||
|
|
||||||
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, ILoggingService logger, UserHelper userHelper)
|
public PermissionServices(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, UserHelper userHelper)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_rolesHelper = rolesHelper;
|
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
@ -34,72 +33,23 @@ namespace Marco.Pms.Services.Service
|
|||||||
/// <returns>True if the user has the permission, otherwise false.</returns>
|
/// <returns>True if the user has the permission, otherwise false.</returns>
|
||||||
public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId, Guid? projectId = null)
|
public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId, Guid? projectId = null)
|
||||||
{
|
{
|
||||||
// 1. Try fetching permissions from cache (fast-path lookup).
|
var featurePermissionIds = await GetPermissionIdsByEmployeeId(employeeId, projectId);
|
||||||
var featurePermissionIds = await _cache.GetPermissions(employeeId, tenantId);
|
|
||||||
|
|
||||||
// If not found in cache, fallback to database (slower).
|
|
||||||
if (featurePermissionIds == null)
|
|
||||||
{
|
|
||||||
var featurePermissions = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId, tenantId);
|
|
||||||
featurePermissionIds = featurePermissions.Select(fp => fp.Id).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Handle project-level permission overrides if a project is specified.
|
|
||||||
if (projectId.HasValue)
|
|
||||||
{
|
|
||||||
// Fetch permissions explicitly assigned to this employee in the project.
|
|
||||||
var projectLevelPermissionIds = await _context.ProjectLevelPermissionMappings
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(pl => pl.ProjectId == projectId.Value && pl.EmployeeId == employeeId)
|
|
||||||
.Select(pl => pl.PermissionId)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (projectLevelPermissionIds?.Any() ?? false)
|
|
||||||
{
|
|
||||||
|
|
||||||
// Define modules where project-level overrides apply.
|
|
||||||
var projectLevelModuleIds = new HashSet<Guid>
|
|
||||||
{
|
|
||||||
Guid.Parse("53176ebf-c75d-42e5-839f-4508ffac3def"),
|
|
||||||
Guid.Parse("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"),
|
|
||||||
Guid.Parse("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"),
|
|
||||||
Guid.Parse("a8cf4331-8f04-4961-8360-a3f7c3cc7462")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all feature permissions under those modules where the user didn't have explicit project-level grants.
|
|
||||||
var allOverriddenPermissions = await _context.FeaturePermissions
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(fp => projectLevelModuleIds.Contains(fp.FeatureId) &&
|
|
||||||
!projectLevelPermissionIds.Contains(fp.Id))
|
|
||||||
.Select(fp => fp.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
// Apply overrides:
|
|
||||||
// - Remove global permissions overridden by project-level rules.
|
|
||||||
// - Add explicit project-level permissions.
|
|
||||||
featurePermissionIds = featurePermissionIds
|
|
||||||
.Except(allOverriddenPermissions) // Remove overridden
|
|
||||||
.Concat(projectLevelPermissionIds) // Add project-level
|
|
||||||
.Distinct() // Ensure no duplicates
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Final check: does the employee have the requested permission?
|
|
||||||
return featurePermissionIds.Contains(featurePermissionId);
|
return featurePermissionIds.Contains(featurePermissionId);
|
||||||
}
|
}
|
||||||
public async Task<bool> HasPermissionAny(List<Guid> featurePermissionIds, Guid employeeId)
|
public async Task<bool> HasPermissionAny(List<Guid> featurePermissionIds, Guid employeeId)
|
||||||
{
|
{
|
||||||
var allFeaturePermissionIds = await _cache.GetPermissions(employeeId, tenantId);
|
var allFeaturePermissionIds = await GetPermissionIdsByEmployeeId(employeeId);
|
||||||
if (allFeaturePermissionIds == null)
|
|
||||||
{
|
|
||||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId, tenantId);
|
|
||||||
allFeaturePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
|
|
||||||
}
|
|
||||||
var hasPermission = featurePermissionIds.Any(f => allFeaturePermissionIds.Contains(f));
|
var hasPermission = featurePermissionIds.Any(f => allFeaturePermissionIds.Contains(f));
|
||||||
|
|
||||||
return hasPermission;
|
return hasPermission;
|
||||||
}
|
}
|
||||||
|
public bool HasPermissionAny(List<Guid> realPermissionIds, List<Guid> toCheckPermissionIds, Guid employeeId)
|
||||||
|
{
|
||||||
|
var hasPermission = toCheckPermissionIds.Any(f => realPermissionIds.Contains(f));
|
||||||
|
return hasPermission;
|
||||||
|
}
|
||||||
public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
|
public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
|
||||||
{
|
{
|
||||||
var employeeId = LoggedInEmployee.Id;
|
var employeeId = LoggedInEmployee.Id;
|
||||||
@ -199,5 +149,164 @@ namespace Marco.Pms.Services.Service
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves permission IDs for an employee, supporting both global role-based permissions
|
||||||
|
/// and project-specific overrides with cache-first strategy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="employeeId">The ID of the employee to fetch permissions for.</param>
|
||||||
|
/// <param name="projectId">Optional project ID for project-level permission overrides.</param>
|
||||||
|
/// <returns>List of unique permission IDs the employee has access to.</returns>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when employeeId or tenantId is empty.</exception>
|
||||||
|
public async Task<List<Guid>> GetPermissionIdsByEmployeeId(Guid employeeId, Guid? projectId = null)
|
||||||
|
{
|
||||||
|
// Input validation
|
||||||
|
if (employeeId == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("EmployeeId cannot be empty.");
|
||||||
|
return new List<Guid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tenantId == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("TenantId cannot be empty.");
|
||||||
|
return new List<Guid>();
|
||||||
|
}
|
||||||
|
_logger.LogDebug(
|
||||||
|
"GetPermissionIdsByEmployeeId started. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, TenantId: {TenantId}",
|
||||||
|
employeeId, projectId ?? Guid.Empty, tenantId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Phase 1: Cache-first lookup for role-based permissions (fast path)
|
||||||
|
var featurePermissionIds = await _cache.GetPermissions(employeeId, tenantId);
|
||||||
|
var permissionsFromCache = featurePermissionIds != null;
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Permission lookup from cache: {CacheHit}, InitialPermissions: {PermissionCount}, EmployeeId: {EmployeeId}",
|
||||||
|
permissionsFromCache, featurePermissionIds?.Count ?? 0, employeeId);
|
||||||
|
|
||||||
|
// Phase 2: Database fallback if cache miss
|
||||||
|
if (featurePermissionIds == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Cache miss detected, falling back to database lookup. EmployeeId: {EmployeeId}, TenantId: {TenantId}",
|
||||||
|
employeeId, tenantId);
|
||||||
|
|
||||||
|
var roleIds = await _context.EmployeeRoleMappings
|
||||||
|
.Where(erm => erm.EmployeeId == employeeId && erm.TenantId == tenantId)
|
||||||
|
.Select(erm => erm.RoleId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!roleIds.Any())
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"No roles found for employee. EmployeeId: {EmployeeId}, TenantId: {TenantId}",
|
||||||
|
employeeId, tenantId);
|
||||||
|
return new List<Guid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
featurePermissionIds = await _context.RolePermissionMappings
|
||||||
|
.Where(rpm => roleIds.Contains(rpm.ApplicationRoleId))
|
||||||
|
.Select(rpm => rpm.FeaturePermissionId)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// The cache service might also need its own context, or you can pass the data directly.
|
||||||
|
// Assuming AddApplicationRole takes the data, not a context.
|
||||||
|
await _cache.AddApplicationRole(employeeId, roleIds, tenantId);
|
||||||
|
_logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", employeeId);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Loaded {RoleCount} roles → {PermissionCount} permissions from database. EmployeeId: {EmployeeId}",
|
||||||
|
roleIds.Count, featurePermissionIds.Count, employeeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early return for global permissions (no project context)
|
||||||
|
if (!projectId.HasValue)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Returning global permissions. Count: {PermissionCount}, EmployeeId: {EmployeeId}",
|
||||||
|
featurePermissionIds.Count, employeeId);
|
||||||
|
return featurePermissionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Apply project-level permission overrides
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Applying project-level overrides. ProjectId: {ProjectId}, EmployeeId: {EmployeeId}",
|
||||||
|
projectId.Value, employeeId);
|
||||||
|
|
||||||
|
var projectLevelPermissionIds = await _context.ProjectLevelPermissionMappings
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(pl => pl.ProjectId == projectId.Value &&
|
||||||
|
pl.EmployeeId == employeeId)
|
||||||
|
.Select(pl => pl.PermissionId)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!projectLevelPermissionIds.Any())
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"No project-level permissions found. ProjectId: {ProjectId}, EmployeeId: {EmployeeId}",
|
||||||
|
projectId.Value, employeeId);
|
||||||
|
return featurePermissionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4: Override logic for specific project modules
|
||||||
|
var projectOverrideModules = new HashSet<Guid>
|
||||||
|
{
|
||||||
|
// Hard-coded module IDs for project-level override scope
|
||||||
|
// TODO: Consider moving to configuration or database lookup
|
||||||
|
Guid.Parse("53176ebf-c75d-42e5-839f-4508ffac3def"), // Module: Projects
|
||||||
|
Guid.Parse("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), // Module: Expenses
|
||||||
|
Guid.Parse("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Module: Invoices
|
||||||
|
Guid.Parse("a8cf4331-8f04-4961-8360-a3f7c3cc7462") // Module: Documents
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find permissions in override modules that employee lacks at project level
|
||||||
|
var overriddenPermissions = await _context.FeaturePermissions
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(fp => projectOverrideModules.Contains(fp.FeatureId) &&
|
||||||
|
!projectLevelPermissionIds.Contains(fp.Id))
|
||||||
|
.Select(fp => fp.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Apply override rules:
|
||||||
|
// 1. Remove global permissions overridden by project context
|
||||||
|
// 2. Add explicit project-level grants
|
||||||
|
// 3. Ensure uniqueness
|
||||||
|
var finalPermissions = featurePermissionIds
|
||||||
|
.Except(overriddenPermissions) // Remove overridden global perms
|
||||||
|
.Concat(projectLevelPermissionIds) // Add project-specific grants
|
||||||
|
.Distinct() // Deduplicate
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Project override applied. Before: {BeforeCount}, After: {AfterCount}, Added: {AddedCount}, Removed: {RemovedCount}, EmployeeId: {EmployeeId}",
|
||||||
|
featurePermissionIds.Count, finalPermissions.Count,
|
||||||
|
projectLevelPermissionIds.Count, overriddenPermissions.Count, employeeId);
|
||||||
|
|
||||||
|
return finalPermissions;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetPermissionIdsByEmployeeId cancelled. EmployeeId: {EmployeeId}", employeeId);
|
||||||
|
|
||||||
|
return new List<Guid>();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Database error in GetPermissionIdsByEmployeeId. EmployeeId: {EmployeeId}, TenantId: {TenantId}", employeeId, tenantId);
|
||||||
|
|
||||||
|
return new List<Guid>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unexpected error in GetPermissionIdsByEmployeeId. EmployeeId: {EmployeeId}, TenantId: {TenantId}", employeeId, tenantId);
|
||||||
|
|
||||||
|
return new List<Guid>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user