diff --git a/src/Meezi.API/Controllers/AuditController.cs b/src/Meezi.API/Controllers/AuditController.cs index a7c1324..da05c02 100644 --- a/src/Meezi.API/Controllers/AuditController.cs +++ b/src/Meezi.API/Controllers/AuditController.cs @@ -42,7 +42,7 @@ public class AuditController : CafeApiControllerBase [FromQuery] int pageSize = 50) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsurePermission(tenant, Permission.ViewReports) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ViewAuditLog) is { } forbidden) return forbidden; if (page < 1) page = 1; if (pageSize < 1) pageSize = 50; diff --git a/src/Meezi.API/Controllers/BillingController.cs b/src/Meezi.API/Controllers/BillingController.cs index 936117e..ae77023 100644 --- a/src/Meezi.API/Controllers/BillingController.cs +++ b/src/Meezi.API/Controllers/BillingController.cs @@ -3,13 +3,14 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Billing; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; namespace Meezi.API.Controllers; [ApiController] -public class BillingController : ControllerBase +public class BillingController : CafeApiControllerBase { private readonly IBillingService _billing; private readonly IValidator _subscribeValidator; @@ -27,13 +28,9 @@ public class BillingController : ControllerBase ITenantContext tenant, CancellationToken ct) { + if (EnsurePermission(tenant, Permission.ManageBilling) is { } permDenied) return permDenied; if (string.IsNullOrEmpty(tenant.CafeId)) return Unauthorized(); - if (tenant.Role != Core.Enums.EmployeeRole.Owner) - { - return StatusCode(403, new ApiResponse(false, null, - new ApiError("OWNER_REQUIRED", "Only the cafe owner can manage subscription billing."))); - } var validation = await _subscribeValidator.ValidateAsync(request, ct); if (!validation.IsValid) @@ -108,11 +105,9 @@ public class BillingController : ControllerBase [HttpDelete("api/billing/queued/{paymentId}")] public async Task CancelQueued(string paymentId, ITenantContext tenant, CancellationToken ct) { + if (EnsurePermission(tenant, Permission.ManageBilling) is { } permDenied) return permDenied; if (string.IsNullOrEmpty(tenant.CafeId)) return Unauthorized(); - if (tenant.Role != Core.Enums.EmployeeRole.Owner) - return StatusCode(403, new ApiResponse(false, null, - new ApiError("OWNER_REQUIRED", "Only the cafe owner can manage subscription billing."))); var (ok, code, message) = await _billing.CancelQueuedAsync(tenant.CafeId, paymentId, ct); if (!ok) diff --git a/src/Meezi.API/Controllers/BranchMenuController.cs b/src/Meezi.API/Controllers/BranchMenuController.cs index 7a06c50..8a5a758 100644 --- a/src/Meezi.API/Controllers/BranchMenuController.cs +++ b/src/Meezi.API/Controllers/BranchMenuController.cs @@ -1,8 +1,8 @@ using FluentValidation; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Menu; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -43,7 +43,6 @@ public class BranchMenuController : CafeApiControllerBase } [HttpPut("{menuItemId}/override")] - [Authorize(Roles = "Manager,Owner")] public async Task UpsertOverride( string cafeId, string branchId, @@ -53,8 +52,7 @@ public class BranchMenuController : CafeApiControllerBase CancellationToken cancellationToken = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (!BranchMenuService.CanManageOverrides(tenant.Role)) - return Forbid(); + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var validation = await _upsertValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -84,7 +82,6 @@ public class BranchMenuController : CafeApiControllerBase } [HttpDelete("{menuItemId}/override")] - [Authorize(Roles = "Owner")] public async Task DeleteOverride( string cafeId, string branchId, @@ -93,6 +90,7 @@ public class BranchMenuController : CafeApiControllerBase CancellationToken cancellationToken = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var deleted = await _branchMenu.DeleteOverrideAsync( cafeId, branchId, menuItemId, cancellationToken); diff --git a/src/Meezi.API/Controllers/BranchPrintSettingsController.cs b/src/Meezi.API/Controllers/BranchPrintSettingsController.cs index 1025df1..484c945 100644 --- a/src/Meezi.API/Controllers/BranchPrintSettingsController.cs +++ b/src/Meezi.API/Controllers/BranchPrintSettingsController.cs @@ -1,7 +1,7 @@ using FluentValidation; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Printing; +using Meezi.Core.Authorization; using Meezi.Core.Entities; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; @@ -11,7 +11,6 @@ using Microsoft.EntityFrameworkCore; namespace Meezi.API.Controllers; [Route("api/cafes/{cafeId}/branches/{branchId}/print-settings")] -[Authorize(Roles = "Manager,Owner")] public class BranchPrintSettingsController : CafeApiControllerBase { private readonly AppDbContext _db; @@ -54,6 +53,7 @@ public class BranchPrintSettingsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManagePrintSettings) is { } permDenied) return permDenied; var validation = await _validator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); diff --git a/src/Meezi.API/Controllers/BranchTablesController.cs b/src/Meezi.API/Controllers/BranchTablesController.cs index 16d1583..f1e2dc9 100644 --- a/src/Meezi.API/Controllers/BranchTablesController.cs +++ b/src/Meezi.API/Controllers/BranchTablesController.cs @@ -1,8 +1,8 @@ using FluentValidation; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Tables; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -68,7 +68,6 @@ public class BranchTablesController : CafeApiControllerBase } [HttpPost] - [Authorize(Roles = "Manager,Owner")] public async Task CreateTable( string cafeId, string branchId, @@ -77,6 +76,7 @@ public class BranchTablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) return Forbid(); @@ -88,7 +88,6 @@ public class BranchTablesController : CafeApiControllerBase } [HttpPatch("{id}")] - [Authorize(Roles = "Manager,Owner")] public async Task PatchTable( string cafeId, string branchId, @@ -98,6 +97,7 @@ public class BranchTablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) return Forbid(); @@ -109,7 +109,6 @@ public class BranchTablesController : CafeApiControllerBase } [HttpDelete("{id}")] - [Authorize(Roles = "Manager,Owner")] public async Task DeleteTable( string cafeId, string branchId, @@ -118,6 +117,7 @@ public class BranchTablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) return Forbid(); @@ -135,6 +135,7 @@ public class BranchTablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) return Forbid(); @@ -180,7 +181,6 @@ public class BranchTablesController : CafeApiControllerBase } [HttpPost("sections")] - [Authorize(Roles = "Manager,Owner")] public async Task CreateSection( string cafeId, string branchId, @@ -189,6 +189,7 @@ public class BranchTablesController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) return Forbid(); @@ -200,7 +201,6 @@ public class BranchTablesController : CafeApiControllerBase } [HttpPatch("sections/{sectionId}")] - [Authorize(Roles = "Manager,Owner")] public async Task PatchSection( string cafeId, string branchId, @@ -210,6 +210,7 @@ public class BranchTablesController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) return Forbid(); @@ -221,7 +222,6 @@ public class BranchTablesController : CafeApiControllerBase } [HttpDelete("sections/{sectionId}")] - [Authorize(Roles = "Manager,Owner")] public async Task DeleteSection( string cafeId, string branchId, @@ -230,6 +230,7 @@ public class BranchTablesController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; if (!await _tables.CanAccessBranchAsync(cafeId, branchId, tenant.UserId, tenant.Role, ct)) return Forbid(); diff --git a/src/Meezi.API/Controllers/BranchesController.cs b/src/Meezi.API/Controllers/BranchesController.cs index b9588d5..e9d4a6b 100644 --- a/src/Meezi.API/Controllers/BranchesController.cs +++ b/src/Meezi.API/Controllers/BranchesController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Models.Branches; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Constants; using Meezi.Core.Entities; using Meezi.Core.Enums; @@ -96,7 +97,7 @@ public class BranchesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + if (EnsurePermission(tenant, Permission.CreateBranch) is { } permDenied) return permDenied; var validation = await _createValidator.ValidateAsync(request, ct); if (!validation.IsValid) @@ -169,7 +170,7 @@ public class BranchesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + if (EnsurePermission(tenant, Permission.EditBranch) is { } permDenied) return permDenied; var validation = await _patchValidator.ValidateAsync(request, ct); if (!validation.IsValid) @@ -222,7 +223,7 @@ public class BranchesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + if (EnsurePermission(tenant, Permission.DeleteBranch) is { } permDenied) return permDenied; var (ok, code, message) = await _lifecycle.ScheduleDeletionAsync(cafeId, branchId, ct); if (!ok) @@ -257,7 +258,7 @@ public class BranchesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + if (EnsurePermission(tenant, Permission.EditBranch) is { } permDenied) return permDenied; var (ok, code, message) = await _lifecycle.RestoreAsync(cafeId, branchId, ct); if (!ok) diff --git a/src/Meezi.API/Controllers/CafeDiscoverProfileController.cs b/src/Meezi.API/Controllers/CafeDiscoverProfileController.cs index c09e358..5420124 100644 --- a/src/Meezi.API/Controllers/CafeDiscoverProfileController.cs +++ b/src/Meezi.API/Controllers/CafeDiscoverProfileController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Discover; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; @@ -45,6 +46,7 @@ public class CafeDiscoverProfileController : CafeApiControllerBase { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageDiscoverProfile) is { } permDenied) return permDenied; var planTier = tenant.PlanTier ?? PlanTier.Free; if (!await catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, "discover_profile", ct)) diff --git a/src/Meezi.API/Controllers/CafePublicProfileController.cs b/src/Meezi.API/Controllers/CafePublicProfileController.cs index 7b34e40..e447635 100644 --- a/src/Meezi.API/Controllers/CafePublicProfileController.cs +++ b/src/Meezi.API/Controllers/CafePublicProfileController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Public; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Discover; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; @@ -71,6 +72,7 @@ public class CafePublicProfileController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageDiscoverProfile) is { } permDenied) return permDenied; var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, ct); if (cafe is null) @@ -121,6 +123,7 @@ public class CafePublicProfileController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageDiscoverProfile) is { } permDenied) return permDenied; if (photo is null || photo.Length == 0) return BadRequest(Fail("NO_FILE", "No photo provided.")); @@ -155,6 +158,7 @@ public class CafePublicProfileController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageDiscoverProfile) is { } permDenied) return permDenied; if (string.IsNullOrWhiteSpace(url)) return BadRequest(Fail("NO_URL", "Provide ?url= of the photo to remove.")); diff --git a/src/Meezi.API/Controllers/CafeReviewsController.cs b/src/Meezi.API/Controllers/CafeReviewsController.cs index c0e21db..8471408 100644 --- a/src/Meezi.API/Controllers/CafeReviewsController.cs +++ b/src/Meezi.API/Controllers/CafeReviewsController.cs @@ -2,6 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Public; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Services.Platform; @@ -48,6 +49,7 @@ public class CafeReviewsController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageReviews) is { } permDenied) return permDenied; // Replying to reviews is a paid feature (Starter+). var tier = tenant.PlanTier ?? PlanTier.Free; @@ -76,6 +78,7 @@ public class CafeReviewsController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageReviews) is { } permDenied) return permDenied; var data = await _reviews.SetHiddenAsync(cafeId, reviewId, request.IsHidden, ct); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); diff --git a/src/Meezi.API/Controllers/CafeSettingsController.cs b/src/Meezi.API/Controllers/CafeSettingsController.cs index 211253a..d1a0336 100644 --- a/src/Meezi.API/Controllers/CafeSettingsController.cs +++ b/src/Meezi.API/Controllers/CafeSettingsController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Models.Cafes; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Core.Utilities; @@ -47,6 +48,7 @@ public class CafeSettingsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageCafeSettings) is { } permDenied) return permDenied; var validation = await _validator.ValidateAsync(request, ct); if (!validation.IsValid) diff --git a/src/Meezi.API/Controllers/CouponsController.cs b/src/Meezi.API/Controllers/CouponsController.cs index 2673c3d..90c0197 100644 --- a/src/Meezi.API/Controllers/CouponsController.cs +++ b/src/Meezi.API/Controllers/CouponsController.cs @@ -2,6 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Crm; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -62,6 +63,7 @@ public class CouponsController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.CreateCoupon) is { } permDenied) return permDenied; var validation = await _createValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -82,6 +84,7 @@ public class CouponsController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditCoupon) is { } permDenied) return permDenied; var data = await _couponService.UpdateAsync(cafeId, id, request, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); @@ -95,6 +98,7 @@ public class CouponsController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.DeleteCoupon) is { } permDenied) return permDenied; var deleted = await _couponService.DeleteAsync(cafeId, id, cancellationToken); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); diff --git a/src/Meezi.API/Controllers/CustomRolesController.cs b/src/Meezi.API/Controllers/CustomRolesController.cs index b21f9ba..5e3a563 100644 --- a/src/Meezi.API/Controllers/CustomRolesController.cs +++ b/src/Meezi.API/Controllers/CustomRolesController.cs @@ -23,7 +23,7 @@ public class CustomRolesController : CafeApiControllerBase public async Task List(string cafeId, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageRoles) is { } forbidden) return forbidden; var roles = await _db.CustomRoles .AsNoTracking() @@ -57,7 +57,7 @@ public class CustomRolesController : CafeApiControllerBase public async Task Get(string cafeId, string id, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageRoles) is { } forbidden) return forbidden; var r = await _db.CustomRoles.AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id && x.CafeId == cafeId, ct); @@ -80,7 +80,7 @@ public class CustomRolesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageRoles) is { } forbidden) return forbidden; var name = request.Name?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(name)) @@ -113,7 +113,7 @@ public class CustomRolesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageRoles) is { } forbidden) return forbidden; var role = await _db.CustomRoles .FirstOrDefaultAsync(r => r.Id == id && r.CafeId == cafeId, ct); @@ -152,7 +152,7 @@ public class CustomRolesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageRoles) is { } forbidden) return forbidden; var role = await _db.CustomRoles .FirstOrDefaultAsync(r => r.Id == id && r.CafeId == cafeId, ct); @@ -180,7 +180,7 @@ public class CustomRolesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageRoles) is { } forbidden) return forbidden; var employee = await _db.Employees .FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId && e.DeletedAt == null, ct); diff --git a/src/Meezi.API/Controllers/CustomersController.cs b/src/Meezi.API/Controllers/CustomersController.cs index 10b64fa..6055394 100644 --- a/src/Meezi.API/Controllers/CustomersController.cs +++ b/src/Meezi.API/Controllers/CustomersController.cs @@ -2,6 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Crm; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -57,6 +58,7 @@ public class CustomersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.CreateCustomer) is { } permDenied) return permDenied; var validation = await _createValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -77,6 +79,7 @@ public class CustomersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditCustomer) is { } permDenied) return permDenied; var validation = await _updateValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -99,6 +102,7 @@ public class CustomersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.DeleteCustomer) is { } permDenied) return permDenied; var deleted = await _customerService.DeleteAsync(cafeId, id, cancellationToken); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); diff --git a/src/Meezi.API/Controllers/DeliveryReportsController.cs b/src/Meezi.API/Controllers/DeliveryReportsController.cs index f858b21..24058c5 100644 --- a/src/Meezi.API/Controllers/DeliveryReportsController.cs +++ b/src/Meezi.API/Controllers/DeliveryReportsController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Meezi.API.Services.Delivery; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -21,6 +22,7 @@ public class DeliveryReportsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewReports) is { } permDenied) return permDenied; var utcTo = to ?? DateTime.UtcNow; var utcFrom = from ?? utcTo.AddDays(-30); diff --git a/src/Meezi.API/Controllers/ExpensesController.cs b/src/Meezi.API/Controllers/ExpensesController.cs index fe90430..4852c72 100644 --- a/src/Meezi.API/Controllers/ExpensesController.cs +++ b/src/Meezi.API/Controllers/ExpensesController.cs @@ -2,7 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Expenses; using Meezi.API.Services; -using Meezi.Core.Enums; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -30,14 +30,11 @@ public class ExpensesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.CreateExpense) is { } permDenied) return permDenied; if (string.IsNullOrEmpty(tenant.UserId)) return StatusCode(StatusCodes.Status401Unauthorized, new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "User context is missing."))); - if (!CanLogExpense(tenant.Role)) - return StatusCode(StatusCodes.Status403Forbidden, - new ApiResponse(false, null, new ApiError("FORBIDDEN", "You cannot log expenses."))); - var validation = await _createValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -85,10 +82,7 @@ public class ExpensesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - - if (!CanDeleteExpense(tenant.Role)) - return StatusCode(StatusCodes.Status403Forbidden, - new ApiResponse(false, null, new ApiError("FORBIDDEN", "Only managers can delete expenses."))); + if (EnsurePermission(tenant, Permission.DeleteExpense) is { } permDenied) return permDenied; var result = await _expenses.DeleteExpenseAsync(cafeId, id, ct); if (!result.Success) @@ -104,12 +98,6 @@ public class ExpensesController : CafeApiControllerBase return Ok(new ApiResponse(true, null)); } - private static bool CanLogExpense(EmployeeRole? role) => - role is EmployeeRole.Owner or EmployeeRole.Manager or EmployeeRole.Cashier; - - private static bool CanDeleteExpense(EmployeeRole? role) => - role is EmployeeRole.Owner or EmployeeRole.Manager; - private IActionResult ExpenseResult(ExpenseServiceResult result, int successStatus = StatusCodes.Status200OK) { if (result.Success) diff --git a/src/Meezi.API/Controllers/HrController.cs b/src/Meezi.API/Controllers/HrController.cs index 1b4cdc1..dc61591 100644 --- a/src/Meezi.API/Controllers/HrController.cs +++ b/src/Meezi.API/Controllers/HrController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Models.Hr; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Entities; using Meezi.Core.Enums; using Meezi.Core.Interfaces; @@ -57,7 +58,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.CreateStaff) is { } forbidden) return forbidden; IActionResult Invalid(string message, string field) => BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", message, field))); @@ -204,7 +205,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageSchedules) is { } forbidden) return forbidden; var data = await _hr.UpsertShiftsAsync(cafeId, employeeId, request, ct); return Ok(new ApiResponse>(true, data)); } @@ -217,6 +218,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ReviewLeave) is { } forbidden) return forbidden; var data = await _hr.GetLeaveRequestsAsync(cafeId, status, ct); return Ok(new ApiResponse>(true, data)); } @@ -248,7 +250,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ReviewLeave) is { } forbidden) return forbidden; var validation = await _reviewValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -265,6 +267,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewSalaries) is { } forbidden) return forbidden; var data = await _hr.GetSalariesAsync(cafeId, monthYear, ct); return Ok(new ApiResponse>(true, data)); } @@ -277,7 +280,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageSalaries) is { } forbidden) return forbidden; var validation = await _salaryValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -290,7 +293,7 @@ public class HrController : CafeApiControllerBase public async Task MarkPaid(string cafeId, string salaryId, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageSalaries) is { } forbidden) return forbidden; var data = await _hr.MarkSalaryPaidAsync(cafeId, salaryId, ct); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); @@ -306,7 +309,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageStaffCredentials) is { } forbidden) return forbidden; var username = request.Username.Trim().ToLowerInvariant(); @@ -344,7 +347,7 @@ public class HrController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageStaffCredentials) is { } forbidden) return forbidden; var employee = await _db.Employees .FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId && e.DeletedAt == null, ct); diff --git a/src/Meezi.API/Controllers/InventoryController.cs b/src/Meezi.API/Controllers/InventoryController.cs index 4187de8..2a471bb 100644 --- a/src/Meezi.API/Controllers/InventoryController.cs +++ b/src/Meezi.API/Controllers/InventoryController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -36,6 +37,7 @@ public class InventoryController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.CreateInventory) is { } permDenied) return permDenied; if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Name is required."))); @@ -56,6 +58,7 @@ public class InventoryController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditInventory) is { } permDenied) return permDenied; var updated = await _inventory.UpdateAsync(cafeId, ingredientId, request, ct); if (updated is null) return NotFoundError(); return Ok(new ApiResponse(true, updated)); @@ -69,6 +72,7 @@ public class InventoryController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.DeleteInventory) is { } permDenied) return permDenied; var deleted = await _inventory.DeleteAsync(cafeId, ingredientId, ct); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id = ingredientId })); @@ -83,6 +87,7 @@ public class InventoryController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditInventory) is { } permDenied) return permDenied; try { var updated = await _inventory.AdjustAsync(cafeId, ingredientId, request, tenant.UserId, ct); @@ -146,6 +151,7 @@ public class InventoryController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditInventory) is { } permDenied) return permDenied; var recipe = await _inventory.SetRecipeAsync(cafeId, menuItemId, request, ct); if (recipe is null) return NotFoundError("Menu item not found."); return Ok(new ApiResponse(true, recipe)); diff --git a/src/Meezi.API/Controllers/KitchenStationsController.cs b/src/Meezi.API/Controllers/KitchenStationsController.cs index 5339803..0e8b654 100644 --- a/src/Meezi.API/Controllers/KitchenStationsController.cs +++ b/src/Meezi.API/Controllers/KitchenStationsController.cs @@ -2,6 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Kitchen; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -40,6 +41,7 @@ public class KitchenStationsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageKitchenStations) is { } permDenied) return permDenied; var validation = await _createValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -59,6 +61,7 @@ public class KitchenStationsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageKitchenStations) is { } permDenied) return permDenied; var validation = await _updateValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -71,6 +74,7 @@ public class KitchenStationsController : CafeApiControllerBase public async Task Delete(string cafeId, string id, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageKitchenStations) is { } permDenied) return permDenied; var ok = await _stations.DeleteAsync(cafeId, id, ct); if (!ok) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); diff --git a/src/Meezi.API/Controllers/MediaController.cs b/src/Meezi.API/Controllers/MediaController.cs index ec709c0..dfca159 100644 --- a/src/Meezi.API/Controllers/MediaController.cs +++ b/src/Meezi.API/Controllers/MediaController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; @@ -20,13 +21,21 @@ public class MediaController : CafeApiControllerBase [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadMenuImage( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) - => Upload(cafeId, file, tenant, _media.SaveMenuImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return Task.FromResult(permDenied); + return Upload(cafeId, file, tenant, _media.SaveMenuImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + } [HttpPost("menu-video")] [RequestSizeLimit(25 * 1024 * 1024)] public Task UploadMenuVideo( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) - => Upload(cafeId, file, tenant, _media.SaveMenuVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return Task.FromResult(permDenied); + return Upload(cafeId, file, tenant, _media.SaveMenuVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); + } [HttpPost("menu-model3d")] [RequestSizeLimit(8 * 1024 * 1024)] @@ -38,6 +47,7 @@ public class MediaController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var planTier = tenant.PlanTier ?? PlanTier.Free; if (!await catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, "menu_3d", cancellationToken)) { @@ -63,25 +73,41 @@ public class MediaController : CafeApiControllerBase [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadTableImage( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) - => Upload(cafeId, file, tenant, _media.SaveTableImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return Task.FromResult(permDenied); + return Upload(cafeId, file, tenant, _media.SaveTableImageAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + } [HttpPost("table-video")] [RequestSizeLimit(25 * 1024 * 1024)] public Task UploadTableVideo( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) - => Upload(cafeId, file, tenant, _media.SaveTableVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return Task.FromResult(permDenied); + return Upload(cafeId, file, tenant, _media.SaveTableVideoAsync, "INVALID_FILE", "Use MP4/WebM/MOV up to 25MB.", cancellationToken); + } [HttpPost("cafe-logo")] [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadCafeLogo( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) - => Upload(cafeId, file, tenant, _media.SaveCafeLogoAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); + if (EnsurePermission(tenant, Permission.ManageCafeSettings) is { } permDenied) return Task.FromResult(permDenied); + return Upload(cafeId, file, tenant, _media.SaveCafeLogoAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + } [HttpPost("cafe-cover")] [RequestSizeLimit(5 * 1024 * 1024)] public Task UploadCafeCover( string cafeId, IFormFile file, ITenantContext tenant, CancellationToken cancellationToken) - => Upload(cafeId, file, tenant, _media.SaveCafeCoverAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + { + if (EnsureCafeAccess(cafeId, tenant) is { } denied) return Task.FromResult(denied); + if (EnsurePermission(tenant, Permission.ManageCafeSettings) is { } permDenied) return Task.FromResult(permDenied); + return Upload(cafeId, file, tenant, _media.SaveCafeCoverAsync, "INVALID_FILE", "Use JPEG/PNG/WebP up to 5MB.", cancellationToken); + } /// Media library for this café — previously uploaded files so the UI can /// reuse one instead of re-uploading. Deduplication means each distinct file appears once. diff --git a/src/Meezi.API/Controllers/MenuController.cs b/src/Meezi.API/Controllers/MenuController.cs index 62b8430..75fc821 100644 --- a/src/Meezi.API/Controllers/MenuController.cs +++ b/src/Meezi.API/Controllers/MenuController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Models.Menu; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Constants; using Meezi.Core.Enums; using Meezi.Core.Interfaces; @@ -59,6 +60,7 @@ public class MenuController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.CreateMenuItem) is { } permDenied) return permDenied; var validation = await _createCategoryValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -86,6 +88,7 @@ public class MenuController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var data = await _menuService.UpdateCategoryAsync(cafeId, id, request, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); @@ -95,6 +98,7 @@ public class MenuController : CafeApiControllerBase public async Task DeleteCategory(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.DeleteMenuItem) is { } permDenied) return permDenied; var deleted = await _menuService.DeleteCategoryAsync(cafeId, id, cancellationToken); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); @@ -120,6 +124,7 @@ public class MenuController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.CreateMenuItem) is { } permDenied) return permDenied; var validation = await _createItemValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -148,6 +153,7 @@ public class MenuController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var data = await _menuService.UpdateItemAsync(cafeId, id, request, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); @@ -162,6 +168,7 @@ public class MenuController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var data = await _menuService.SetAvailabilityAsync(cafeId, id, request.IsAvailable, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); @@ -171,6 +178,7 @@ public class MenuController : CafeApiControllerBase public async Task DeleteItem(string cafeId, string id, ITenantContext tenant, CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.DeleteMenuItem) is { } permDenied) return permDenied; var deleted = await _menuService.DeleteItemAsync(cafeId, id, cancellationToken); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); @@ -193,6 +201,7 @@ public class MenuController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditMenuItem) is { } permDenied) return permDenied; var tier = tenant.PlanTier ?? PlanTier.Free; var (data, code, message) = await _menuAi3d.GenerateFromItemImageAsync(cafeId, id, tier, cancellationToken); if (code is not null) diff --git a/src/Meezi.API/Controllers/OrdersController.cs b/src/Meezi.API/Controllers/OrdersController.cs index d1994ce..dc642a3 100644 --- a/src/Meezi.API/Controllers/OrdersController.cs +++ b/src/Meezi.API/Controllers/OrdersController.cs @@ -1,5 +1,4 @@ using FluentValidation; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Orders; using Meezi.API.Services; @@ -120,6 +119,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ProcessOrders) is { } permDenied) return permDenied; var validation = await _createValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -139,6 +139,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditOrder) is { } permDenied) return permDenied; var validation = await _appendValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -150,7 +151,6 @@ public class OrdersController : CafeApiControllerBase } [HttpPatch("{id}/items/{itemId}/void")] - [Authorize(Roles = "Manager,Owner")] public async Task VoidOrderItem( string cafeId, string id, @@ -159,6 +159,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.VoidOrder) is { } permDenied) return permDenied; if (string.IsNullOrEmpty(tenant.UserId)) return StatusCode(StatusCodes.Status403Forbidden, new ApiResponse(false, null, new ApiError("FORBIDDEN", "User context required."))); @@ -181,7 +182,6 @@ public class OrdersController : CafeApiControllerBase } [HttpPost("{id}/transfer")] - [Authorize(Roles = "Manager,Owner,Waiter")] public async Task TransferTable( string cafeId, string id, @@ -190,6 +190,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditOrder) is { } permDenied) return permDenied; var result = await _orderService.TransferTableAsync(cafeId, id, request.TargetTableId, cancellationToken); if (!result.Success) @@ -207,6 +208,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditOrder) is { } permDenied) return permDenied; var validation = await _sessionValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -226,6 +228,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.UpdateOrderStatus) is { } permDenied) return permDenied; var validation = await _statusValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -243,7 +246,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsurePermission(tenant, Permission.ProcessOrders) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.VoidOrder) is { } forbidden) return forbidden; var result = await _orderService.CancelOrderAsync( cafeId, id, request.Reason, tenant.UserId, cancellationToken); @@ -279,6 +282,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.HandlePayments) is { } permDenied) return permDenied; var validation = await _paymentsValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -319,7 +323,7 @@ public class OrdersController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageFinancials) is { } forbidden) return forbidden; var validation = await _correctionValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); diff --git a/src/Meezi.API/Controllers/PosDeviceController.cs b/src/Meezi.API/Controllers/PosDeviceController.cs index ae86f6d..e7dd023 100644 --- a/src/Meezi.API/Controllers/PosDeviceController.cs +++ b/src/Meezi.API/Controllers/PosDeviceController.cs @@ -1,15 +1,14 @@ using FluentValidation; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Printing; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; namespace Meezi.API.Controllers; [Route("api/cafes/{cafeId}/branches/{branchId}/pos-device")] -[Authorize(Roles = "Cashier,Manager,Owner")] public class PosDeviceController : CafeApiControllerBase { private readonly IPosDeviceService _posDevice; @@ -30,6 +29,7 @@ public class PosDeviceController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.HandlePayments) is { } permDenied) return permDenied; var validation = await _validator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); diff --git a/src/Meezi.API/Controllers/PrintController.cs b/src/Meezi.API/Controllers/PrintController.cs index 9c8742c..2f51977 100644 --- a/src/Meezi.API/Controllers/PrintController.cs +++ b/src/Meezi.API/Controllers/PrintController.cs @@ -1,7 +1,7 @@ -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Printing; using Meezi.API.Services.Printing; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -41,7 +41,6 @@ public class PrintController : CafeApiControllerBase } [HttpPost("test")] - [Authorize(Roles = "Manager,Owner")] public async Task TestPrint( string cafeId, [FromBody] TestPrintRequest request, @@ -49,6 +48,7 @@ public class PrintController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManagePrintSettings) is { } permDenied) return permDenied; var result = await _printer.TestPrintAsync(request.PrinterIp, request.Port, ct); return ToActionResult(result); diff --git a/src/Meezi.API/Controllers/QueueController.cs b/src/Meezi.API/Controllers/QueueController.cs index a8bde01..b623566 100644 --- a/src/Meezi.API/Controllers/QueueController.cs +++ b/src/Meezi.API/Controllers/QueueController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Queue; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -37,6 +38,7 @@ public class QueueController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageQueue) is { } permDenied) return permDenied; var (ticket, error) = await _queue.IssueNextAsync(cafeId, tenant.UserId, request, ct); if (error == "BRANCH_NOT_FOUND") return NotFound(new ApiResponse(false, null, new ApiError(error, "Branch not found."))); @@ -54,6 +56,7 @@ public class QueueController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageQueue) is { } permDenied) return permDenied; var (ticket, error) = await _queue.UpdateStatusAsync(cafeId, ticketId, request.Status, ct); if (error == "NOT_FOUND") return NotFound(new ApiResponse(false, null, new ApiError(error, "Ticket not found."))); @@ -71,6 +74,7 @@ public class QueueController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageQueue) is { } permDenied) return permDenied; var board = await _queue.GetTodayBoardAsync(cafeId, branchId, ct); var next = board.Tickets.FirstOrDefault(t => t.Status == QueueTicketStatus.Waiting); if (next is null) diff --git a/src/Meezi.API/Controllers/ReportsController.cs b/src/Meezi.API/Controllers/ReportsController.cs index 957f48b..aad74fd 100644 --- a/src/Meezi.API/Controllers/ReportsController.cs +++ b/src/Meezi.API/Controllers/ReportsController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Reports; using Meezi.API.Services; using Meezi.API.Utils; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Services.Platform; @@ -38,6 +39,7 @@ public class ReportsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewReports) is { } permDenied) return permDenied; if (string.IsNullOrWhiteSpace(branchId)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "branchId is required.", "branchId"))); @@ -65,6 +67,7 @@ public class ReportsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewReports) is { } permDenied) return permDenied; if (!TryParseReportDate(from, out var startDate) || !TryParseReportDate(to, out var endDate)) return BadRequest(new ApiResponse(false, null, @@ -99,6 +102,7 @@ public class ReportsController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewReports) is { } permDenied) return permDenied; var maxDays = await MaxHistoryDaysAsync(tenant, ct); if (days > maxDays && maxDays != int.MaxValue) @@ -120,6 +124,7 @@ public class ReportsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewReports) is { } permDenied) return permDenied; if (date is not null && !JalaliCalendarHelper.TryParseJalaliDate(date, out _, out _, out _)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Invalid Jalali date. Use yyyy-MM-dd."))); @@ -136,6 +141,7 @@ public class ReportsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewReports) is { } permDenied) return permDenied; if (month is not null && !JalaliCalendarHelper.TryParseJalaliMonth(month, out _, out _)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Invalid Jalali month. Use yyyy-MM."))); @@ -152,6 +158,7 @@ public class ReportsController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ViewReports) is { } permDenied) return permDenied; var data = await _reports.GetTrendAsync(cafeId, days, ct); return Ok(new ApiResponse>(true, data)); } @@ -165,6 +172,7 @@ public class ReportsController : CafeApiControllerBase CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ExportReports) is { } permDenied) return permDenied; if (!string.Equals(format, "excel", StringComparison.OrdinalIgnoreCase)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Only excel format is supported."))); diff --git a/src/Meezi.API/Controllers/ReservationsController.cs b/src/Meezi.API/Controllers/ReservationsController.cs index 8b33fa3..dcdc8cc 100644 --- a/src/Meezi.API/Controllers/ReservationsController.cs +++ b/src/Meezi.API/Controllers/ReservationsController.cs @@ -2,6 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Public; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -30,6 +31,7 @@ public class ReservationsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.CreateReservation) is { } permDenied) return permDenied; var validation = await _createValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -62,6 +64,7 @@ public class ReservationsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.EditReservation) is { } permDenied) return permDenied; var data = await _reservations.UpdateStatusAsync(cafeId, id, request.Status, ct); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); @@ -75,6 +78,7 @@ public class ReservationsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.DeleteReservation) is { } permDenied) return permDenied; var deleted = await _reservations.DeleteAsync(cafeId, id, ct); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); diff --git a/src/Meezi.API/Controllers/ShiftsController.cs b/src/Meezi.API/Controllers/ShiftsController.cs index 420568c..31ea206 100644 --- a/src/Meezi.API/Controllers/ShiftsController.cs +++ b/src/Meezi.API/Controllers/ShiftsController.cs @@ -2,6 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Shifts; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -33,6 +34,7 @@ public class ShiftsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.OperateRegister) is { } permDenied) return permDenied; if (string.IsNullOrEmpty(tenant.UserId)) return StatusCode(StatusCodes.Status401Unauthorized, new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "User context is missing."))); @@ -54,6 +56,7 @@ public class ShiftsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.OperateRegister) is { } permDenied) return permDenied; if (string.IsNullOrEmpty(tenant.UserId)) return StatusCode(StatusCodes.Status401Unauthorized, new ApiResponse(false, null, new ApiError("UNAUTHORIZED", "User context is missing."))); diff --git a/src/Meezi.API/Controllers/SmsController.cs b/src/Meezi.API/Controllers/SmsController.cs index a765bb3..f5efe30 100644 --- a/src/Meezi.API/Controllers/SmsController.cs +++ b/src/Meezi.API/Controllers/SmsController.cs @@ -2,6 +2,7 @@ using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Crm; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -43,7 +44,7 @@ public class SmsController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureManager(tenant) is { } forbidden) return forbidden; + if (EnsurePermission(tenant, Permission.ManageSmsSettings) is { } permDenied) return permDenied; var (success, data, code, message) = await _smsMarketingService.UpdateSettingsAsync( cafeId, request, cancellationToken); @@ -85,6 +86,7 @@ public class SmsController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.SendSms) is { } permDenied) return permDenied; var validation = await _campaignValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); diff --git a/src/Meezi.API/Controllers/TablesController.cs b/src/Meezi.API/Controllers/TablesController.cs index 3b7c2d2..da1e5ae 100644 --- a/src/Meezi.API/Controllers/TablesController.cs +++ b/src/Meezi.API/Controllers/TablesController.cs @@ -1,9 +1,9 @@ using FluentValidation; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Orders; using Meezi.API.Models.Tables; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -65,6 +65,7 @@ public class TablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; var validation = await _createValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -82,6 +83,7 @@ public class TablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; var validation = await _patchValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); @@ -104,7 +106,6 @@ public class TablesController : CafeApiControllerBase } [HttpDelete("{id}")] - [Authorize(Roles = "Manager,Owner")] public async Task DeleteTable( string cafeId, string id, @@ -112,6 +113,7 @@ public class TablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; var result = await _tableService.DeleteTableAsync(cafeId, id, ct); if (!result.Success) @@ -135,6 +137,7 @@ public class TablesController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageTables) is { } permDenied) return permDenied; var validation = await _cleaningValidator.ValidateAsync(request, ct); if (!validation.IsValid) return BadRequest(ValidationError(validation)); diff --git a/src/Meezi.API/Controllers/TarazController.cs b/src/Meezi.API/Controllers/TarazController.cs index 5897513..4d91c79 100644 --- a/src/Meezi.API/Controllers/TarazController.cs +++ b/src/Meezi.API/Controllers/TarazController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -19,6 +20,7 @@ public class TarazController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageFinancials) is { } permDenied) return permDenied; var targetDate = date ?? DateTime.UtcNow.Date; var result = await _taraz.SubmitDailyInvoicesAsync(cafeId, targetDate, ct); diff --git a/src/Meezi.API/Controllers/TaxesController.cs b/src/Meezi.API/Controllers/TaxesController.cs index 5bdad99..7d257f5 100644 --- a/src/Meezi.API/Controllers/TaxesController.cs +++ b/src/Meezi.API/Controllers/TaxesController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Taxes; using Meezi.API.Services; +using Meezi.Core.Authorization; using Meezi.Core.Interfaces; using Meezi.Shared; @@ -29,7 +30,7 @@ public class TaxesController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + if (EnsurePermission(tenant, Permission.CreateTax) is { } permDenied) return permDenied; var data = await _taxService.CreateAsync(cafeId, request, cancellationToken); return Ok(new ApiResponse(true, data)); } @@ -43,7 +44,7 @@ public class TaxesController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + if (EnsurePermission(tenant, Permission.EditTax) is { } permDenied) return permDenied; var data = await _taxService.UpdateAsync(cafeId, id, request, cancellationToken); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); @@ -57,7 +58,7 @@ public class TaxesController : CafeApiControllerBase CancellationToken cancellationToken) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - if (EnsureOwner(tenant) is { } ownerDenied) return ownerDenied; + if (EnsurePermission(tenant, Permission.DeleteTax) is { } permDenied) return permDenied; var deleted = await _taxService.DeleteAsync(cafeId, id, cancellationToken); if (!deleted) return NotFoundError(); return Ok(new ApiResponse(true, new { id })); diff --git a/src/Meezi.API/Controllers/TerminalsController.cs b/src/Meezi.API/Controllers/TerminalsController.cs index f42b042..f950052 100644 --- a/src/Meezi.API/Controllers/TerminalsController.cs +++ b/src/Meezi.API/Controllers/TerminalsController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.API.Services; @@ -52,6 +53,7 @@ public class TerminalsController : CafeApiControllerBase CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; + if (EnsurePermission(tenant, Permission.ManageCafeSettings) is { } permDenied) return permDenied; await _terminals.RevokeAsync(cafeId, terminalId, ct); return Ok(new ApiResponse(true, new { revoked = true })); }