feat(rbac): enforce permissions on every café write endpoint
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m9s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Has been cancelled
CI/CD / Deploy · all services (push) Has been cancelled

Closes the gap where the custom-role matrix was defined but unenforced — most
write endpoints only checked café membership, so the API would accept writes a
role's UI hid. Adds EnsurePermission(...) to all mutating/sensitive endpoints
across 32 controllers, mapped to the granular catalog:

- menu/inventory/coupons/customers/expenses/reservations/taxes/branches → CRUD perms
- tables/queue/kitchen-stations/print-settings → manage perms
- orders → ProcessOrders / EditOrder / VoidOrder / UpdateOrderStatus / HandlePayments,
  payment corrections → ManageFinancials
- HR → CreateStaff / ManageSchedules / ReviewLeave / View+ManageSalaries /
  ManageStaffCredentials (self-service clock-in/leave preserved)
- reports → ViewReports, export → ExportReports, audit → ViewAuditLog
- billing → ManageBilling, sms → SendSms/ManageSmsSettings, reviews → ManageReviews,
  discover/public profile → ManageDiscoverProfile, café settings → ManageCafeSettings,
  custom roles → ManageRoles

Removes legacy [Authorize(Roles=...)] attributes that would have overridden the
permission model (orders, branch-menu, pos-device, print). Manual discount/comp
have no backend endpoint yet (discounts come from coupons) — gated on the POS UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-21 05:43:07 +03:30
parent 236013f53c
commit 7a5ea75b50
32 changed files with 162 additions and 77 deletions
+1 -1
View File
@@ -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;
@@ -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<SubscribeRequest> _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<object>(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<IActionResult> 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<object>(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)
@@ -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<IActionResult> 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<IActionResult> 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);
@@ -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));
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
@@ -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)
@@ -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))
@@ -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."));
@@ -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<CafeReviewDto>(true, data));
@@ -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)
@@ -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<CouponDto>(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<object>(true, new { id }));
@@ -23,7 +23,7 @@ public class CustomRolesController : CafeApiControllerBase
public async Task<IActionResult> 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<IActionResult> 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);
@@ -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<object>(true, new { id }));
@@ -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);
@@ -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<object>(false, null, new ApiError("UNAUTHORIZED", "User context is missing.")));
if (!CanLogExpense(tenant.Role))
return StatusCode(StatusCodes.Status403Forbidden,
new ApiResponse<object>(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<object>(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<object>(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<ExpenseDto> result, int successStatus = StatusCodes.Status200OK)
{
if (result.Success)
+10 -7
View File
@@ -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<object>(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<IReadOnlyList<ShiftDto>>(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<IReadOnlyList<LeaveRequestDto>>(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<IReadOnlyList<EmployeeSalaryDto>>(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<IActionResult> 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<EmployeeSalaryDto>(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);
@@ -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<object>(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<object>(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<object>(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<object>(true, recipe));
@@ -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<IActionResult> 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<object>(true, new { id }));
+32 -6
View File
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
}
/// <summary>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.</summary>
@@ -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<MenuCategoryDto>(true, data));
@@ -95,6 +98,7 @@ public class MenuController : CafeApiControllerBase
public async Task<IActionResult> 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<object>(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<MenuItemDto>(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<MenuItemDto>(true, data));
@@ -171,6 +178,7 @@ public class MenuController : CafeApiControllerBase
public async Task<IActionResult> 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<object>(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)
@@ -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<IActionResult> 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<object>(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<IActionResult> 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));
@@ -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));
+2 -2
View File
@@ -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<IActionResult> 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);
@@ -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<object>(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<object>(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)
@@ -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<object>(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<object>(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<object>(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<object>(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<IReadOnlyList<TrendDayDto>>(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<object>(false, null,
new ApiError("VALIDATION_ERROR", "Only excel format is supported.")));
@@ -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<ReservationDto>(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<object>(true, new { id }));
@@ -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<object>(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<object>(false, null, new ApiError("UNAUTHORIZED", "User context is missing.")));
+3 -1
View File
@@ -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));
@@ -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<IActionResult> 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));
@@ -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);
+4 -3
View File
@@ -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<TaxDto>(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<TaxDto>(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<object>(true, new { id }));
@@ -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<object>(true, new { revoked = true }));
}