Only sending the feature that tenant has permission of

This commit is contained in:
ashutosh.nehete 2025-08-18 11:46:42 +05:30
parent 288c0fe492
commit cf161e4a04
4 changed files with 241 additions and 25 deletions

View File

@ -1,9 +1,11 @@
using Marco.Pms.DataAccess.Data; using AutoMapper;
using Marco.Pms.Model.Entitlements; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Master; using Marco.Pms.Model.Master;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -15,43 +17,133 @@ namespace MarcoBMS.Services.Controllers
public class FeatureController : ControllerBase public class FeatureController : ControllerBase
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly GeneralHelper _generalHelper;
//private readonly UserHelper _userHelper;
private readonly IMapper _mapper;
private readonly ILoggingService _logger;
private readonly Guid tenantId;
public FeatureController(ApplicationDbContext context, GeneralHelper generalHelper, UserHelper userHelper, IMapper mapper, ILoggingService logger)
public FeatureController(ApplicationDbContext context)
{ {
_context = context; _context = context;
_generalHelper = generalHelper;
//_userHelper = userHelper;
_mapper = mapper;
_logger = logger;
tenantId = userHelper.GetTenantId();
} }
private ICollection<FeaturePermissionVM> GetFeaturePermissionVMs(Feature model) private ICollection<FeaturePermissionVM> GetFeaturePermissionVM(Feature model)
{ {
ICollection<FeaturePermissionVM> features = []; if (model.FeaturePermissions == null)
if (model.FeaturePermissions != null)
{ {
foreach (FeaturePermission permission in model.FeaturePermissions) return [];
{
FeaturePermissionVM item = permission.ToFeaturePermissionVMFromFeaturePermission();
features.Add(item);
}
} }
return features.OrderBy(f => f.Name).ToList();
ICollection<FeaturePermissionVM> features = model.FeaturePermissions.Select(p => _mapper.Map<FeaturePermissionVM>(p)).OrderBy(f => f.Name).ToList();
return features;
} }
[HttpGet] [HttpGet("features")]
public async Task<IActionResult> GetAllFeatures() public async Task<IActionResult> GetAllFeatures()
{ {
var roles = await _context.Features.Include("FeaturePermissions").Include("Module").ToListAsync(); List<Guid> featureIds = await _generalHelper.GetFeatureIdsByTenentId(tenantId);
var roles = await _context.Features
.Include(f => f.FeaturePermissions)
.Include(f => f.Module)
.Where(f => featureIds.Contains(f.Id))
.ToListAsync();
var rolesVM = roles.Select(c => new FeatureVM() var rolesVM = roles.Select(c => new FeatureVM()
{ {
Id = c.Id, Id = c.Id,
Name = c.Name, Name = c.Name,
Description = c.Description, Description = c.Description,
FeaturePermissions = GetFeaturePermissionVMs(c), FeaturePermissions = GetFeaturePermissionVM(c),
ModuleId = c.ModuleId, ModuleId = c.ModuleId,
ModuleName = c.Module != null ? c.Module.Name : string.Empty, ModuleName = c.Module != null ? c.Module.Name : string.Empty,
IsActive = c.IsActive IsActive = c.IsActive
}).OrderBy(f => f.Name).ToList(); }).OrderBy(f => f.Name).ToList();
return Ok(ApiResponse<object>.SuccessResponse(rolesVM, "Success.", 200)); return Ok(ApiResponse<object>.SuccessResponse(rolesVM, "Success.", 200));
} }
/// <summary>
/// Converts FeaturePermissions from Feature entity into FeaturePermissionVM collection.
/// </summary>
/// <param name="model">Feature entity from DB</param>
/// <returns>Collection of FeaturePermissionVM, ordered by Name</returns>
private ICollection<FeaturePermissionVM> GetFeaturePermissionVMs(Feature model)
{
if (model.FeaturePermissions == null || !model.FeaturePermissions.Any())
{
_logger.LogInfo("No feature permissions found for Feature: {FeatureId}", model.Id);
return new List<FeaturePermissionVM>();
}
// Project and order feature permissions
var features = model.FeaturePermissions
.Select(p => _mapper.Map<FeaturePermissionVM>(p))
.OrderBy(f => f.Name)
.ToList();
_logger.LogDebug("Mapped {Count} feature permissions for Feature: {FeatureId}", features.Count, model.Id);
return features;
}
/// <summary>
/// API endpoint to fetch all features and their permissions for the given tenant.
/// </summary>
[HttpGet]
public async Task<IActionResult> GetAllFeaturesAsync()
{
try
{
_logger.LogInfo("Fetching all features for tenant: {TenantId}", tenantId);
// Step 1: Get tenant-specific FeatureIds
List<Guid> featureIds = await _generalHelper.GetFeatureIdsByTenentIdAsync(tenantId);
if (featureIds == null || !featureIds.Any())
{
_logger.LogWarning("No features found for tenant: {TenantId}", tenantId);
return Ok(ApiResponse<object>.SuccessResponse(new List<FeatureVM>(), "No features found.", 200));
}
_logger.LogDebug("Retrieved {Count} feature IDs for tenant: {TenantId}", featureIds.Count, tenantId);
// Step 2: Query Features with related FeaturePermissions & Module
var features = await _context.Features
.AsNoTracking() // Optimization: Read-only query
.Include(f => f.FeaturePermissions)
.Include(f => f.Module)
.Where(f => featureIds.Contains(f.Id))
.ToListAsync();
_logger.LogDebug("Fetched {Count} features from DB for tenant: {TenantId}", features.Count, tenantId);
// Step 3: Map features to ViewModels
var featureVMs = features
.Select(c => new FeatureVM
{
Id = c.Id,
Name = c.Name,
Description = c.Description,
FeaturePermissions = GetFeaturePermissionVMs(c),
ModuleId = c.ModuleId,
ModuleName = c.Module?.Name ?? string.Empty,
IsActive = c.IsActive
})
.OrderBy(f => f.Name)
.ToList();
_logger.LogInfo("Returning {Count} features for tenant: {TenantId}", featureVMs.Count, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(featureVMs, "Success.", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while fetching features for tenant: {TenantId}", tenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", 500));
}
}
} }
} }

