feat(permissions): per-desk till guards, jobs in the role composer, permission-scoped live feed; role reassignment applies without re-login
CI / check (push) Successful in 46s
Build & push images / images (push) Successful in 2m58s
Build desktop / desktop (push) Successful in 4m53s

Permissions matrix rethink (wiki/decisions/venue-modules.md §"Permissions matrix",
open-questions #16) — the grid stays the enforcement layer:

- Move 1: each desk's money is guarded by that desk's own permissions. Manifest
  tillGuards {read, shift, cash}: booth = shift:read / shift:create / drawer:create
  (unchanged), carwash = carwash:read / carwash:cash (new). Shift + drawer routes
  resolve the guard FROM THE TILL (requireTill); a wash role holds no shift:* and cannot
  touch the booth by construction. Replaces the session:read borrowing (tillPermission).
  /api/shift/tills lists the role's readable tills with canWork; history/movements
  without a till filter return the union of readable tills.
- Move 2: jobs — manifest permission bundles (booth-operator, booth-supervisor,
  merchant, wash-operator) as one-click chips in Setup → Roles, with "mixes desks" and
  "partial job" lints (warnings, never blocks).
- Move 3: the live WebSocket admits any watch permission (event/session/device read or
  a module's feedPermission) and filters every push per role; report:read is the
  reports screen only.

Auth: the token's roleId is only a hint — refreshRole() after every jwtVerify resolves
the user's CURRENT role (cached, bumped on role/user writes), so reassigning a user's
role applies on the next request and a deleted user's session ends with 401.

Tests: till guards + look-only role, feed rules, every job's permissions exist, role
reassignment without re-login. 353/353.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-05 14:45:48 +02:00
parent a9ccf9e20c
commit 55d6242c7d
24 changed files with 654 additions and 206 deletions
+5
View File
@@ -32,6 +32,11 @@ Authentication and authorization, kept **fully local** — a direct consequence
name. The JWT carries `roleId` (not the permission list); the guard resolves the role's permission
set per-request from an **in-memory cache** (`bumpPermsCache()` on any role write), so editing a
role applies immediately — no re-login, no token bloat. No Casbin/engine needed at this scale.
**The token's `roleId` is only a hint (2026-09-05):** after every `jwtVerify` the guard replaces it
with the user's CURRENT role from the DB (`refreshRole()`; cached per user, cleared by the same
`bumpPermsCache()`, which user update/delete now call), so REASSIGNING a user's role — or deleting
the user (→ 401 on their next request) — applies immediately too. Found when a user moved to a new
wash role kept the old role's rights until logout.
- **Protected built-in `admin` role** (`id='admin'`, `builtin=1`): non-editable, non-deletable, and
always resolves to the FULL permission set in code. The app refuses to delete or downgrade the
**last user holding admin** — administration can never be locked out of the appliance.