Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

13 — State Management (RBAC Module)

Cubit architecture for the RBAC surface, on 00-shared/06. Server model recap: JWT carries roles claim (minted at login, auth.service.ts:145-154); effective permissions resolve via Redis-cached getPermissionsForUser (rbac.service.ts:44-73); the client mirrors this with its own permission cache.


1. Cubits

CubitStateAPI
RoleListCubitLoadState + List<Role> + searchQueryGET /rbac/roles
RoleEditorCubitLoadState + Role? + Set<String> selected + List<String> catalog + formFields + savingGET /rbac/permissions, POST /rbac/roles, PATCH /rbac/roles/:id
MemberListCubitLoadState + List<MemberView> + searchQueryGET /rbac/members + user.read merge
MemberSheetCubitLoadState + User? selected + Set<String> roles + saving + editModePOST /rbac/members, PATCH /rbac/members/:id, GET /rbac/roles (picker)
AuditCubitLoadState + List<AuditRow> + filters + PaginatedListMixinGET /audit-logs

All follow LoadState (00-shared/06 §3.1); audit uses PaginatedListMixin (§3.2).

Events: Load, Refresh, Retry, ChangeSearch, TogglePermission, ToggleGroup, Save, ClearFilters — naming per 00-shared/06 §4.

2. PermissionMirror — client-side permission state (core)

The client's analog of rbac.service.ts:44-73:

class PermissionMirror {
  // key: 'sl:{tenantId}:perm:{userId}' — mirrors server key shape (rbac.service.ts:48)
  final Map<String, List<String>> _cache;          // in-memory
  final HiveCache _hive;                            // persistent, per tenant
  // stale-while-revalidate: serve cached, refresh ≤ 300 s TTL (server EX 300,
  // rbac.service.ts:68), rebuild on miss
  Future<Set<String>> permissionsFor(String userId, String tenantId);
  void invalidate(String tenantId, String userId); // after role/member edits by self
}

Rules:

  • Server is authoritative. The mirror is a cache, never a decision-maker — every critical action still surfaces server 403s.
  • TTL mirrors the server: ≤ 300 s freshness; invalidates on own writes (create/ update/delete role, add/update/remove member) and on login.
  • Route rebuild: when the mirror resolves a changed permission set for the current user (login, manual refresh, own RBAC write, session resume), emit permissionsChangedAppRouter.refresh() → hidden/unroutable destinations update (05 §9, 11_Flutter_App_Architecture.md §6).
  • JWT roles claim is separate (jwt-payload.interface.ts:3): AuthCubit holds it; role-claim changes require re-login (OQ-R4) — the client offers "Refresh session" after self-role edits (09 §3).

3. Cross-screen data flow

AppShell (05 §1)
 ├─ AuthCubit → roles claim (JWT) + userId + tenantId
 ├─ PermissionMirror → Set<String> myPermissions (SWR, 300 s)
 └─ FeatureFlagsCubit → gates coaching perms (planned) — render only when server ships
Router guards (permissionGuard('rbac.role.read'), …) — 15 §5
   │
   ▼
RBAC screens → module cubits (above) → repositories (dio) → API
  • Shared selectors: currentUser, currentTenant (00-shared/06 §4).
  • No global singleton holds screen state; cubits are get_it lazy factories.

4. Guard composition (client routes)

Current server reality (role-gated /rbac/*, rbac.controller.ts:21) vs intended perm-gated model → combined guard (see 04 §3):

roleOrPermissionGuard(roles: ['org_admin'], permissions: ['rbac.role.read'])

Resolves true if the JWT claims org_admin or the mirror grants rbac.role.read — removes screens for both the teacher (no role, no perm) and the future role-manager (perm yes, role no) as OQ-R1 evolves. Audit route: permissionGuard('audit.read') (server lags, audit.controller.ts:9(planned)).

5. Editor state persistence

  • RoleEditorCubit holds selected (Set) across backgrounding/session expiry (in-memory; survives re-login per 09 §9).
  • Dirty tracking: formFields != original or selected != role.permissions → back confirm (10 §4).
  • Save: one PATCH with the full array (rbac.service.ts:96); no per-cell writes.

6. Member merge strategy

MemberListCubit fetches GET /rbac/members + GET /api/v1/users (user.read, cached 24 h) → MemberView {member, user?}; missing user → fallback tile (userId mono, 06 §S4). Merge is display-only; writes send member.userId unchanged (OQ-R7).

7. Optimistic updates — allowed list

ActionOptimistic?Rule
Toggle permission / groupyes (local set)single PATCH on Save; rollback on error
Add membernoserver-confirm, then insert row
Remove member / delete rolenoserver-confirm, then remove row (irreversible-ish, 09 §4)
Role perm count badgeyesderived from editor state

8. Realtime & refresh

  • No WS topic for RBAC changes exists (server WsModule topics list has none — 00-shared/07 §8; OQ-R9). Cross-admin freshness = pull-to-refresh + stale-while- revalidate only.
  • On app resume: refetch roles/members lists if stale > 5 min; re-validate mirror.

9. Testing hooks

  • Cubits pure-Dart with mocked repositories; PermissionMirror unit-tested for SWR/TTL/invalidate semantics; widget tests for 3-state screens (00-shared/06 §6).