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 /// truth for authorization — controllers check a <see cref="Permission"/> rather
/// than hard-coding role names, so the role→capability mapping lives in exactly /// than hard-coding role names, so the role→capability mapping lives in exactly
/// one place (<see cref="RolePermissions"/>). /// 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> /// </summary>
public enum Permission public enum Permission
{ {
// Café-level administration (Owner only) // ── Café administration (owner tier) ──────────────────────────────────────
ViewCafeSettings,
ManageCafeSettings, ManageCafeSettings,
ManageDiscoverProfile,
ViewBilling,
ManageBilling, ManageBilling,
ManageBranches, ViewBranches,
CreateBranch,
// Management (Owner + Manager) EditBranch,
ManageStaff, DeleteBranch,
ManageMenu, ManageRoles,
ManageInventory, ViewPrintSettings,
ManageExpenses,
ManageTaxes,
ManageCoupons,
ManageReservations,
ManageTables,
ViewReports,
ReviewLeave,
ManageSalaries,
ManagePrintSettings, 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, ProcessOrders,
EditOrder,
VoidOrder,
RefundOrder,
ApplyDiscount,
CompOrder,
HandlePayments, HandlePayments,
UpdateOrderStatus,
// ── Register / cash ──────────────────────────────────────────────────────
OperateRegister, OperateRegister,
OpenCashDrawer,
// ── Queue ─────────────────────────────────────────────────────────────────
ViewQueue,
ManageQueue, ManageQueue,
// Kitchen // ── Kitchen ───────────────────────────────────────────────────────────────
ViewKitchen, ViewKitchen,
ManageKitchenStations,
// Delivery // ── Delivery ──────────────────────────────────────────────────────────────
ViewDelivery,
HandleDelivery, 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 Meezi.Core.Enums;
using static Meezi.Core.Authorization.Permission;
namespace Meezi.Core.Authorization; namespace Meezi.Core.Authorization;
/// <summary> /// <summary>
/// The authoritative role→capability matrix. Change what a role can do here and /// The authoritative role→capability matrix. Change what a base role can do here
/// every controller that calls <c>EnsurePermission</c> updates automatically. /// and every controller that calls <c>EnsurePermission</c> updates automatically.
/// Owners customise further with custom roles (which override this matrix entirely).
/// </summary> /// </summary>
public static class RolePermissions 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 = private static readonly IReadOnlyDictionary<EmployeeRole, HashSet<Permission>> Matrix =
new Dictionary<EmployeeRole, HashSet<Permission>> new Dictionary<EmployeeRole, HashSet<Permission>>
{ {
[EmployeeRole.Owner] = AllPermissions(), [EmployeeRole.Owner] = AllPermissions(),
[EmployeeRole.Manager] = new() // Manager runs the café day to day: everything except the owner-only
{ // governance (billing, branches, café identity, role definitions).
Permission.ManageStaff, [EmployeeRole.Manager] = AllExcept(OwnerOnly),
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,
},
[EmployeeRole.Cashier] = new() [EmployeeRole.Cashier] = new()
{ {
Permission.ProcessOrders, ViewOrders, ProcessOrders, EditOrder, HandlePayments, UpdateOrderStatus,
Permission.HandlePayments, OperateRegister, OpenCashDrawer,
Permission.OperateRegister, ViewQueue, ManageQueue,
Permission.ManageQueue, ViewTables,
Permission.ManageReservations, ViewReservations, CreateReservation, EditReservation,
ViewMenu,
ViewCustomers, CreateCustomer,
ViewCoupons,
}, },
[EmployeeRole.Waiter] = new() [EmployeeRole.Waiter] = new()
{ {
Permission.ProcessOrders, ViewOrders, ProcessOrders, EditOrder, UpdateOrderStatus,
Permission.ManageReservations, ViewTables,
Permission.ManageQueue, ViewMenu,
ViewReservations, CreateReservation, EditReservation,
ViewQueue, ManageQueue,
}, },
[EmployeeRole.Chef] = new() [EmployeeRole.Chef] = new()
{ {
Permission.ViewKitchen, ViewKitchen, UpdateOrderStatus, ViewOrders, ViewMenu,
}, },
[EmployeeRole.Delivery] = new() [EmployeeRole.Delivery] = new()
{ {
Permission.HandleDelivery, ViewDelivery, HandleDelivery, ViewOrders,
}, },
}; };
@@ -73,4 +75,7 @@ public static class RolePermissions
private static HashSet<Permission> AllPermissions() => private static HashSet<Permission> AllPermissions() =>
new(Enum.GetValues<Permission>()); new(Enum.GetValues<Permission>());
private static HashSet<Permission> AllExcept(HashSet<Permission> excluded) =>
new(Enum.GetValues<Permission>().Where(p => !excluded.Contains(p)));
} }