View File

@ -57,13 +57,13 @@ namespace Marco.Pms.Services.Controllers
UserHelper userHelper, UserHelper userHelper,
FeatureDetailsHelper featureDetailsHelper) FeatureDetailsHelper featureDetailsHelper)
{ {
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); ; _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); ; _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); ; _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); ; _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); ; _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); ; _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper)); ; _featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper));
} }
#region =================================================================== Tenant APIs =================================================================== #region =================================================================== Tenant APIs ===================================================================

View File

@ -1,4 +1,5 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.MongoDBModels.Masters; using Marco.Pms.Model.MongoDBModels.Masters;
using Marco.Pms.Model.MongoDBModels.Project; using Marco.Pms.Model.MongoDBModels.Project;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
@ -11,13 +12,16 @@ namespace Marco.Pms.Services.Helpers
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory; private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly FeatureDetailsHelper _featureDetailsHelper;
public GeneralHelper(IDbContextFactory<ApplicationDbContext> dbContextFactory, public GeneralHelper(IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context, ApplicationDbContext context,
ILoggingService logger) ILoggingService logger,
FeatureDetailsHelper featureDetailsHelper)
{ {
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_context = context ?? throw new ArgumentNullException(nameof(context)); _context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper));
} }
public async Task<List<BuildingMongoDB>> GetProjectInfraFromDB(Guid projectId) public async Task<List<BuildingMongoDB>> GetProjectInfraFromDB(Guid projectId)
{ {
@ -211,5 +215,122 @@ namespace Marco.Pms.Services.Helpers
return new List<WorkItemMongoDB>(); return new List<WorkItemMongoDB>();
} }
} }
public async Task<List<Guid>> GetFeatureIdsByTenentId(Guid tenantId)
{
var tenantSubscription = await _context.TenantSubscriptions.Include(ts => ts.Plan)
.FirstOrDefaultAsync(ts => ts.TenantId == tenantId && ts.Plan != null && !ts.IsCancelled && ts.EndDate.Date < DateTime.UtcNow.Date);
if (tenantSubscription == null)
{
_logger.LogWarning("Cannot found the tenant subscription for tenant {TenantId}", tenantId);
return new List<Guid>();
}
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);
if (featureDetails == null)
{
_logger.LogWarning("Cannot found the feature details for tenant {TenantId}", tenantId);
return new List<Guid>();
}
var featureIds = new List<Guid>();
if (featureDetails.Modules!.Attendance!.Enabled)
{
featureIds.AddRange(featureDetails.Modules.Attendance.FeatureId);
}
if (featureDetails.Modules.ProjectManagement!.Enabled)
{
featureIds.AddRange(featureDetails.Modules.ProjectManagement.FeatureId);
}
if (featureDetails.Modules.Directory!.Enabled)
{
featureIds.AddRange(featureDetails.Modules.Directory.FeatureId);
}
if (featureDetails.Modules.Expense!.Enabled)
{
featureIds.AddRange(featureDetails.Modules.Expense.FeatureId);
}
return featureIds;
}
/// <summary>
/// Retrieves all enabled feature IDs for a given tenant based on their active subscription.
/// </summary>
/// <param name="tenantId">The unique identifier of the tenant.</param>
/// <returns>A list of feature IDs available for the tenant.</returns>
public async Task<List<Guid>> GetFeatureIdsByTenentIdAsync(Guid tenantId)
{
try
{
_logger.LogInfo("Fetching feature IDs for tenant: {TenantId}", tenantId);
// Step 1: Get active tenant subscription with plan
var tenantSubscription = await _context.TenantSubscriptions
.Include(ts => ts.Plan)
.AsNoTracking() // Optimization: Read-only query, no need to track
.FirstOrDefaultAsync(ts =>
ts.TenantId == tenantId &&
ts.Plan != null &&
!ts.IsCancelled &&
ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired
if (tenantSubscription == null)
{
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
return new List<Guid>();
}
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
tenantId, tenantSubscription.Plan!.Id);
// Step 2: Get feature details from Plan
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);
if (featureDetails == null)
{
_logger.LogWarning("No feature details found for tenant: {TenantId}, PlanId: {PlanId}",
tenantId, tenantSubscription.Plan!.Id);
return new List<Guid>();
}
// Step 3: Collect all enabled feature IDs from modules
var featureIds = new List<Guid> { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
if (featureDetails.Modules?.Attendance?.Enabled == true)
{
featureIds.AddRange(featureDetails.Modules.Attendance.FeatureId);
_logger.LogDebug("Added Attendance module features for tenant: {TenantId}", tenantId);
}
if (featureDetails.Modules?.ProjectManagement?.Enabled == true)
{
featureIds.AddRange(featureDetails.Modules.ProjectManagement.FeatureId);
_logger.LogDebug("Added Project Management module features for tenant: {TenantId}", tenantId);
}
if (featureDetails.Modules?.Directory?.Enabled == true)
{
featureIds.AddRange(featureDetails.Modules.Directory.FeatureId);
_logger.LogDebug("Added Directory module features for tenant: {TenantId}", tenantId);
}
if (featureDetails.Modules?.Expense?.Enabled == true)
{
featureIds.AddRange(featureDetails.Modules.Expense.FeatureId);
_logger.LogDebug("Added Expense module features for tenant: {TenantId}", tenantId);
}
_logger.LogInfo("Returning {Count} feature IDs for tenant: {TenantId}", featureIds.Count, tenantId);
return featureIds.Distinct().ToList();
}
catch (Exception ex)
{
// Step 4: Handle unexpected errors
_logger.LogError(ex, "Error retrieving feature IDs for tenant: {TenantId}", tenantId);
return new List<Guid>();
}
}
} }
} }

View File

@ -4,6 +4,7 @@ using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Dtos.Tenant;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Expenses;
using Marco.Pms.Model.Master; using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.MongoDBModels;
@ -221,6 +222,8 @@ namespace Marco.Pms.Services.MappingProfiles
#region ======================================================= Master ======================================================= #region ======================================================= Master =======================================================
CreateMap<FeaturePermission, FeaturePermissionVM>();
#region ======================================================= Expenses Type Master ======================================================= #region ======================================================= Expenses Type Master =======================================================
CreateMap<ExpensesTypeMasterDto, ExpensesTypeMaster>() CreateMap<ExpensesTypeMasterDto, ExpensesTypeMaster>()