docs/multi-user-plan.md lists the system operations as admin-only, but
these routes had no role check, so any signed-in regular user could run
them in multi-user mode:
- POST /api/auth/revoke with an empty body cleared EVERY login session on
the server, every admin's included. A non-admin now revokes only their
own logins, and a named sessionToken only when it is one of theirs.
- POST /api/system/update (update and restart the server for everyone),
POST /api/system/span-displays (a window on the server's desktop),
POST /api/tunnel/qr/regenerate (clears every user's pending QR login
code) and POST /api/cleanup-state (drops every user's stale session
records) now answer 403 FORBIDDEN to a non-admin.
- GET /api/tunnel/info lists only a non-admin's own logins (it showed the
address and browser of every user's sessions).
Admins and single-user mode are unchanged. The tunnel panel's Regenerate
QR button no longer reports success for a refused request.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GET /api/settings returned the whole settings.json to every signed-in user.
An admin's App Settings save stores the Deepgram key there
(voiceSettings.apiKey), so any regular user could read it, and their
browser copied it into its own voice config and dictated on the admin's
account. caseOrder and lastUsedCase also named the admin's cases.
In multi-user mode a non-admin now reads the file without those three
(settingsForNonAdmin). Admins and single-user mode get the file as it
is. Pinned by route tests and the live multi-user test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- test/routes/system-routes-settings-admin-only.test.ts (app.inject): a
non-admin PUT /api/settings is 403 FORBIDDEN before the body is parsed, and
nothing is written, no watcher toggled, no tunnel started, no history limit
set; the same gate on PUT /api/execution/model-config, PUT /api/config and
PUT /api/cases/order; admins and single-user mode write as before; GET stays
readable.
- test/settings-admin-only-multiuser.test.ts (live server, port 0, real
multi-user auth): settings.json is byte-identical after a non-admin's PUTs,
and a non-admin's quick-start or Ralph loop no longer records lastUsedCase.
- test/settings-admin-only-ui.test.ts (real index.html in JSDOM): a non-admin
App Settings save sends no PUT, keeps per-device keys, keeps synced keys at
the server value, and toasts once only on a real synced change; the
background writes are skipped; admins still PUT; zh-CN for both toasts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>