Files
dashcaddy/dashcaddy-api/routes/auth/index.js
T
hermes 321334cd33
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
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.
2026-07-20 17:44:11 -07:00

159 lines
6.2 KiB
JavaScript

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');
/**
* 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,
},
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-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;
};