[glm-grade=B] fix(auth): mount /auth/me on single-user installs — kill the 60s 404 log storm (DC-093)
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

The frontend admin panel polls GET /api/v1/auth/me on every dashboard
load and every 60s per open tab. /me lived only in the DC-048 admin
router, mounted only when email auth is enabled — so every single-user
install (the default) answered 404 and logged a DC-404 ERROR + stack
once per minute per open tab. Verified live in production logs.

- routes/auth/index.js: /auth/me now ALWAYS mounted. Multi-user +
  req.user returns the stored profile (mode:'multi'); otherwise the
  legacy single-operator response (role:'admin', isAdmin:true,
  legacy:true, mode:'single'). Session-gated — NOT added to
  PUBLIC_ROUTES, so unauthenticated polls get a clean 401.
- Frontend behavior unchanged: attachTrigger requires me.user.role
  === 'admin', and user stays null in single-user mode, so no Admin
  button appears on single-user installs.
- openapi.yaml /api/v1/auth/me (200/401) now matches reality.
- +6 tests (route present both modes, response shapes, PUBLIC_ROUTES
  absence, DC-048 admin-mount invariant, HTTP-level dispatch reach).

Judge: GLM-5.3 cold read, grade B ship (verdict URN recorded in
STATE.md); polish note (HTTP-level mount-order test) folded in same
commit. Full suite 115 suites / 2687 tests green.
This commit is contained in:
Hermes
2026-08-22 18:54:57 -07:00
parent 7e4ee60dcf
commit 4125d7a4e1
2 changed files with 197 additions and 3 deletions
+45 -3
View File
@@ -7,6 +7,7 @@ const initLogin = require('./login');
const initAdmin = require('./admin');
const { createAuthProviderRegistry } = require('../../src/auth/providers');
const { createUserStore } = require('../../src/security/user-store');
const { ok } = require('../../src/utils/responses');
/**
* Auth routes aggregator
@@ -144,10 +145,51 @@ module.exports = function(ctx) {
router.use(initKeys(deps));
router.use(initSsoGate({ ...deps, getAppSession, appSessionCache }));
// DC-093: /auth/me is ALWAYS mounted — even on single-user installs where
// the rest of the admin router is not. The frontend admin panel polls
// /api/v1/auth/me on every dashboard load (and re-probes every 60s while
// unauthenticated), so leaving the route unmounted meant every single-user
// install logged a DC-404 ERROR + stack at 1/min per open tab — for years
// of tab-time. The response mirrors the mounted /me shape (routes/auth/
// admin.js) and adds `mode` so clients can distinguish "multi-user with
// this identity" from "single-user install" without guessing from a 404.
// It sits behind the standard session middleware (NOT in PUBLIC_ROUTES),
// so unauthenticated probes get a clean 401, never this handler.
router.get('/auth/me', deps.asyncHandler(async (req, res) => {
if (userStore && req.user && req.user.id) {
const stored = await userStore.getUser(req.user.id);
return ok(res, {
user: stored
? {
id: stored.id,
email: stored.email,
displayName: stored.displayName,
role: stored.role,
isAdmin: stored.role === 'admin',
createdAt: stored.createdAt,
lastLoginAt: stored.lastLoginAt,
loginCount: stored.loginCount,
}
: null,
authenticated: deps.session ? deps.session.isSessionValid(req) : true,
mode: 'multi',
});
}
// No user store mounted → single-user install. The operator who
// unlocked TOTP IS the admin (there is no other identity).
return ok(res, {
user: null,
authenticated: deps.session ? deps.session.isSessionValid(req) : true,
role: 'admin',
isAdmin: true,
legacy: true,
mode: 'single',
});
}, 'auth-me-mode'));
// DC-048: mount admin routes ONLY when the user-store was instantiated
// (i.e. email auth is enabled). Single-user installs don't see /me,
// /admin/*, or /invites/* at all. The route paths simply don't exist
// so a request to /api/v1/auth/me returns 404 from the apiRouter.
// (i.e. email auth is enabled). Single-user installs don't see
// /admin/* or /invites/* at all — /me above is the one exception.
if (userStore) {
// DC-052: pass licenseManager + userStore through so the tier-gate
// middleware can read them. Both are optional — the gate short-