Eleven routers are mounted on '/api' and called router.use(authMiddleware) with
no path. Mounted that way, the gate applies to every /api request that reaches
the router — including routes belonging to routers mounted further down
server.js. extensions.js did it from line 295; the assistant is mounted at 305.
So a signed-out request to /api/clinical-assistant/status was refused ten lines
before the preview middleware could look at it, whatever the admin setting said.
server.js line 250 already warned about this shape.
Each gate now names its own prefix, so a router protects its own routes and
nothing else. Verified afterwards that every namespace which must stay shut
still answers 401 signed out: extensions, encounters, memories, notes, diagrams,
generated images, image jobs, documents, audio backups, ED encounters,
don't-miss, patient education, billing, well visit, admin, transcribe and the
rest. Two of these routers were gating routes nobody realised they were gating.
Second defect in the same path: authMiddleware only ever looks for a token, so
calling it unconditionally after the preview identity had been assigned rejected
exactly the requests preview exists to serve. Only that identity may skip it;
authMiddleware stays strict everywhere else.
Preview now answers with a real cited answer, and stays as narrow as it was
designed to be — four allow-listed paths, no identity, nothing ownable. A test
now walks every /api router and fails on a blanket gate, which is how the last
six were found.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
New top-level tab positioned after Physical Exam Guide. Per-user
directory of hospital phone extensions and pagers — grouped by location
then type, searchable, soft-deleted.
Data:
- New table user_phone_extensions (id, user_id, location, name, number,
type CHECK (extension|pager), notes, trashed_at, timestamps).
Partial indexes on active vs trashed rows for fast filtering.
- Not PHI — hospital internal phone directory. Plaintext.
API (all user-scoped, all params validated):
- GET /api/extensions?trash=1&q=text — list active or trash, optional search
- POST /api/extensions — create
- PUT /api/extensions/:id — update (requires all three core fields)
- DELETE /api/extensions/:id — soft-delete (sets trashed_at)
- POST /api/extensions/:id/restore — un-trash
- DELETE /api/extensions/:id/purge — hard-delete (only if trashed)
All :id params parsed + validated (positive integer) before query.
All queries parameterized, every WHERE includes user_id scoping.
UI (public/js/extensions.js + components/extensions.html):
- Search bar with 200ms debounce, server-side LIKE on location/name/number/notes
- Add button expands inline form — location (with datalist of existing
locations for autocomplete), name/dept, number, type, optional notes
- Each entry renders as a card: big monospace number, dept, type badge,
edit + delete inline
- Grouped by location → type (Extensions / Pagers subheaders)
- Trash view: toggle shows trashed items with Restore + Purge actions
- Trash count badge on the Trash button updates after every delete/restore
- Delete requires confirm() dialog, then soft-delete (easy to undo)
- Purge from trash requires a second confirm() ("cannot be undone")
- Esc closes the form; form resets between Add and Edit