Skip to content

Permissions ​

The SPA does not invent authorization. The Worker is the source of truth; the frontend mirrors that model for routing and UI so users do not see actions they cannot perform.

Related: Routing · Features · Lifecycle permissions matrix.


Three-layer model ​

LayerWhereJob
1. RoutePermissionGuard + routeConfigBlock whole pages
2. Action UIusePermissions() / hasPermissionHide or disable buttons
3. BackendWorker requirePermission*Final deny — always

If the UI is wrong, the API must still reject. If the API allows it, the UI should not invent a stricter rule without updating the shared permission catalog.


How the SPA gets permissions ​

  1. Auth / me load establishes the current user
  2. Permission IDs come from the user payload (and/or system catalog for labels)
  3. usePermissions() (apps/web/src/context/auth/usePermissions.ts) exposes helpers such as hasPermission(id)
  4. Super admin bypass is preserved in the client helper (aligned with Worker behavior)
  5. Role fallback is used only when a permission list is not present — prefer explicit IDs

System permission catalog (for admin UIs): systemService → GET /system/permissions.


Route gating ​

  • Canonical map: apps/web/src/router/AppRoutes.tsx
  • Access table: apps/web/src/router/config/routeConfig.ts (getRouteAccess, ROUTE_ACCESS)
  • Wrapper: PermissionGuard applies access around the page element

Paths are not role-prefixed (/users, not /admin/users). Role folders under domains/* are for unique experiences, not for access control by path segment.

Missing page access → unauthorized / blocked route flow (not a blank screen).


Action gating ​

On shared pages (users, maintenance, departments, system):

typescript
const { hasPermission } = usePermissions();
// e.g. show Create only if hasPermission('user.create')
SituationUX
No page accessUnauthorized route
Page OK, action deniedHide or disable the control
Backend still deniesShow error from ApiError gracefully

Maintainer rules ​

  1. New routes must declare access in routeConfig and wrap with PermissionGuard.
  2. New destructive / privileged buttons must check a real permission ID from the Worker catalog.
  3. Do not hard-code role names for access when a permission ID exists.
  4. Keep lifecycle matrices in docs/lifecycle/ in sync when maintenance permissions change.