DC-048: multi-user bootstrap + admin invites (opt-in)
Implements the user-store + invite-store + admin routes. The whole system is opt-in via siteConfig.authProviders.email.enabled = true; single-user TOTP-only installs see zero behavior change. Backend: - src/security/user-store.js: users + allowlist + bootstrap sentinel, atomic writes, last-admin protection, defensive dataDir resolver. - src/security/invite-store.js: single-use tokens (SHA-256 hashed on disk), TTL, auto-prune, defensive dataDir resolver. - routes/auth/admin.js: /me, /admin/users (CRUD), /admin/allowlist, /admin/invites (CRUD), public /invites/:token (peek + accept). - routes/auth/index.js: wires userStore, gates admin router on email auth being enabled. - src/auth/providers/email.js: verify() enforces allowlist, creates user record, tags req.user; default-enabled flipped to opt-in. - src/auth/providers/totp.js: bootstraps system@totp.local admin on first verify so current DNS2 operator shows in /admin/users. - src/security/audit-logger.js: middleware adds userId/userEmail/ userRole/viaProvider to log details when req.user is tagged. - PUBLIC_ROUTES + CSRF allowlists updated for invite redemption. Frontend: - status/js/admin.js: modal overlay with users list (role-edit, delete), invite form (email/role/TTL), copy-link button, outstanding-invites list with revoke. Exports window.AdminPanel. - status/js/core/init.js: calls AdminPanel.attachTrigger so the Admin button only appears when /me returns isAdmin=true. Tests: 35 new tests across 3 files (user-store, invite-store, auth multistore integration). Full suite: 1298/1298 passing. Docs: BACKLOG.md marks DC-048 done. CHANGELOG.md [Unreleased] section gets the DC-048 entry.
This commit is contained in:
@@ -288,9 +288,20 @@ describe('Public-routes allowlist drift (prevents DC-012-style dead entries)', (
|
||||
|
||||
describe('No stale PUBLIC_ROUTES entries (the DC-012 failure mode)', () => {
|
||||
test('every PUBLIC_ROUTES entry matches an actual mounted route', () => {
|
||||
// DC-048: invite routes are only mounted when the operator has
|
||||
// enabled email auth (siteConfig.authProviders.email.enabled === true).
|
||||
// The aggregator factory gates this on a non-proxied config flag, so
|
||||
// the router walker in this test (which runs with stub deps) doesn't
|
||||
// see them mounted. They're not stale — they're conditional. Same
|
||||
// for any future provider-conditional mount.
|
||||
const conditionalMounts = new Set([
|
||||
'/api/v1/auth/invites/:token',
|
||||
'/api/v1/auth/invites/:token/accept',
|
||||
]);
|
||||
const stale = [];
|
||||
for (const entry of publicRoutes) {
|
||||
if (entry.endsWith('/')) continue; // prefix matches, skip
|
||||
if (conditionalMounts.has(entry)) continue; // gated by config flag
|
||||
if (!mountedRoutes.has(entry)) stale.push(entry);
|
||||
}
|
||||
expect(stale).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user