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:
@@ -4,7 +4,9 @@ const initKeys = require('./keys');
|
||||
const initSessionHandlers = require('./session-handlers');
|
||||
const initSsoGate = require('./sso-gate');
|
||||
const initLogin = require('./login');
|
||||
const initAdmin = require('./admin');
|
||||
const { createAuthProviderRegistry } = require('../../src/auth/providers');
|
||||
const { createUserStore } = require('../../src/security/user-store');
|
||||
|
||||
/**
|
||||
* Auth routes aggregator
|
||||
@@ -39,6 +41,32 @@ function _extractEmailConfig(ctx) {
|
||||
module.exports = function(ctx) {
|
||||
const router = express.Router();
|
||||
|
||||
// DC-048: opt-in user store. Only instantiated when the operator has
|
||||
// explicitly enabled email auth in siteConfig. The default for new
|
||||
// installs is "no user-store, no allowlist, no admin invites" — the
|
||||
// legacy single-user TOTP flow. Operators who turn email auth on
|
||||
// (siteConfig.authProviders.email.enabled = true) opt into multi-user.
|
||||
// Once opted in, the first email to log in is the bootstrap admin.
|
||||
const platformPaths = ctx.platformPaths || require('../../platform-paths');
|
||||
let userStore = null;
|
||||
|
||||
const _emailExplicitlyEnabled =
|
||||
ctx.siteConfig &&
|
||||
ctx.siteConfig.authProviders &&
|
||||
ctx.siteConfig.authProviders.email &&
|
||||
ctx.siteConfig.authProviders.email.enabled === true;
|
||||
|
||||
if (_emailExplicitlyEnabled) {
|
||||
userStore = createUserStore({
|
||||
dataDir: platformPaths.dataDir,
|
||||
log: ctx.log,
|
||||
});
|
||||
ctx.userStore = userStore;
|
||||
ctx.log && ctx.log.info && ctx.log.info('user', 'multi-user mode enabled (email auth on)');
|
||||
} else {
|
||||
ctx.log && ctx.log.info && ctx.log.info('user', 'single-user mode (email auth not enabled — set siteConfig.authProviders.email.enabled = true to opt into multi-user)');
|
||||
}
|
||||
|
||||
// Extract dependencies from context
|
||||
const deps = {
|
||||
authManager: ctx.authManager,
|
||||
@@ -62,7 +90,11 @@ module.exports = function(ctx) {
|
||||
notificationManager: ctx.notification,
|
||||
siteConfig: ctx.siteConfig,
|
||||
// DC-047: data-directory resolution for the email-token JSON store.
|
||||
platformPaths: ctx.platformPaths || null,
|
||||
platformPaths,
|
||||
// DC-048: user store for allowlist + bootstrap. Null when email
|
||||
// auth is disabled — providers fall back to "allow everyone" legacy
|
||||
// behavior (DC-046/047 semantics).
|
||||
userStore,
|
||||
};
|
||||
|
||||
const { getAppSession, appSessionCache } = initSessionHandlers(deps);
|
||||
@@ -75,7 +107,7 @@ module.exports = function(ctx) {
|
||||
credentialManager: ctx.credentialManager,
|
||||
session: ctx.session,
|
||||
saveTotpConfig: ctx.saveTotpConfig,
|
||||
config: { totp: ctx.totpConfig, email: ctx.emailProviderConfig || { enabled: true } },
|
||||
config: { totp: ctx.totpConfig, email: ctx.emailProviderConfig || { enabled: false } },
|
||||
log: ctx.log,
|
||||
renewCSRFToken: ctx.middlewareResult?.renewCSRFToken,
|
||||
// DC-047: EmailMagicLinkProvider needs SMTP config + a public URL
|
||||
@@ -84,6 +116,9 @@ module.exports = function(ctx) {
|
||||
emailConfig: _extractEmailConfig(ctx),
|
||||
siteConfig: ctx.siteConfig || {},
|
||||
platformPaths: deps.platformPaths,
|
||||
// DC-048: user store shared by every provider for allowlist checks
|
||||
// and the bootstrap-admin-on-first-login rule.
|
||||
userStore: deps.userStore,
|
||||
},
|
||||
ctx.siteConfig
|
||||
);
|
||||
@@ -106,5 +141,18 @@ module.exports = function(ctx) {
|
||||
router.use(initKeys(deps));
|
||||
router.use(initSsoGate({ ...deps, getAppSession, appSessionCache }));
|
||||
|
||||
// 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.
|
||||
if (userStore) {
|
||||
router.use('/auth', initAdmin({
|
||||
asyncHandler: ctx.asyncHandler,
|
||||
errorResponse: ctx.errorResponse,
|
||||
log: ctx.log,
|
||||
session: ctx.session,
|
||||
}));
|
||||
}
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user