From 86725b6f40bd850e657d8a46c61c7f333c9b3fad Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 23 Mar 2026 03:19:03 -0400 Subject: [PATCH] Add named admins design spec Covers multi-key admin config, per-admin localStorage preferences, and real-time presence badges via WebSocket. Made-with: Cursor --- .../specs/2026-03-23-named-admins-design.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-23-named-admins-design.md diff --git a/docs/superpowers/specs/2026-03-23-named-admins-design.md b/docs/superpowers/specs/2026-03-23-named-admins-design.md new file mode 100644 index 0000000..83d1f60 --- /dev/null +++ b/docs/superpowers/specs/2026-03-23-named-admins-design.md @@ -0,0 +1,156 @@ +# Named Admins Design + +**Date:** 2026-03-23 +**Status:** Approved + +## Overview + +Replace the single shared `ADMIN_KEY` with named admin accounts, each with their own key. Per-admin preferences are stored in namespaced `localStorage`. A real-time "who is watching" presence bar shows which admins are on the same page. + +## Requirements + +1. **Named admins with multiple keys** — each admin has a name and a unique key, defined in a server-side config file. +2. **Per-admin preferences** — UI preferences (saved filter view, show limit, etc.) are linked to the admin who set them via namespaced `localStorage`. +3. **Presence badge** — a "who is watching" card in the page header shows which admins are viewing the same page. The current admin sees "me" for themselves, full names for others. + +## Approach + +Identity-in-JWT with a config file. No new database tables. Preferences stay in `localStorage` (per-browser, which is desirable). Presence piggybacks on the existing WebSocket infrastructure. Falls back to the old `ADMIN_KEY` env var for backward compatibility. + +--- + +## Section 1: Admin Configuration + +### Config file + +- **`backend/config/admins.example.json`** — committed to repo, shows the expected shape: + ```json + [ + { "name": "Alice", "key": "change-me-alice-key" }, + { "name": "Bob", "key": "change-me-bob-key" } + ] + ``` +- **`backend/config/admins.json`** — gitignored, contains real keys. + +### Loader module + +**New file: `backend/config/load-admins.js`** + +Startup behavior: +1. Read path from `ADMIN_CONFIG_PATH` env var, or default to `backend/config/admins.json`. +2. If the file exists: parse and validate (must be an array of `{ name, key }`, no duplicate names or keys). +3. If the file does not exist: fall back to `ADMIN_KEY` env var → `[{ name: "Admin", key: ADMIN_KEY }]`. +4. If neither exists: throw at startup (fail-fast, same as current behavior). + +Exports: +- `findAdminByKey(key)` — returns `{ name }` or `null`. + +--- + +## Section 2: Authentication Changes (Backend) + +### Login endpoint (`backend/routes/auth.js`) + +- Replace single `ADMIN_KEY` comparison with `findAdminByKey(key)`. +- On match, embed the admin name in the JWT payload: `{ role: 'admin', name: 'Alice', timestamp: Date.now() }`. +- Add `name` to the login response JSON: `{ token, name, message, expiresIn }`. +- Startup guard changes from checking `ADMIN_KEY` to checking that `loadAdmins()` returned at least one admin. + +### Verify endpoint + +No changes needed. Already returns `{ valid: true, user: req.user }`. The decoded JWT now naturally includes `name`, so the frontend gets it for free. + +### Auth middleware (`backend/middleware/auth.js`) + +No changes. Already decodes the full JWT payload into `req.user`. Routes that need the admin name can read `req.user.name`. + +--- + +## Section 3: Frontend Auth & Per-Admin Preferences + +### AuthContext changes (`frontend/src/context/AuthContext.jsx`) + +- Add `adminName` to state (alongside `token`, `isAuthenticated`). +- `login()`: read `name` from the login response JSON. Store in state and `localStorage` as `adminName`. +- `verify()`: read `name` from `response.data.user.name`. Restore `adminName` from that. +- `logout()`: clear `adminName` from state and `localStorage`. +- Expose `adminName` from the context. + +### Preference namespacing + +A utility function (e.g. in `frontend/src/utils/adminPrefs.js`): +- `prefixKey(adminName, key)` → returns `${adminName}:${key}` when `adminName` is set, or plain `key` as fallback. +- History page changes from `localStorage.getItem('history-filter')` → `localStorage.getItem(prefixKey(adminName, 'history-filter'))`. + +### One-time migration + +On login, if old un-namespaced keys exist (e.g. `history-filter`), copy them to the namespaced version (`alice:history-filter`) and delete the originals. This preserves existing preferences for the first admin who logs in after the upgrade. + +--- + +## Section 4: Presence System — "Who Is Watching" + +### WebSocket changes (`backend/utils/websocket-manager.js`) + +- On `auth` message: JWT now carries `name`. Store as `clientInfo.adminName` instead of `clientInfo.userId = decoded.role`. +- New message type `page_focus`: clients send `{ type: 'page_focus', page: '/history' }` on navigation. Stored as `clientInfo.currentPage`. +- On `page_focus` and on `removeClient` (disconnect): broadcast `presence_update` to all authenticated clients. +- Presence payload: `{ type: 'presence_update', admins: [{ name: 'Alice', page: '/history' }, { name: 'Bob', page: '/picker' }] }`. +- Unauthenticated clients do not participate in presence. + +### Message flow + +1. Admin connects → sends `{ type: 'auth', token }` → server stores `adminName` from JWT. +2. Frontend route change → sends `{ type: 'page_focus', page: '/history' }` → server stores page, broadcasts presence. +3. Admin disconnects → server removes them, broadcasts updated presence. +4. Each client receives the full presence list and filters locally to admins on the same page. + +### Frontend hook — `usePresence` + +- Connects to the existing WebSocket, sends `page_focus` on route changes (via `useLocation`). +- Listens for `presence_update` messages. +- Filters to admins on the current page. +- Returns `{ viewers }` — array of names, with the current admin's name replaced by `"me"`. + +### Frontend component — `PresenceBar` + +- Renders below the `