using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
namespace Marco.Pms.Services.Service
{
public class PermissionServices
{
private readonly ApplicationDbContext _context;
private readonly RolesHelper _rolesHelper;
private readonly CacheUpdateHelper _cache;
private readonly ILoggingService _logger;
private readonly Guid tenantId;
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, ILoggingService logger, UserHelper userHelper)
{
_context = context;
_rolesHelper = rolesHelper;
_cache = cache;
_logger = logger;
tenantId = userHelper.GetTenantId();
}
///
/// Checks whether an employee has a specific feature permission, optionally within a project context.
///
/// The target feature permission ID to check.
/// The ID of the employee.
/// Optional project ID for project-scoped permissions.
/// True if the user has the permission, otherwise false.
public async Task HasPermission(Guid featurePermissionId, Guid employeeId, Guid? projectId = null)
{
// 1. Try fetching permissions from cache (fast-path lookup).
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.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);
}
public async Task HasPermissionAny(List featurePermissionIds, Guid employeeId)
{
var allFeaturePermissionIds = await _cache.GetPermissions(employeeId, tenantId);
if (allFeaturePermissionIds == null)
{
List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId, tenantId);
allFeaturePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
}
var hasPermission = featurePermissionIds.Any(f => allFeaturePermissionIds.Contains(f));
return hasPermission;
}
public async Task HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
{
var employeeId = LoggedInEmployee.Id;
var projectIds = await _cache.GetProjects(employeeId, tenantId);
if (projectIds == null)
{
var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId);
if (hasPermission)
{
var projects = await _context.Projects.AsNoTracking().Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync();
projectIds = projects.Select(p => p.Id).ToList();
}
else
{
var allocation = await _context.ProjectAllocations.AsNoTracking().Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync();
if (!allocation.Any())
{
return false;
}
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
}
await _cache.AddProjects(LoggedInEmployee.Id, projectIds, tenantId);
}
return projectIds.Contains(projectId);
}
///
/// Determines if an employee has permission to access a specific service project.
/// Permission is granted if the user is directly allocated to the project OR
/// assigned to any active job ticket within the project.
///
/// The ID of the user requesting access.
/// The ID of the project to access.
/// True if access is allowed, otherwise False.
public async Task HasServiceProjectPermission(Guid loggedInEmployeeId, Guid projectId)
{
Guid ReviewDoneStatus = Guid.Parse("ed10ab57-dbaa-4ca5-8ecd-56745dcbdbd7");
Guid ClosedStatus = Guid.Parse("3ddeefb5-ae3c-4e10-a922-35e0a452bb69");
// 1. Input Validation
if (loggedInEmployeeId == Guid.Empty || projectId == Guid.Empty)
{
_logger.LogWarning("Permission check failed: Invalid input parameters. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployeeId, projectId);
return false;
}
try
{
_logger.LogInfo("Starting permission check for Employee: {EmployeeId} on Project: {ProjectId}", loggedInEmployeeId, projectId);
// 2. Check Level 1: Is the user a generic Team Member of the project?
// This is usually the most common case, so checking this first saves complex query execution.
bool isTeamMember = await _context.ServiceProjectAllocations
.AsNoTracking() // Optimization: Read-only query does not need tracking
.AnyAsync(spa => spa.ProjectId == projectId
&& spa.EmployeeId == loggedInEmployeeId
&& spa.IsActive
&& spa.TenantId == tenantId);
if (isTeamMember)
{
_logger.LogInfo("Access Granted: User {EmployeeId} is a team member of Project {ProjectId}.", loggedInEmployeeId, projectId);
return true;
}
// 3. Check Level 2: Is the user assigned to any ACTIVE specific Job Ticket?
// Optimization: Combined the check for JobTicket and Mapping into a single Join query.
// This prevents pulling a list of JobIds into memory (fixing memory bloat) and reduces DB roundtrips.
bool hasActiveJobAssignment = await _context.JobTickets
.AsNoTracking()
.Where(jt => jt.ProjectId == projectId
&& jt.StatusId != ReviewDoneStatus
&& jt.StatusId != ClosedStatus
&& jt.IsActive)
.Join(_context.JobEmployeeMappings,
ticket => ticket.Id,
mapping => mapping.JobTicketId,
(ticket, mapping) => mapping)
.AnyAsync(mapping => mapping.AssigneeId == loggedInEmployeeId
&& mapping.TenantId == tenantId);
if (hasActiveJobAssignment)
{
_logger.LogInfo("Access Granted: User {EmployeeId} is assigned active tickets in Project {ProjectId}.", loggedInEmployeeId, projectId);
return true;
}
// 4. Default Deny
_logger.LogWarning("Access Denied: User {EmployeeId} has no permissions for Project {ProjectId}.", loggedInEmployeeId, projectId);
return false;
}
catch (Exception ex)
{
// 5. Robust Error Handling
// Log the full stack trace for debugging, but return false to maintain security (fail-closed).
_logger.LogError(ex, "An error occurred while checking permissions for Employee: {EmployeeId} on Project: {ProjectId}", loggedInEmployeeId, projectId);
return false;
}
}
}
}