feat(multiuser): phase 5b, frontend (identity boot + admin panel)

- public/admin-ui.js (new, self-contained): on boot fetches GET /api/me and
  stores window.__codemanUser; installs a fetch interceptor that opens a
  change-password modal on any 403 PASSWORD_CHANGE_REQUIRED (and on boot when
  mustChangePassword is set); for a multi-user admin, injects a "Users" tab into
  the existing App Settings modal (create/reset/disable/enable/promote/demote/
  grant-bypass/delete with typed confirm + one-time-password reveal). No header
  button, so the mobile-header policy stays green; nothing renders in single-user
  mode.
- me-routes: GET /api/me returns a `multiUser` flag so the UI distinguishes a
  single-user admin (no admin UI) from a multi-user admin.
- index.html: load admin-ui.js after settings-ui.js, before session-ui.js.

Tests: test/admin-ui.test.ts (JSDOM: identity boot, Users-tab injection gating by
role/mode, forced change-password modal, script-order wiring). Backend verified
end-to-end by test/admin-routes.test.ts against a live server. A full Playwright
pass is recommended before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-07-20 04:31:30 +02:00
parent dafe3de185
commit ac6fe6ef79
4 changed files with 349 additions and 2 deletions
+5 -2
View File
@@ -26,10 +26,12 @@ const PasswordChangeSchema = z.object({
});
export function registerMeRoutes(app: FastifyInstance, ctx: AuthPort): void {
// GET /api/me — identity probe. Synthetic admin in single-user mode.
// GET /api/me — identity probe. Synthetic admin in single-user mode. The
// `multiUser` flag lets the frontend distinguish a single-user admin (no admin
// UI) from a real multi-user admin.
app.get('/api/me', async (req) => {
if (!isMultiUserMode()) {
return { success: true, data: { username: 'admin', role: 'admin', mustChangePassword: false } };
return { success: true, data: { username: 'admin', role: 'admin', mustChangePassword: false, multiUser: false } };
}
const user = getAuthUser(req);
const record = await findUser(user.username);
@@ -39,6 +41,7 @@ export function registerMeRoutes(app: FastifyInstance, ctx: AuthPort): void {
username: user.username,
role: user.role,
mustChangePassword: !!record?.mustChangePassword,
multiUser: true,
},
};
});