using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Shared; namespace Meezi.API.Controllers; [Authorize] [ApiController] public abstract class CafeApiControllerBase : ControllerBase { protected IActionResult? EnsureCafeAccess(string routeCafeId, ITenantContext tenant) { if (string.IsNullOrEmpty(tenant.CafeId) || tenant.CafeId != routeCafeId) return StatusCode(StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError("FORBIDDEN", "You do not have access to this cafe."))); return null; } protected IActionResult? EnsureOwner(ITenantContext tenant) { if (tenant.Role == EmployeeRole.Owner) return null; return StatusCode(StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError("OWNER_REQUIRED", "Only the cafe owner can perform this action."))); } /// Owner or Manager may act. protected IActionResult? EnsureManager(ITenantContext tenant) { if (tenant.Role is EmployeeRole.Owner or EmployeeRole.Manager) return null; return Forbidden("MANAGER_REQUIRED", "Manager access required."); } /// The employee acting on their own record, or a manager/owner. protected IActionResult? EnsureSelfOrManager(string employeeId, ITenantContext tenant) { if (tenant.UserId == employeeId) return null; return EnsureManager(tenant); } /// Gate by an explicit capability from the role→permission matrix. /// When the employee has a custom role its permission set is used instead. protected IActionResult? EnsurePermission(ITenantContext tenant, Permission permission) { if (tenant.CustomPermissions is { } custom) return custom.Contains(permission) ? null : Forbidden("FORBIDDEN", "You do not have permission to perform this action."); if (tenant.Role is { } role && RolePermissions.Has(role, permission)) return null; return Forbidden("FORBIDDEN", "You do not have permission to perform this action."); } /// /// Strict branch isolation at the controller boundary: a branch-scoped session /// may only touch its own branch. Café-wide sessions (Owner) and sessions with /// no active branch are unrestricted here (DB query filters back this up). /// protected IActionResult? EnsureBranchAccess(string? routeBranchId, ITenantContext tenant) { if (tenant.Role is { } role && RolePermissions.IsCafeWide(role)) return null; if (string.IsNullOrEmpty(tenant.BranchId)) return null; if (string.IsNullOrEmpty(routeBranchId) || routeBranchId == tenant.BranchId) return null; return Forbidden("BRANCH_FORBIDDEN", "You do not have access to this branch."); } private ObjectResult Forbidden(string code, string message) => StatusCode(StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError(code, message))); protected static ApiResponse ValidationError(FluentValidation.Results.ValidationResult validation) { var first = validation.Errors.First(); return new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)); } protected IActionResult NotFoundError(string message = "Resource not found.") => NotFound(new ApiResponse(false, null, new ApiError("NOT_FOUND", message))); }