Permission-Based System — Enterprise Plan
Status: Partially implemented. Use this document as the single reference for design and remaining work.
Latest state: See §12 for the most recent work (GET /me full payload, suspend/activate cache invalidation, GET /users/:id from cache, login suspension checks). See §13 for the new task list to continue from here.
Current state (as of last update)
Done:
- Backend
lib/constants/permissions.ts: Single list with user., maintenance., department.*, system.settings, monitoring.read, analytics.read, audit.read, audit.export, logs.read; plus user.manage, user.view_stats, user.activate, user.suspend, user.activity for existing user routes.requirePermissionOrSuperAdmin(c, permission)in auth/verify; all protected system/monitoring/user routes use it (super_admin bypass).ensureCurrentUser(c)used on all system and monitoring routes for stable 401 (expired_token/session_invalid).- GET /system/permissions, GET /me/permissions; calculatePermissions (login + JWT) uses SYSTEM_PERMISSIONS for super_admin; technician and employee get maintenance.*.
- System routes: GET /system/analytics, /system/logs, /system/audit, /system/audit/export, /system/monitoring; all gated by permission + ensureCurrentUser.
- Monitoring routes under /monitoring/* use monitoring.read (or logs.read, audit.read where applicable) + ensureCurrentUser.
- Seed scripts: create-admin and interactive createSuperAdmin use SYSTEM_PERMISSIONS; no duplicate super_admin; offer password reset if one exists. operations.updateUserPassword() added.
- Frontend
- Sidebar: MAIN (Dashboard, Users, Maintenance, Analytics), SYSTEM section (Monitor, Logs, Audit, Settings — permission-based), Account (Profile, Settings, Security). No in-page System tabs; SYSTEM items only in sidebar.
- Routes: /system/monitor, /system/logs, /system/audit, /system/settings with PermissionGuard; super_admin bypass in guard; SystemIndexRedirect to first allowed tab.
- systemService: getMonitoring, getLogs, getAudit, getAuditExport, getAuditExportCsvUrl.
- Plan doc “Where to start” and backend path (types → zod → routes) documented.
Not done yet (continue from here — see §12 and §13):
- Permission editing UI for super_admin (PUT /users/:id/permissions API exists); default permissions on create/register; Register and user create rules (reject super_admin; admin cannot create admin/super_admin or set permissions).
- All items in §13 New tasks: frontend hooks/providers, GET /users & /users/:id cache/errors, permission+role rules, cache invalidation on all user changes, super_admin count/restrictions/self-edit prevention, department tie by role, audit/internal logs coverage and patterns, internal logs storage/value.
Where to start (implementation order)
Start with the backend. Frontend depends on GET /system/permissions and the new /system/* APIs.
Backend path (follow existing patterns):
- types/ — Define (or re-export) all type interfaces for the system routes in
types/system/*(e.g. analytics API response/query, internal logs list response). Align with existingtypes/me,types/cache,types/auth. - lib/services/ — Use existing services where possible:
getServices(['cacheService', 'auditService', 'internalLogger', 'config']), orcreateAnalyticsCacheService(env)from cache/analytics. Add new service classes only if a route needs logic that doesn’t fit existing services. - lib/services/zod/ — Add Zod schemas for query/body validation in
lib/services/zod/system/*(e.g. analytics query, logs query). ReuseAuditLogQuerySchemafrom zod/system for audit. Follow existing patterns in zod/system and zod/me. - routes/ — Add route files under
routes/system/*(e.g. analytics.ts, audit.ts, logs.ts). UserequirePermission(c, 'permission')orrequirePermissionOrSuperAdmin(c, 'permission'), parse query/body with the zod schemas, call the appropriate service or cache, returnApiResponse.success(c, data). Mount each at/system/analytics,/system/audit,/system/logs, etc. inmiddleware/app.ts.
Useful patterns: getService('authService'), getServices(['auditService', 'config']), cache via getService('cacheService').service.analytics or createAnalyticsCacheService(c.env); internal logs via getService('internalLogger').getLogs(options).
Goals: (1) Permissions as the only access mechanism (no role-based checks except super_admin). (2) Clear namespaces: monitoring.*, analytics.*, system.settings only under system, audit., logs.. (3) Single super_admin; Register never allows super_admin. (4) Admins get Monitor + SYSTEM section (Audit, Monitor, Settings, Logs). (5) Analytics API and internal logs for admins/devs. (6) Frontend and backend both rely on lib/constants/permissions.ts via GET /system/permissions and GET /me/permissions.
1. Permission model (single source of truth)
1.1 Canonical list — apps/worker/src/lib/constants/permissions.ts
All permission identifiers live here. Frontend and all services derive from GET /system/permissions (no duplicate list anywhere).
Target permission set (namespaced):
| Namespace | Permissions | Purpose |
|---|---|---|
| user.* | user.create, user.read, user.update, user.delete | User CRUD and list |
| maintenance.* | maintenance.create, read, update, delete, approve, assign, complete, cancel | Maintenance lifecycle |
| department.* | department.create, read, update, delete | Department management |
| system.* | system.settings only | System configuration (admin system settings UI); no monitoring/analytics under system |
| monitoring.* | monitoring.read | Operational monitoring: health, errors, incidents, diagnostics (Monitor page) |
| analytics.* | analytics.read | Analytics/reports (aggregate data from cache); Analytics API and pages |
| audit.* | audit.read, audit.export | Audit log view and CSV export (remove audit.admin) |
| logs.* | logs.read | Internal/system logs (e.g. DO cache logs); /logs page for admins and developers |
Removed / renamed from current:
system.monitor→ monitoring.readsystem.analytics→ analytics.readaudit.admin→ audit.export (and full admin audit access under audit.read for those who have it)
Resulting constant (to implement):
// lib/constants/permissions.ts (target)
export const SYSTEM_PERMISSIONS = [
'user.create', 'user.read', 'user.update', 'user.delete',
'maintenance.create', 'maintenance.read', 'maintenance.update', 'maintenance.delete',
'maintenance.approve', 'maintenance.assign', 'maintenance.complete', 'maintenance.cancel',
'department.create', 'department.read', 'department.update', 'department.delete',
'system.settings',
'monitoring.read',
'analytics.read',
'audit.read', 'audit.export',
'logs.read',
] as const;2. Access control: permission-only (roles only for super_admin)
2.1 Rule
- super_admin: Treated as having all permissions. Backend does not check permissions for
user.role === 'super_admin'; allow the action. (Super_admin is the only role-based exception.) - Everyone else: Backend allows an action only if the acting user has the required permission for that action (from session/me permissions). No checks like “if role === administrator” for access control.
- Where permissions come from: Stored per user (DB or derived from role defaults); returned by GET /me/permissions (and in session). Super_admin’s permissions are never read for checks; they always pass.
2.2 Backend changes (when implementing)
- Replace every role-based guard (e.g. “only administrator/super_admin”) with permission-based guards: e.g.
requirePermission(c, 'user.read')or “allow if has permission X or is super_admin.” - Central helper: e.g.
requirePermissionOrSuperAdmin(c, 'audit.export')— if session.user.role === 'super_admin' then continue; else requirePermission(c, 'audit.export'). - Auth/verify layer: one place that resolves “current user” and “has permission P or is super_admin”; all routes use that. No ad-hoc role checks for access.
2.3 Super_admin uniqueness and registration
- Only one super_admin in the system (by policy). No DB unique constraint required.
- Register API and Register page: Do not allow creating or registering a user with role
super_admin. Allowed roles for registration: e.g.administrator,department_head,developer,employee,technicianonly. Reject with 400 ifrole === 'super_admin'. - Creating a second super_admin (if ever needed) would be a separate, highly protected flow (e.g. existing super_admin only, or seed/migration only), out of scope for normal Register.
3. User management and permission management
3.1 Super_admin
- Can manage all users (create, update, delete, suspend, etc.).
- Can edit any user’s permissions (grant/revoke any permission from the system list).
- Can create users with any role except that Register API/Page still must not accept
super_adminfrom the public flow (see above); internal/admin flows can define who can create super_admin.
3.2 Administrator
- Can do full user management (create, update, delete, suspend, etc.) except:
- Cannot edit any user’s permissions (no permission grant/revoke).
- Cannot create users with role
administratororsuper_admin— only non-admin roles (e.g. employee, developer, department_head, technician). So: admin can create “normal” users only; cannot create another admin or super_admin.
- Implement by:
- Permission: e.g.
user.create,user.update,user.deletefor user management. - Business rule in user create/update: If actor is administrator (not super_admin), reject request if target role is
administratororsuper_admin, and reject any attempt to changepermissions(or the permissions field is not accepted from admins at all).
- Permission: e.g.
3.3 Default permissions for new users
- Registration / user creation uses a default permission set (e.g. per role, or one global default). Stored in config or code (e.g. “default permissions for role employee = [maintenance.create, maintenance.read, maintenance.update]”).
- Super_admin can later change any user’s permissions (full override). No one else can edit permissions.
- Defaults can be configured in system settings (future) or in backend config so that “create user with role X” assigns the default permissions for X; super_admin then adjusts as needed.
4. Frontend: permissions as source of truth
4.1 APIs
- GET /system/permissions — Returns the full system permission list (from
lib/constants/permissions.ts). Authenticated only. Frontend uses this as the only list for permission labels, permission management UI, and validation. - GET /me/permissions — Returns the current user’s permissions (unchanged). Used for route guards and UI visibility.
- Frontend never maintains a separate hardcoded permission list; it always fetches /system/permissions when needed (e.g. once after login and cache in context).
4.2 Sidebar and navigation (all SYSTEM under /system/*)
- All SYSTEM section routes live under /system/*: Every system-related page uses a path under
/system/so the structure is consistent and clear. - Frontend URL paths (sidebar links):
- Analytics →
/system/analytics(requires analytics.read) - Audit →
/system/audit(requires audit.read) - Logs (internal logs) →
/system/logs(requires logs.read) - Settings (system settings) →
/system/settings(requires system.settings) - Monitor (monitoring) →
/system/monitor(requires monitoring.read)
- Analytics →
- Permission-driven sidebar: Sidebar items are shown only if the user has the required permission. The SYSTEM section is shown when the user has at least one of: analytics.read, audit.read, logs.read, system.settings, monitoring.read. Within SYSTEM, each entry (Analytics, Audit, Logs, Settings, Monitor) is visible only when the user has the corresponding permission.
- Admins and developers: Both access system features the same way (via /system/*). A developer sees the SYSTEM section in the sidebar but will not see Audit (or other items) unless they have the right permission (e.g. audit.read). By default, dev may have only monitoring.read and logs.read; super_admin can grant audit.read (or others) later, and then Audit will appear for that user.
- Main / Account section: Keep existing main section (Dashboard, Users, Maintenance, etc.) and account-related items; all system-related items (Analytics, Audit, Logs, Settings, Monitor) live under the SYSTEM section with /system/* paths only.
4.3 Routes and guards
- All protected routes are gated by permission (e.g. PermissionGuard with required permission). No “allowedRoles” for access; optional role fallback only for backward compatibility during migration.
- Route config (e.g.
routeConfig.ts) uses permission IDs from the system list (aligned with backend constant). Frontend loads system permissions once and can validate that route-required permissions are in the system list.
5. Backend system routes (all under /system/*)
Backend API base path for system features: All of the following live under /system/ so they differ from any existing top-level routes. Frontend pages use the same path names where it makes sense (see §4.2).
| Feature | Backend API path | Permission | Note |
|---|---|---|---|
| Analytics | GET /system/analytics | analytics.read | Optional query: fromDate, toDate, departmentId, etc. |
| Audit | GET /system/audit (list/count), GET /system/audit/export (CSV) | audit.read, audit.export | Move from /admin/audit to /system/audit. |
| Logs | GET /system/logs | logs.read | Internal/system logs (e.g. DO cache); pagination or cursor. |
| Monitoring | GET /system/monitoring (and sub-routes for health, errors, incidents, etc.) | monitoring.read | Move or mount existing monitoring routes under /system/monitoring. |
| Permissions list | GET /system/permissions | (auth only) | Already exists; returns full system permission list. |
If the frontend uses a different path for a page (e.g. /system/monitor for the Monitor page) than the backend uses for the API (e.g. /system/monitoring), the frontend page simply calls the backend at /system/monitoring. The plan uses: backend /system/monitoring for monitoring APIs; frontend /system/monitor for the Monitor page URL.
6. Analytics API (implementation step)
- Endpoint: GET /system/analytics with optional query:
fromDate,toDate,departmentId, etc. - Backend: Use existing AnalyticsCacheService (e.g.
generateAnalytics(query)). No new service; wire a new route under /system/analytics that calls the cache service and returns the result. - Permission: Gate with analytics.read (and super_admin bypass).
- Response: Return the same structure as
AnalyticsData(totalUsers, activeUsers, maintenance stats, performance, security stats, time range, etc.) as JSON. - Frontend: Analytics page at
/system/analyticscalls GET /system/analytics when user has analytics.read; remove placeholder-only behavior.
7. Internal logs and Monitor (paths under /system/*)
7.1 Internal logs
- Backend: GET /system/logs for listing internal/system logs (e.g. from internal logger / DO cache). Pagination or cursor-based. Permission: logs.read.
- Frontend: Page at /system/logs; sidebar SYSTEM section “Logs” →
/system/logs. Both admins and developers with logs.read access it.
7.2 Monitor (monitoring)
- Backend: GET /system/monitoring (and existing monitoring sub-routes mounted under /system/monitoring). Permission: monitoring.read.
- Frontend: Page at /system/monitor (Monitor page URL); sidebar SYSTEM section “Monitor” →
/system/monitor. Page calls backend /system/monitoring APIs. Grant monitoring.read to administrators (and super_admin) so admins and developers both get the Monitor page when they have the permission.
8. Audit permissions (audit.read, audit.export)
- audit.read — View audit logs (list and filters). Grant per user (e.g. super_admin, administrator, or developer if super_admin grants it).
- audit.export — Export audit logs (e.g. CSV). Grant to users who need compliance/export (e.g. super_admin, administrator). Replace all audit.admin checks with audit.read for read and audit.export for export.
- Backend: Under /system/audit: e.g. GET /system/audit (list/count), GET /system/audit/export (CSV). Require audit.read for read, audit.export for export (or super_admin). This differs from the old
/admin/audit/*paths; new paths are /system/audit and /system/audit/export.
9. Implementation checklist (use when implementing)
Backend (workers):
- [ ] Update
lib/constants/permissions.ts: add monitoring.read, analytics.read, audit.read, audit.export, logs.read; remove system.monitor, system.analytics, audit.admin; keep system.settings. - [ ] Add central guard: e.g.
requirePermissionOrSuperAdmin(c, permission); refactor all protected routes to use permission (or this helper) instead of role. - [ ] Register API/validation: reject
role === 'super_admin'(and optionally rejectrole === 'administrator'from public register if only admins can create other roles). - [ ] User create (admin flow): if actor is administrator, reject target role administrator/super_admin and reject updating permissions.
- [ ] Default permissions: define default set per role (or global); assign on user create/register; super_admin can change any user’s permissions via a dedicated API (e.g. PUT /users/:id/permissions).
- [ ] GET /system/permissions: return list from constants (already exists); ensure it returns the new list.
- [ ] GET /me/permissions: continue to return user’s permissions (from DB or derived); for super_admin you may return full system list or leave as-is and rely on “super_admin bypass” in guards.
- [ ] Analytics API: add GET /system/analytics, call AnalyticsCacheService.generateAnalytics(query), gate with analytics.read.
- [ ] Internal logs API: add GET /system/logs, gate with logs.read.
- [ ] Audit routes: mount under /system/audit (e.g. GET /system/audit, GET /system/audit/export); switch from audit.admin to audit.read and audit.export.
- [ ] Monitoring routes: mount under /system/monitoring; switch from system.monitor to monitoring.read; ensure administrators (and super_admin) have monitoring.read in default/role mapping so admins get Monitor.
Frontend (web):
- [ ] Use GET /system/permissions as single source for permission list (already in auth/context); ensure route config and PermissionGuard use only IDs from that list.
- [ ] Sidebar: SYSTEM section with paths under /system/* only: Analytics → /system/analytics, Audit → /system/audit, Logs → /system/logs, Settings → /system/settings, Monitor → /system/monitor. Show each item only if user has the corresponding permission (audit.read, logs.read, system.settings, monitoring.read, analytics.read). Both admins and developers see SYSTEM when they have at least one of these; e.g. dev without audit.read does not see Audit until super_admin grants it.
- [ ] Navigation helper: build menu from permissions (and system permission list) instead of role switch; one source of truth.
- [ ] Route config: update to /system/* paths and new permission IDs (monitoring.read, analytics.read, audit.read, audit.export, logs.read, system.settings).
- [ ] Analytics page at /system/analytics: call GET /system/analytics when user has analytics.read.
- [ ] Logs page at /system/logs: call GET /system/logs when user has logs.read.
- [ ] Monitor page at /system/monitor: call GET /system/monitoring (and sub-routes) when user has monitoring.read.
Shared:
- [ ] Document “one super_admin” and “Register never allows super_admin” in API and deployment docs.
- [ ] No DB schema change required for “only one super_admin” (policy-only).
10. Summary
| Topic | Decision |
|---|---|
| Permissions | Single list in lib/constants/permissions.ts; namespaces: user., maintenance., department.*, system.settings, monitoring.read, analytics.read, audit.read, audit.export, logs.read. |
| Access control | Permission-only; super_admin is the only exception (always allow). No role-based access for non–super_admin. |
| Super_admin | One in system; Register API/Page never allow role=super_admin; can manage all users and all permissions. |
| Administrator | Full user management except: cannot edit permissions; cannot create administrator or super_admin. |
| Default permissions | Assigned on user create/register; super_admin can change any user’s permissions later. |
| Frontend paths | All SYSTEM section under /system/*: /system/analytics, /system/audit, /system/logs, /system/settings, /system/monitor. Sidebar shows only items the user has permission for (e.g. dev sees SYSTEM but not Audit unless audit.read). |
| Backend paths | /system/analytics, /system/audit (and /system/audit/export), /system/logs, /system/monitoring (monitoring APIs). GET /system/permissions unchanged. Frontend Monitor page is /system/monitor and calls /system/monitoring. |
| Analytics | GET /system/analytics using AnalyticsCacheService; gate with analytics.read. |
| Monitor | Backend GET /system/monitoring; frontend page /system/monitor; monitoring.read for admins and developers. |
| Logs | Backend GET /system/logs; frontend page /system/logs; logs.read for admins and developers. |
This plan is the reference for designing and implementing the new permission-based system.
11. Remaining implementation (next session)
Priority order:
Permission editing API (super_admin only)
- Add PUT (or PATCH) /users/:id/permissions (or PUT /users/:id with a
permissionsfield). - Body:
{ permissions: string[] }— must be a subset of GET /system/permissions. - Guard: super_admin only (e.g.
requireSuperAdmin(c)); no one else can change another user’s permissions. - Validation: reject if any permission is not in SYSTEM_PERMISSIONS.
- If permissions are stored per user: update DB and invalidate caches; if still role-derived only, this API can be “reserved” and implemented when per-user permissions storage exists.
- Add PUT (or PATCH) /users/:id/permissions (or PUT /users/:id with a
Register / user create rules
- Register API and UI: Reject
role === 'super_admin'(400). Allowed roles: administrator, department_head, developer, employee, technician (no super_admin). - User create (admin flow): If actor is administrator (not super_admin): reject target role
administratororsuper_admin; do not accept or persistpermissionsfrom request (only super_admin can set permissions).
- Register API and UI: Reject
Default permissions for new users
- On user create/register, set default permissions (e.g. from a role→permissions map in code or config).
- Ensures GET /me/permissions and session have a consistent list even before any super_admin edit.
Per-user permissions storage (if desired)
- To support “super_admin edits any user’s permissions”, permissions must be stored per user (e.g.
user_permissionstable orpermissionsJSON column on users). - calculatePermissions / GET /me/permissions: if user has stored permissions use them, else fall back to role-based defaults.
- Super_admin can then PUT /users/:id/permissions and they persist.
- To support “super_admin edits any user’s permissions”, permissions must be stored per user (e.g.
Frontend: permission management UI
- For super_admin only: on user detail/edit, show a “Permissions” section: list from GET /system/permissions with checkboxes, save via PUT /users/:id/permissions.
- Hide this section for non–super_admin or when viewing own profile (optional).
Permissions list check (done):
lib/constants/permissions.tsis the single source; GET /system/permissions returns it.lib/services/auth/login/permissions.ts: super_admin gets all SYSTEM_PERMISSIONS; administrator, department_head, developer, employee, technician get the correct role-based lists; technician now has same as employee (maintenance.create, maintenance.read, maintenance.update).- JWT calculatePermissions: super_admin gets SYSTEM_PERMISSIONS; other roles aligned.
- No duplicate or typo in the permission IDs in the constant; extra IDs (user.manage, user.view_stats, user.activate, user.suspend, user.activity) are used by existing user routes.
12. Current state (recent work) — work from here tomorrow
Done (me & users & cache & auth):
- GET /me returns full current-user data in one response:
userId,permissions, profile, settings, security, avatar (and any other needed fields). Single source for “full user on load.” - Frontend uses only GET /me for full user data on page load and sets user (including permissions) from that. GET /me/permissions is used only when explicitly refetching (e.g. after permission change), not on every load.
- Suspend API (
PUT /users/:id/suspend): after suspending, invalidates user sessions (session micro cache), sets DBstatusto'suspended', invalidates user data cache (invalidateUserData), and updates users list cache (updateUserInListwithisSuspended: true,status: 'suspended'). Login (password and OIDC) blocks suspended/inactive with clear messages. - Activate API (
PUT /users/:id/activate): updates DB (isActive: true,isSuspended: false,status: 'active'), then invalidates user data cache and updates users list cache withisActive: true,isSuspended: false,status: 'active'. - Login (password and OIDC): detects suspended/inactive and blocks with clear messages (“Your account has been suspended…” / “Your account is inactive…”).
- GET /users/:id uses user micro cache service (
getUserData) with normal cache/DB fallback instead of reading only from DB. - PUT /users/:id/permissions exists (super_admin); invalidates user data and updates user in list cache after permission change.
Not done yet (continue from here):
- Permission editing UI for super_admin; default permissions on create/register; Register and user create rules (reject super_admin, admin cannot create admin/super_admin or set permissions).
- All new tasks listed in §13 below.
13. New tasks (added for next sessions)
Use this section as the checklist for the next phases. Order is by dependency and impact.
13.1 Frontend: central stable hooks/providers
- Goal: Have central, stable hooks and providers so new pages and libs can plug in easily across the whole frontend.
- Tasks:
- Audit existing providers (Auth, Theme, etc.) and ensure a single pattern (e.g. one auth provider, one place for “current user + permissions”).
- Define a small set of “core” hooks (e.g.
useAuth,usePermissions,useMe) that are the only way to access user/session/permissions so new features don’t duplicate logic. - Document where to add new providers/hooks and how new pages should consume them (e.g. “new page uses useAuth + usePermissions only”).
- Optional: create a thin
@/hooksor@/providersindex that re-exports these so libs and new pages have one import path.
13.2 GET /users and GET /users/:id — advanced cache and predictable errors
- Goal: Make list and single-user APIs robust: stable cache behavior and predictable edge-case errors (e.g. not found, invalid id, cache miss, stale).
- Tasks:
- Document and implement clear error responses for GET /users (e.g. invalid query, 400) and GET /users/:id (404 when user missing, consistent shape).
- Ensure GET /users uses list cache correctly (e.g. pagination, filters, invalidation when list-affecting mutations run).
- Ensure GET /users/:id uses user micro cache (
getUserData) everywhere and that cache invalidation is applied on all user mutations (already partially done; verify and document). - Define “edge cases” (e.g. deleted user, suspended user in list, permission to see sensitive fields) and document expected behavior and status codes.
13.3 User management API: permissions + role-based restrictions
- Goal: All user management APIs rely on permissions for access; use roles only to block giving high-level permissions to lower roles (e.g. admin cannot grant permissions above their own or create super_admin).
- Tasks:
- Ensure every user management route (create, update, delete, suspend, activate, permissions, etc.) is gated by the correct permission (e.g.
user.update,user.activate) and super_admin bypass. - Implement role hierarchy / rules: e.g. when an administrator updates a user, reject if target role is
administratororsuper_admin; reject or ignorepermissionsin body (only super_admin can set permissions). - When implementing “default permissions by role,” ensure lower roles cannot receive permission sets that exceed what their role allows (e.g. employee cannot be granted
user.deleteunless policy says so); document the matrix (role × allowed permissions).
- Ensure every user management route (create, update, delete, suspend, activate, permissions, etc.) is gated by the correct permission (e.g.
13.4 Cache invalidation on all user-related DB changes
- Goal: Both users list cache and user:id (single user) cache are updated on every DB-level change to the user (profile, settings, security, or new fields added later).
- Tasks:
- List all mutation points: user table (profile, status, role, department), user settings table, user security table, permissions, avatar, etc.
- At each mutation, after successful DB write: call
invalidateUserData(userId)and, where the change affects list view (e.g. name, role, department, status), callupdateUserInList(userId, patch)with the updated fields. - Add a short “cache invalidation” checklist in the plan or in code comments: “When you add a new user-related field or table, invalidate user cache and update list if visible.”
- Option: central helper e.g.
afterUserMutated(userId, listPatch?)used by all user mutations to avoid missing invalidation.
13.5 Super_admin: count, restrictions, and self-edit prevention
- Goal: Clarify how many super_admins can exist, any restrictions between super_admins in user management, and ensure a user cannot edit/change themselves in the user management UI/API (e.g. change role, permissions, or critical fields for their own userId).
- Tasks:
- Count: Document or enforce “single super_admin” vs “multiple super_admins allowed” (current plan says “one by policy”; if multiple allowed, document any restrictions).
- Restrictions between super_admins: Can one super_admin suspend/activate/delete another? Can they change another super_admin’s permissions? Define and implement (e.g. “super_admin cannot edit another super_admin” or “only self cannot be edited”).
- Self-edit prevention: In user management API and frontend: compare
userIdfrom GET /me (current user) with the target user id (from list or /users/:id). Reject (403 or 400) any mutation that would change the current user’s own record in user management (e.g. change own role, permissions, suspend self, delete self). Frontend: disable or hide “Edit”/“Suspend”/“Permissions” for the row whereuser.id === me.userId.
13.6 Department tie: DB-level enforcement by role
- Goal: Users with roles department_head and employee are tied to a department (required); super_admin, admin, technician, and developer are not tied (department optional or null). Enforce at DB and API level.
- Tasks:
- DB: add constraint or validation so that for
role IN ('department_head', 'employee'),departmentIdis NOT NULL (and optionally FK to departments). For other roles,departmentIdmay be null. - API: on user create/update and register, validate departmentId when role is department_head or employee; reject with 400 if missing or invalid. When role is super_admin, admin, technician, developer, allow null departmentId (or ignore department for those roles).
- Frontend: when creating/editing a user with role department_head or employee, require department selection; for other roles, department can be optional.
- DB: add constraint or validation so that for
13.7 Audit logs and internal logs — coverage and patterns
- Goal: Add audit logs (and internal logs where useful) at all important places so admins and devs can observe system actions. Provide examples and patterns to follow later.
- Tasks:
- Audit logs: Identify all “important” actions (user create/update/delete/suspend/activate, permission change, login/logout, password change, 2FA change, maintenance lifecycle, system settings change, etc.). Ensure each calls the audit service with a consistent shape (actor, action, target, timestamp, optional metadata). Add missing audit calls at these points.
- Internal logs: Add internal logging (e.g. structured logs or internal logger) at critical paths: auth failures, cache misses, high-latency requests, errors. Use a consistent pattern (e.g.
internalLogger.info('auth.login.failed', { userId, reason })). - Document patterns: Add a short “Logging and audit” section or file with 2–3 example snippets: (1) how to log an audit event after a mutation, (2) how to log an internal event for debugging/observability, (3) when to use audit vs internal log. This makes it easy to follow the same pattern when adding new features.
13.8 Internal logs: long-term storage and value
- Goal: Rethink where internal logs are stored and for how long, and whether they “matter” long-term or are best-effort/short-lived.
- Tasks:
- Current state: Document where internal logs go today (e.g. DO cache, in-memory, stdout, external service). Define TTL or retention (e.g. 7 days in cache, then discard).
- Long-term vs short-term: Decide if internal logs should ever be stored long-term (e.g. object storage, log aggregation service) for compliance or debugging, or if they are “operational only” and can be ephemeral. If long-term: define storage, retention, and access (e.g. logs.read only for recent; export for compliance).
- Value: If they “won’t matter” long-term, document that explicitly and keep implementation simple (e.g. in-memory/DO with short TTL, no persistence). If they do matter, add a small “Internal log retention” subsection to the plan with storage and retention decisions.