feat(rbac): full-CRUD permission catalog + per-role matrix
CI/CD / CI · API (dotnet build + test) (push) Successful in 55s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m9s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 1m33s

Expands the authorization catalog from 21 coarse page-level permissions to a
granular set: View/Create/Edit/Delete per record module, plus distinct
permissions for sensitive actions (VoidOrder, RefundOrder, ApplyDiscount,
CompOrder, OpenCashDrawer, ExportReports) and the previously-uncovered pages
(customers/CRM, SMS, reviews, financials, audit log, attendance, schedules).

RolePermissions now derives Manager as "everything except owner-only governance"
and gives Cashier/Waiter/Chef/Delivery sensible day-to-day defaults; owners
refine further via custom roles. Effective permissions already flow to the
client through AuthService, so no token-shape change is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-21 05:27:02 +03:30
parent 170a9aa7ac
commit 236013f53c
2 changed files with 154 additions and 51 deletions
+116 -18
View File
@@ -5,37 +5,135 @@ namespace Meezi.Core.Authorization;
/// truth for authorization — controllers check a <see cref="Permission"/> rather
/// than hard-coding role names, so the role→capability mapping lives in exactly
/// one place (<see cref="RolePermissions"/>).
///
/// Granularity is "full CRUD per module + distinct sensitive actions": each page
/// has a View capability, record modules split Create/Edit/Delete, and high-risk
/// operations (void, refund, discount, comp, cash drawer, export) are their own
/// permissions so an owner can grant day-to-day work without the dangerous bits.
///
/// Names are persisted (custom roles store them by name in JSON, and they ride in
/// the JWT). Renaming or removing a value is a breaking change — add, don't rename.
/// </summary>
public enum Permission
{
// Café-level administration (Owner only)
// ── Café administration (owner tier) ──────────────────────────────────────
ViewCafeSettings,
ManageCafeSettings,
ManageDiscoverProfile,
ViewBilling,
ManageBilling,
ManageBranches,
// Management (Owner + Manager)
ManageStaff,
ManageMenu,
ManageInventory,
ManageExpenses,
ManageTaxes,
ManageCoupons,
ManageReservations,
ManageTables,
ViewReports,
ReviewLeave,
ManageSalaries,
ViewBranches,
CreateBranch,
EditBranch,
DeleteBranch,
ManageRoles,
ViewPrintSettings,
ManagePrintSettings,
// Front-of-house operations
// ── Taxes ─────────────────────────────────────────────────────────────────
ViewTaxes,
CreateTax,
EditTax,
DeleteTax,
// ── Staff & HR ──────────────────────────────────────────────────────────────
ViewStaff,
CreateStaff,
EditStaff,
DeleteStaff,
/// <summary>Assign per-branch roles / org structure (distinct from editing a record).</summary>
ManageStaff,
ManageStaffCredentials,
ViewAttendance,
ManageAttendance,
ViewSchedules,
ManageSchedules,
ViewLeave,
ReviewLeave,
ViewSalaries,
ManageSalaries,
// ── Menu ──────────────────────────────────────────────────────────────────
ViewMenu,
CreateMenuItem,
EditMenuItem,
DeleteMenuItem,
// ── Inventory ───────────────────────────────────────────────────────────────
ViewInventory,
CreateInventory,
EditInventory,
DeleteInventory,
// ── Tables ──────────────────────────────────────────────────────────────────
ViewTables,
ManageTables,
// ── Reservations ──────────────────────────────────────────────────────────
ViewReservations,
CreateReservation,
EditReservation,
DeleteReservation,
// ── Orders & POS ──────────────────────────────────────────────────────────
ViewOrders,
ProcessOrders,
EditOrder,
VoidOrder,
RefundOrder,
ApplyDiscount,
CompOrder,
HandlePayments,
UpdateOrderStatus,
// ── Register / cash ──────────────────────────────────────────────────────
OperateRegister,
OpenCashDrawer,
// ── Queue ─────────────────────────────────────────────────────────────────
ViewQueue,
ManageQueue,
// Kitchen
// ── Kitchen ───────────────────────────────────────────────────────────────
ViewKitchen,
ManageKitchenStations,
// Delivery
// ── Delivery ──────────────────────────────────────────────────────────────
ViewDelivery,
HandleDelivery,
AssignDelivery,
// ── Customers / CRM ───────────────────────────────────────────────────────
ViewCustomers,
CreateCustomer,
EditCustomer,
DeleteCustomer,
// ── Coupons ───────────────────────────────────────────────────────────────
ViewCoupons,
CreateCoupon,
EditCoupon,
DeleteCoupon,
// ── SMS / marketing ──────────────────────────────────────────────────────
ViewSms,
SendSms,
ManageSmsSettings,
// ── Reviews ───────────────────────────────────────────────────────────────
ViewReviews,
ManageReviews,
// ── Reports & finance ─────────────────────────────────────────────────────
ViewReports,
ExportReports,
ViewAuditLog,
ViewFinancials,
ManageFinancials,
// ── Expenses ──────────────────────────────────────────────────────────────
ViewExpenses,
CreateExpense,
EditExpense,
DeleteExpense,
}
+38 -33
View File
@@ -1,64 +1,66 @@
using Meezi.Core.Enums;
using static Meezi.Core.Authorization.Permission;
namespace Meezi.Core.Authorization;
/// <summary>
/// The authoritative role→capability matrix. Change what a role can do here and
/// every controller that calls <c>EnsurePermission</c> updates automatically.
/// The authoritative role→capability matrix. Change what a base role can do here
/// and every controller that calls <c>EnsurePermission</c> updates automatically.
/// Owners customise further with custom roles (which override this matrix entirely).
/// </summary>
public static class RolePermissions
{
/// <summary>Capabilities reserved to the Owner — the rest is the Manager baseline.</summary>
private static readonly HashSet<Permission> OwnerOnly = new()
{
ManageCafeSettings,
ManageDiscoverProfile,
ViewBilling,
ManageBilling,
CreateBranch,
EditBranch,
DeleteBranch,
ManageRoles,
};
private static readonly IReadOnlyDictionary<EmployeeRole, HashSet<Permission>> Matrix =
new Dictionary<EmployeeRole, HashSet<Permission>>
{
[EmployeeRole.Owner] = AllPermissions(),
[EmployeeRole.Manager] = new()
{
Permission.ManageStaff,
Permission.ManageMenu,
Permission.ManageInventory,
Permission.ManageExpenses,
Permission.ManageTaxes,
Permission.ManageCoupons,
Permission.ManageReservations,
Permission.ManageTables,
Permission.ViewReports,
Permission.ReviewLeave,
Permission.ManageSalaries,
Permission.ManagePrintSettings,
Permission.ProcessOrders,
Permission.HandlePayments,
Permission.OperateRegister,
Permission.ManageQueue,
Permission.ViewKitchen,
Permission.HandleDelivery,
},
// Manager runs the café day to day: everything except the owner-only
// governance (billing, branches, café identity, role definitions).
[EmployeeRole.Manager] = AllExcept(OwnerOnly),
[EmployeeRole.Cashier] = new()
{
Permission.ProcessOrders,
Permission.HandlePayments,
Permission.OperateRegister,
Permission.ManageQueue,
Permission.ManageReservations,
ViewOrders, ProcessOrders, EditOrder, HandlePayments, UpdateOrderStatus,
OperateRegister, OpenCashDrawer,
ViewQueue, ManageQueue,
ViewTables,
ViewReservations, CreateReservation, EditReservation,
ViewMenu,
ViewCustomers, CreateCustomer,
ViewCoupons,
},
[EmployeeRole.Waiter] = new()
{
Permission.ProcessOrders,
Permission.ManageReservations,
Permission.ManageQueue,
ViewOrders, ProcessOrders, EditOrder, UpdateOrderStatus,
ViewTables,
ViewMenu,
ViewReservations, CreateReservation, EditReservation,
ViewQueue, ManageQueue,
},
[EmployeeRole.Chef] = new()
{
Permission.ViewKitchen,
ViewKitchen, UpdateOrderStatus, ViewOrders, ViewMenu,
},
[EmployeeRole.Delivery] = new()
{
Permission.HandleDelivery,
ViewDelivery, HandleDelivery, ViewOrders,
},
};
@@ -73,4 +75,7 @@ public static class RolePermissions
private static HashSet<Permission> AllPermissions() =>
new(Enum.GetValues<Permission>());
private static HashSet<Permission> AllExcept(HashSet<Permission> excluded) =>
new(Enum.GetValues<Permission>().Where(p => !excluded.Contains(p)));
}