const express = require('express'); const initTotp = require('./totp'); 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'); const { ok } = require('../../src/utils/responses'); /** * Auth routes aggregator * Assembles all auth sub-routes with their dependencies * @param {Object} ctx - Application context (for backward compatibility) * @returns {express.Router} */ /** * Pull the SMTP/email provider config from whichever source has it. * * Resolution order: * 1. ctx.emailProviderConfig — explicit override (operator or env) * 2. ctx.notification.getConfig?.().providers.email — reuse the same * SMTP settings notifications use. This is the "magic" — operators * configure SMTP once for system notifications and email-auth picks * it up automatically. * 3. null — provider will operate in dev-console fallback mode. */ function _extractEmailConfig(ctx) { if (ctx.emailProviderConfig && typeof ctx.emailProviderConfig === 'object') { return ctx.emailProviderConfig; } const n = ctx.notification; if (n && typeof n.getConfig === 'function') { const cfg = n.getConfig(); if (cfg && cfg.providers && cfg.providers.email) return cfg.providers.email; } return null; } 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, credentialManager: ctx.credentialManager, totpConfig: ctx.totpConfig, saveTotpConfig: ctx.saveTotpConfig, session: ctx.session, asyncHandler: ctx.asyncHandler, errorResponse: ctx.errorResponse, log: ctx.log, // Additional deps for sso-gate fetchT: ctx.fetchT, getServiceById: ctx.getServiceById, licenseManager: ctx.licenseManager, servicesStateManager: ctx.servicesStateManager, renewCSRFToken: ctx.middlewareResult?.renewCSRFToken, // For DC-046 pluggable auth providers (EmailMagicLink, OIDC, …). // Pass-through — providers like the EmailMagicLinkProvider need // notificationManager for SMTP delivery, plus the siteConfig for // building verification links. notificationManager: ctx.notification, siteConfig: ctx.siteConfig, // DC-047: data-directory resolution for the email-token JSON store. 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); // DC-046: pluggable auth provider registry. The TOTP provider is wired // here against the existing totpConfig / saveTotpConfig objects so it // behaves identically to the legacy /api/v1/totp/* routes mounted below. const registry = createAuthProviderRegistry( { credentialManager: ctx.credentialManager, session: ctx.session, saveTotpConfig: ctx.saveTotpConfig, 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 // resolver + the data dir for the token store. All three come from // existing global config — no new config knobs required. 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, // DC-052: license manager so providers can gate Pro-only flows // (e.g. magic-link signup that crosses the 3-user cap). licenseManager: ctx.licenseManager, }, ctx.siteConfig ); ctx.authProviders = registry; // exposed for /api/v1/auth/methods, etc. // NEW (DC-046): pluggable /api/v1/auth/login/* routes. Frontends should // migrate here over time — the legacy /api/v1/totp/* routes below stay // for back-compat. Mounted under `/auth` so internal paths // (`/login/methods`, `/disable/:provider`) resolve at the canonical // `/api/v1/auth/login/*` and `/api/v1/auth/disable/*` URLs that match // PUBLIC_ROUTES and the documented login UI contract. router.use('/auth', initLogin({ registry, asyncHandler: ctx.asyncHandler, errorResponse: ctx.errorResponse, log: ctx.log, })); router.use(initTotp(deps)); 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) => { // Defense-in-depth: the production session context exposes isValid() // (src/context/session.js). If a future refactor passes a differently- // shaped object, fall back to authenticated:true rather than throwing // a 500 — /me is polled by every open dashboard tab every 60s, so a // throw here becomes a log storm (exactly what DC-093 removed), and // this handler only runs after the session middleware already // admitted the request, so default-false would misreport a valid // session as unauthenticated. const _authed = deps.session && typeof deps.session.isValid === 'function' ? deps.session.isValid(req) : true; 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: _authed, 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: _authed, 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 // /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- // circuits when licenseManager is absent. const adminRouter = initAdmin({ asyncHandler: ctx.asyncHandler, errorResponse: ctx.errorResponse, log: ctx.log, session: ctx.session, licenseManager: ctx.licenseManager, userStore, }); // DC-048 attach: licenseManager + userStore on app.locals if (ctx.licenseManager || userStore) { router.use('/auth', (req, _res, next) => { if (ctx.licenseManager) req.app.locals.licenseManager = ctx.licenseManager; if (userStore) req.app.locals.userStore = userStore; next(); }); } router.use('/auth', adminRouter); } return router; };