Files
dashcaddy/dashcaddy-api/src/auth/providers/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

120 lines
4.5 KiB
JavaScript

/**
* AuthProvider registry — composes every enabled provider for the current
* DashCaddy instance.
*
* Today only `totp` exists. Adding a new provider:
* 1. Create src/auth/providers/<name>.js exporting a class extending
* AuthProvider (see ./base.js for the contract).
* 2. Add a case to `loadProvider()` below.
* 3. Document in CHANGELOG.md. No route edits, no app.js edits.
*
* The registry returns a single object the auth router walks to:
* - list all enabled providers + their login methods (for the UI)
* - look up a provider by name (for /api/v1/auth/login/:provider/...)
* - look up the active provider for the check-session endpoint (every
* provider's session is the same DashCaddy cookie, so check-session
* is a global concern, not provider-specific)
*
* Sessions are global, not per-provider. Every successful verify() creates
* the same DashCaddy session cookie. TOTP and email magic link both use
* session.create() / setCookie() — there's no "TOTP session" vs "email session"
* distinction. The check-session endpoint therefore doesn't dispatch to a
* specific provider — it just verifies the cookie's validity (the existing
* behavior in src/utilities/middleware.js isSessionValid()).
*/
const TotpProvider = require('./totp');
const EmailMagicLinkProvider = require('./email');
/**
* Instantiate every enabled provider for this instance.
*
* @param {Object} deps Shared infrastructure (see TotpProvider constructor)
* @param {Object} config The site config — providers.enabled.{name} toggles each.
* @returns {Object} { providers: Map<name, AuthProvider>, listEnabled() }
*/
function createAuthProviderRegistry(deps, config) {
const providers = new Map();
// Always load TOTP — it's the default. The legacy /api/v1/totp/* routes
// and the Caddy forward_auth gate all assume TOTP exists.
const totpProvider = new TotpProvider({
...deps,
config: deps.config.totp, // the existing totpConfig object from app.js
saveProviderConfig: deps.saveTotpConfig, // existing helper
// DC-048: user store for bootstrap + audit attribution on TOTP logins.
userStore: deps.userStore || null,
});
providers.set('totp', totpProvider);
// DC-047: EmailMagicLinkProvider — second AuthProvider. Always register;
// enablement is controlled by config.authProviders.email.enabled (defaults
// to true so dev installs "just work"). Providers are constructed lazily
// in the sense that even a provider with no SMTP config still issues and
// verifies tokens — only delivery falls back to the dev console-log path.
providers.set('email', new EmailMagicLinkProvider({
...deps,
config: deps.config.email || { enabled: true, sessionDuration: '24h' },
saveProviderConfig: deps.saveProviderConfig || (async () => {}),
emailConfig: deps.emailConfig || null,
siteConfig: deps.siteConfig || {},
// DC-048: real authorization check via the user store. Without it,
// every email is allowed (legacy single-user behavior).
userStore: deps.userStore || null,
}));
// Future: OIDC, SAML, passkeys — each gated on config.authProviders
// or its own toggle. The shape is uniform: { enabled: bool, ...rest }.
//
// if (config?.authProviders?.oidc?.enabled) {
// providers.set('oidc', new OidcProvider({
// ...deps, config: config.authProviders.oidc, saveProviderConfig: ...
// }));
// }
return {
providers,
/**
* Return the list of enabled providers with their public config.
* Used by GET /api/v1/auth/methods to drive the login UI.
*/
async listEnabled() {
const out = [];
for (const [name, provider] of providers) {
if (!(await provider.isEnabled())) continue;
const methods = await provider.listMethods();
out.push({
name,
config: await provider.getConfig(),
methods,
});
}
return out;
},
/**
* All configured providers, including disabled-but-not-deleted ones.
* For the settings UI.
*/
async listAll() {
const out = [];
for (const [name, provider] of providers) {
out.push({
name,
config: await provider.getConfig(),
methods: await provider.listMethods(),
});
}
return out;
},
/**
* Look up a provider by name. Returns null if not registered.
*/
getProvider(name) {
return providers.get(name) || null;
},
};
}
module.exports = { createAuthProviderRegistry };