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.
153 lines
6.2 KiB
JavaScript
153 lines
6.2 KiB
JavaScript
/**
|
|
* DC-093: /api/v1/auth/me must ALWAYS exist.
|
|
*
|
|
* Regression guard for the single-user-install 404 storm: the frontend
|
|
* admin panel (status/js/admin.js attachTrigger) polls /api/v1/auth/me on
|
|
* every dashboard load and re-probes every 60s while unauthenticated.
|
|
* The /me handler used to live exclusively in the DC-048 admin router,
|
|
* which is only mounted when email auth (multi-user) is enabled — so every
|
|
* single-user install answered 404 and the API logged a full ERROR +
|
|
* stack trace once per minute per open browser tab.
|
|
*
|
|
* These tests verify the routes/auth/index.js factory (the full aggregator,
|
|
* real sub-routers, stubbed services):
|
|
* 1. GET /auth/me route EXISTS in single-user mode (no email auth)
|
|
* 2. single-user response: mode='single', isAdmin=true, legacy=true
|
|
* 3. multi-user + req.user: mode='multi', stored profile returned
|
|
* 4. multi-user + legacy session (no req.user): legacy branch
|
|
* 5. /auth/me is NOT in PUBLIC_ROUTES (session-gated — unauthenticated
|
|
* probes must 401 at the middleware, never reach the handler)
|
|
* 6. admin routes (/auth/admin/users) still mounted ONLY in multi-user
|
|
*/
|
|
|
|
describe('DC-093: /auth/me always mounted (routes/auth/index.js)', () => {
|
|
function makeCtx(siteConfig, dataDir) {
|
|
return {
|
|
siteConfig,
|
|
asyncHandler: (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next),
|
|
errorResponse: (res, code, msg) => res.status(code).json({ success: false, error: msg }),
|
|
log: { info() {}, warn() {}, error() {}, debug() {} },
|
|
session: { isSessionValid: () => true },
|
|
licenseManager: {
|
|
requirePremium: () => (req, res, next) => next(),
|
|
hasFeature: () => true,
|
|
},
|
|
platformPaths: { dataDir },
|
|
};
|
|
}
|
|
|
|
function tmpDir() {
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'dc093-me-'));
|
|
}
|
|
|
|
function findRoute(router, routePath, method) {
|
|
const layer = router.stack.find(
|
|
(l) => l.route && l.route.path === routePath && l.route.methods[method]
|
|
);
|
|
return layer || null;
|
|
}
|
|
|
|
function invoke(layer, req) {
|
|
return new Promise((resolve) => {
|
|
const res = {
|
|
_status: 200,
|
|
_body: null,
|
|
status(c) { this._status = c; return this; },
|
|
json(j) { this._body = j; resolve(this); return this; },
|
|
setHeader() {},
|
|
};
|
|
const fn = layer.route.stack[0].handle;
|
|
Promise.resolve(fn(req, res, () => resolve(res)));
|
|
});
|
|
}
|
|
|
|
let factory;
|
|
beforeAll(() => {
|
|
factory = require('../../routes/auth/index');
|
|
});
|
|
|
|
test('single-user mode: /auth/me route exists and reports mode=single, isAdmin=true', async () => {
|
|
const dir = tmpDir();
|
|
const router = factory(makeCtx({}, dir));
|
|
const layer = findRoute(router, '/auth/me', 'get');
|
|
expect(layer).toBeTruthy();
|
|
const res = await invoke(layer, { user: undefined });
|
|
expect(res._status).toBe(200);
|
|
expect(res._body).toMatchObject({
|
|
success: true,
|
|
user: null,
|
|
authenticated: true,
|
|
role: 'admin',
|
|
isAdmin: true,
|
|
legacy: true,
|
|
mode: 'single',
|
|
});
|
|
});
|
|
|
|
test('multi-user mode with req.user: /auth/me returns stored profile, mode=multi', async () => {
|
|
const dir = tmpDir();
|
|
const userStore = require('../../src/security/user-store').createUserStore({ dataDir: dir });
|
|
await userStore.login({ email: 'admin@x.com' });
|
|
const ctx = makeCtx({ authProviders: { email: { enabled: true } } }, dir);
|
|
// Attach the same store the factory builds — deterministic id resolution
|
|
const router = factory(ctx);
|
|
const layer = findRoute(router, '/auth/me', 'get');
|
|
expect(layer).toBeTruthy();
|
|
const users = await ctx.userStore.listUsers();
|
|
const admin = users.find((u) => u.role === 'admin') || users[0];
|
|
const res = await invoke(layer, { user: { id: admin.id, role: admin.role } });
|
|
expect(res._status).toBe(200);
|
|
expect(res._body.mode).toBe('multi');
|
|
expect(res._body.user).toMatchObject({ id: admin.id, email: 'admin@x.com', isAdmin: true });
|
|
expect(res._body.legacy).toBeUndefined();
|
|
});
|
|
|
|
test('multi-user mode, legacy session (no req.user): /auth/me falls back to legacy admin', async () => {
|
|
const dir = tmpDir();
|
|
const router = factory(makeCtx({ authProviders: { email: { enabled: true } } }, dir));
|
|
const layer = findRoute(router, '/auth/me', 'get');
|
|
const res = await invoke(layer, { user: undefined });
|
|
expect(res._body).toMatchObject({ mode: 'single', role: 'admin', legacy: true });
|
|
});
|
|
|
|
test('/auth/me is NOT in PUBLIC_ROUTES (stays session-gated)', () => {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const mw = fs.readFileSync(
|
|
path.join(__dirname, '..', '..', 'src', 'utilities', 'middleware.js'),
|
|
'utf8'
|
|
);
|
|
expect(/['"]\/api\/v1\/auth\/me['"]/.test(mw)).toBe(false);
|
|
});
|
|
|
|
test('admin router still mounted ONLY in multi-user mode (DC-048 invariant preserved)', () => {
|
|
const single = factory(makeCtx({}, tmpDir()));
|
|
const multi = factory(makeCtx({ authProviders: { email: { enabled: true } } }, tmpDir()));
|
|
const hasAdminMount = (router) =>
|
|
router.stack.some(
|
|
(l) => l.name === 'router' && l.handle && l.handle.stack &&
|
|
l.handle.stack.some((s) => s.route && /^\/admin\//.test(s.route.path))
|
|
);
|
|
expect(hasAdminMount(single)).toBe(false);
|
|
expect(hasAdminMount(multi)).toBe(true);
|
|
});
|
|
|
|
// Judge polish (DC-093 round 1): HTTP-level proof that the route is
|
|
// REACHABLE through real Express dispatch — not merely present in the
|
|
// router stack. Guards against a future mount-order/shadowing change
|
|
// (e.g. an earlier router.use swallowing /auth/*) silently re-404ing
|
|
// the endpoint while the layer-walk tests above keep passing.
|
|
test('HTTP-level: GET /api/v1/auth/me is reachable through real Express dispatch (single-user)', async () => {
|
|
const request = require('supertest');
|
|
const express = require('express');
|
|
const app = express();
|
|
app.use('/api/v1', factory(makeCtx({}, tmpDir())));
|
|
const res = await request(app).get('/api/v1/auth/me');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toMatchObject({ success: true, mode: 'single', isAdmin: true });
|
|
});
|
|
});
|