From df3e8efdd0c43ff0e8f3339ded7c4f7e966b6338 Mon Sep 17 00:00:00 2001 From: Krystie Date: Sun, 29 Mar 2026 21:42:30 -0700 Subject: [PATCH] Refactor auth routes: explicit dependency injection - Updated all auth route modules to use destructured dependencies - Added JSDoc comments for factory functions - Replaced ctx. references with direct parameter access - Updated auth/index.js to extract and pass explicit dependencies - sso-gate.js maintains session helper exports from session-handlers - All files pass syntax validation Files refactored: - routes/auth/keys.js - routes/auth/session-handlers.js - routes/auth/sso-gate.js - routes/auth/totp.js - routes/auth/index.js (orchestrator) --- dashcaddy-api/routes/auth/index.js | 25 +++++++++-- dashcaddy-api/routes/auth/keys.js | 26 +++++++---- dashcaddy-api/routes/auth/session-handlers.js | 44 ++++++++++++------- dashcaddy-api/routes/auth/sso-gate.js | 42 +++++++++++------- dashcaddy-api/routes/auth/totp.js | 31 ++++++++----- 5 files changed, 110 insertions(+), 58 deletions(-) diff --git a/dashcaddy-api/routes/auth/index.js b/dashcaddy-api/routes/auth/index.js index 1b94c5f..430d09b 100644 --- a/dashcaddy-api/routes/auth/index.js +++ b/dashcaddy-api/routes/auth/index.js @@ -4,14 +4,31 @@ const initKeys = require('./keys'); const initSessionHandlers = require('./session-handlers'); const initSsoGate = require('./sso-gate'); +/** + * Auth routes aggregator + * Assembles all auth sub-routes with their dependencies + * @param {Object} ctx - Application context (for backward compatibility) + * @returns {express.Router} + */ module.exports = function(ctx) { const router = express.Router(); - const { getAppSession, appSessionCache } = initSessionHandlers(ctx); + // Extract dependencies from context + const deps = { + authManager: ctx.authManager, + credentialManager: ctx.credentialManager, + totpConfig: ctx.totpConfig, + session: ctx.session, + asyncHandler: ctx.asyncHandler, + errorResponse: ctx.errorResponse, + log: ctx.log + }; - router.use(initTotp(ctx)); - router.use(initKeys(ctx)); - router.use(initSsoGate(ctx, getAppSession, appSessionCache)); + const { getAppSession, appSessionCache } = initSessionHandlers(deps); + + router.use(initTotp(deps)); + router.use(initKeys(deps)); + router.use(initSsoGate({ ...deps, getAppSession, appSessionCache })); return router; }; diff --git a/dashcaddy-api/routes/auth/keys.js b/dashcaddy-api/routes/auth/keys.js index 7246dc4..bf26c15 100644 --- a/dashcaddy-api/routes/auth/keys.js +++ b/dashcaddy-api/routes/auth/keys.js @@ -1,7 +1,15 @@ const express = require('express'); const { ValidationError, ForbiddenError, NotFoundError } = require('../../errors'); +/** + * Auth API keys routes factory + * @param {Object} deps - Explicit dependencies + * @param {Object} deps.authManager - Auth manager + * @param {Function} deps.asyncHandler - Async route handler wrapper + * @param {Object} deps.log - Logger instance + * @returns {express.Router} + */ -module.exports = function(ctx) { +module.exports = function({ authManager, asyncHandler, log }) { const router = express.Router(); // Helper function to parse expiration strings to milliseconds @@ -24,18 +32,18 @@ module.exports = function(ctx) { } // List all API keys - router.get('/auth/keys', ctx.asyncHandler(async (req, res) => { + router.get('/auth/keys', asyncHandler(async (req, res) => { // Require session authentication (not API key - can't manage keys with key itself) if (!req.auth || req.auth.type !== 'session') { throw new ForbiddenError('API key management requires TOTP session authentication'); } - const keys = await ctx.authManager.listAPIKeys(); + const keys = await authManager.listAPIKeys(); res.json({ success: true, keys }); }, 'auth-keys-list')); // Generate new API key - router.post('/auth/keys', ctx.asyncHandler(async (req, res) => { + router.post('/auth/keys', asyncHandler(async (req, res) => { // Require session authentication if (!req.auth || req.auth.type !== 'session') { throw new ForbiddenError('API key generation requires TOTP session authentication'); @@ -53,7 +61,7 @@ module.exports = function(ctx) { throw new ValidationError(`Invalid scopes. Valid options: ${validScopes.join(', ')}`, 'scopes'); } - const keyData = await ctx.authManager.generateAPIKey( + const keyData = await authManager.generateAPIKey( name.trim(), scopes || ['read', 'write'] ); @@ -70,7 +78,7 @@ module.exports = function(ctx) { }, 'auth-keys-generate')); // Revoke API key - router.delete('/auth/keys/:keyId', ctx.asyncHandler(async (req, res) => { + router.delete('/auth/keys/:keyId', asyncHandler(async (req, res) => { // Require session authentication if (!req.auth || req.auth.type !== 'session') { throw new ForbiddenError('API key revocation requires TOTP session authentication'); @@ -82,7 +90,7 @@ module.exports = function(ctx) { throw new ValidationError('Key ID is required', 'keyId'); } - const success = await ctx.authManager.revokeAPIKey(keyId); + const success = await authManager.revokeAPIKey(keyId); if (success) { res.json({ success: true, message: 'API key revoked successfully' }); @@ -92,7 +100,7 @@ module.exports = function(ctx) { }, 'auth-keys-revoke')); // Generate JWT from TOTP session - router.post('/auth/jwt', ctx.asyncHandler(async (req, res) => { + router.post('/auth/jwt', asyncHandler(async (req, res) => { // Require session authentication if (!req.auth || req.auth.type !== 'session') { throw new ForbiddenError('JWT generation requires TOTP session authentication'); @@ -106,7 +114,7 @@ module.exports = function(ctx) { throw new ValidationError('Invalid expiresIn format. Use: 60s, 15m, 24h, 7d, 1y', 'expiresIn'); } - const token = await ctx.authManager.generateJWT( + const token = await authManager.generateJWT( { sub: userId || 'dashcaddy-admin', scope: ['admin'] // Session-generated JWTs have admin scope diff --git a/dashcaddy-api/routes/auth/session-handlers.js b/dashcaddy-api/routes/auth/session-handlers.js index 534b55b..0452c7c 100644 --- a/dashcaddy-api/routes/auth/session-handlers.js +++ b/dashcaddy-api/routes/auth/session-handlers.js @@ -1,8 +1,18 @@ const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants'); const { createCache, CACHE_CONFIGS } = require('../../cache-config'); -module.exports = function(ctx) { +module.exports = function({ authManager, credentialManager, asyncHandler, errorResponse, log }) { // App session cache for auto-login +/** + * Auth session handlers routes factory + * @param {Object} deps - Explicit dependencies + * @param {Object} deps.authManager - Auth manager + * @param {Object} deps.credentialManager - Credential manager + * @param {Function} deps.asyncHandler - Async route handler wrapper + * @param {Function} deps.errorResponse - Error response helper + * @param {Object} deps.log - Logger instance + * @returns {express.Router} + */ const appSessionCache = createCache(CACHE_CONFIGS.appSessions); async function getAppSession(serviceId, baseUrl, username, password) { @@ -36,12 +46,12 @@ module.exports = function(ctx) { const location = locationMatch ? locationMatch[1].trim() : ''; if (location && !location.includes('login')) { appSessionCache.set(serviceId, { cookies: '__ip_session=1', exp: Date.now() + SESSION_TTL.IP_SESSION }); - ctx.log.info('auth', 'Router auto-login successful (IP-based session)', { serviceId }); + log.info('auth', 'Router auto-login successful (IP-based session)', { serviceId }); return '__ip_session=1'; } - ctx.log.warn('auth', 'Router auto-login failed', { serviceId }); + log.warn('auth', 'Router auto-login failed', { serviceId }); } catch (e) { - ctx.log.warn('auth', 'Router auto-login error', { serviceId, error: e.message?.substring(0, 100) }); + log.warn('auth', 'Router auto-login error', { serviceId, error: e.message?.substring(0, 100) }); } appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; @@ -73,12 +83,12 @@ module.exports = function(ctx) { serverId: authData.ServerId, serverName: authData.User?.ServerName || serviceId, }; appSessionCache.set(serviceId, { cookies: `token=${authData.AccessToken}`, token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION }); - ctx.log.info('auth', 'Auto-login successful (token + userId obtained)', { serviceId }); + log.info('auth', 'Auto-login successful (token + userId obtained)', { serviceId }); return `token=${authData.AccessToken}`; } - ctx.log.warn('auth', 'Auto-login failed', { serviceId, status: authResp.status }); + log.warn('auth', 'Auto-login failed', { serviceId, status: authResp.status }); } catch (e) { - ctx.log.warn('auth', 'Auto-login error', { serviceId, error: e.message }); + log.warn('auth', 'Auto-login error', { serviceId, error: e.message }); } appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; @@ -99,12 +109,12 @@ module.exports = function(ctx) { const token = plexData?.user?.authToken; if (token) { appSessionCache.set(serviceId, { cookies: `plexToken=${token}`, token, exp: Date.now() + SESSION_TTL.TOKEN_SESSION }); - ctx.log.info('auth', 'Plex auto-login successful via plex.tv', { serviceId }); + log.info('auth', 'Plex auto-login successful via plex.tv', { serviceId }); return `plexToken=${token}`; } - ctx.log.warn('auth', 'Plex auto-login failed: no token in response', { serviceId, status: plexResp.status }); + log.warn('auth', 'Plex auto-login failed: no token in response', { serviceId, status: plexResp.status }); } catch (e) { - ctx.log.warn('auth', 'Plex auto-login error', { serviceId, error: e.message }); + log.warn('auth', 'Plex auto-login error', { serviceId, error: e.message }); } appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; @@ -129,11 +139,11 @@ module.exports = function(ctx) { if (data.token) { const cookies = `token=${data.token}`; appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION }); - ctx.log.info('auth', 'Auto-login successful (JWT token cached)', { serviceId }); + log.info('auth', 'Auto-login successful (JWT token cached)', { serviceId }); return cookies; } } catch (e) { /* JSON parse failed */ } - ctx.log.warn('auth', 'Auto-login: no token in response', { serviceId, status: resp.status }); + log.warn('auth', 'Auto-login: no token in response', { serviceId, status: resp.status }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; } @@ -141,7 +151,7 @@ module.exports = function(ctx) { if (serviceId === 'torrent') { const text = await resp.text(); if (text.trim() !== 'Ok.') { - ctx.log.warn('auth', 'Auto-login failed', { serviceId, response: text.trim() }); + log.warn('auth', 'Auto-login failed', { serviceId, response: text.trim() }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); return null; } @@ -151,7 +161,7 @@ module.exports = function(ctx) { if (setCookies.length > 0) { const cookies = setCookies.map(c => c.split(';')[0]).join('; '); appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION }); - ctx.log.info('auth', 'Auto-login successful, session cached', { serviceId, cookieCount: setCookies.length }); + log.info('auth', 'Auto-login successful, session cached', { serviceId, cookieCount: setCookies.length }); return cookies; } @@ -159,14 +169,14 @@ module.exports = function(ctx) { if (rawCookie) { const cookies = rawCookie.split(/,(?=[^ ])/).map(c => c.split(';')[0].trim()).join('; '); appSessionCache.set(serviceId, { cookies, exp: Date.now() + SESSION_TTL.COOKIE_SESSION }); - ctx.log.info('auth', 'Auto-login successful (fallback), session cached', { serviceId }); + log.info('auth', 'Auto-login successful (fallback), session cached', { serviceId }); return cookies; } - ctx.log.warn('auth', 'Auto-login: no cookies in response', { serviceId, status: resp.status }); + log.warn('auth', 'Auto-login: no cookies in response', { serviceId, status: resp.status }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); } catch (e) { - ctx.log.warn('auth', 'Auto-login error', { serviceId, error: e.message }); + log.warn('auth', 'Auto-login error', { serviceId, error: e.message }); appSessionCache.set(serviceId, { failed: true, exp: Date.now() + SESSION_TTL.FAILED_LOGIN }); } return null; diff --git a/dashcaddy-api/routes/auth/sso-gate.js b/dashcaddy-api/routes/auth/sso-gate.js index 455ee8a..fa88c6b 100644 --- a/dashcaddy-api/routes/auth/sso-gate.js +++ b/dashcaddy-api/routes/auth/sso-gate.js @@ -2,18 +2,26 @@ const express = require('express'); const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants'); const { AuthenticationError, NotFoundError } = require('../errors'); -module.exports = function(ctx, getAppSession, appSessionCache) { +/** + * Auth SSO gate routes factory + * @param {Object} deps - Explicit dependencies (includes session helpers) + * @returns {express.Router} + */ +module.exports = function(deps) { const router = express.Router(); + + // Extract dependencies + const { authManager, totpConfig, session, asyncHandler, errorResponse, log, getAppSession, appSessionCache } = deps; // Caddy forward_auth gate: checks TOTP session + injects service credentials - router.get('/auth/gate/:serviceId', ctx.asyncHandler(async (req, res) => { + router.get('/auth/gate/:serviceId', asyncHandler(async (req, res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); const serviceId = req.params.serviceId; // Check TOTP session first - if (ctx.totpConfig.enabled && ctx.totpConfig.sessionDuration !== 'never') { - const valid = ctx.session.isValid(req); - if (!valid) return ctx.errorResponse(res, 401, 'Session expired or invalid', { authenticated: false }); + if (totpConfig.enabled && totpConfig.sessionDuration !== 'never') { + const valid = session.isValid(req); + if (!valid) return errorResponse(res, 401, 'Session expired or invalid', { authenticated: false }); } // Session valid (or TOTP disabled) - inject credentials if premium SSO is active @@ -73,18 +81,18 @@ module.exports = function(ctx, getAppSession, appSessionCache) { const apiKey = arrKey || svcKey; if (apiKey) { res.setHeader('X-Api-Key', apiKey); injected = true; } } catch (e) { - ctx.log.warn('auth', 'Credential error', { serviceId, error: e.message }); + log.warn('auth', 'Credential error', { serviceId, error: e.message }); } res.status(200).json({ authenticated: true, credentialsInjected: injected }); }, 'auth-gate')); // Return cached app session token for client-side auth (Premium SSO feature) - router.get('/auth/app-token/:serviceId', ctx.licenseManager.requirePremium('sso'), ctx.asyncHandler(async (req, res) => { + router.get('/auth/app-token/:serviceId', ctx.licenseManager.requirePremium('sso'), asyncHandler(async (req, res) => { const { serviceId } = req.params; - if (ctx.totpConfig.enabled && ctx.totpConfig.sessionDuration !== 'never') { - if (!ctx.session.isValid(req)) throw new AuthenticationError('Not authenticated'); + if (totpConfig.enabled && totpConfig.sessionDuration !== 'never') { + if (!session.isValid(req)) throw new AuthenticationError('Not authenticated'); } // Jellyfin/Emby: separate browser-specific token @@ -92,7 +100,7 @@ module.exports = function(ctx, getAppSession, appSessionCache) { const browserCacheKey = `${serviceId}_browser`; const browserCached = appSessionCache.get(browserCacheKey); if (browserCached && browserCached.exp > Date.now()) { - if (browserCached.failed) return ctx.errorResponse(res, 500, 'Login recently failed'); + if (browserCached.failed) return errorResponse(res, 500, 'Login recently failed'); if (browserCached.token) { const resp = { token: browserCached.token }; if (browserCached.tokenData) Object.assign(resp, browserCached.tokenData); @@ -118,17 +126,17 @@ module.exports = function(ctx, getAppSession, appSessionCache) { appSessionCache.set(browserCacheKey, { token: authData.AccessToken, tokenData, exp: Date.now() + SESSION_TTL.TOKEN_SESSION }); return res.json({ token: authData.AccessToken, ...tokenData }); } - return ctx.errorResponse(res, 500, '[DC-501] Authentication failed'); + return errorResponse(res, 500, '[DC-501] Authentication failed'); } catch (e) { - ctx.log.warn('auth', 'Browser token error', { serviceId, error: e.message }); - return ctx.errorResponse(res, 500, e.message); + log.warn('auth', 'Browser token error', { serviceId, error: e.message }); + return errorResponse(res, 500, e.message); } } // Check cache first const cached = appSessionCache.get(serviceId); if (cached && cached.exp > Date.now()) { - if (cached.failed) return ctx.errorResponse(res, 500, '[DC-501] Login recently failed, retrying in a few minutes'); + if (cached.failed) return errorResponse(res, 500, '[DC-501] Login recently failed, retrying in a few minutes'); if (cached.token) { const resp = { token: cached.token }; if (cached.tokenData) Object.assign(resp, cached.tokenData); @@ -172,10 +180,10 @@ module.exports = function(ctx, getAppSession, appSessionCache) { return res.json({ cookies: appCookies }); } - ctx.errorResponse(res, 500, '[DC-501] Login failed'); + errorResponse(res, 500, '[DC-501] Login failed'); } catch (e) { - ctx.log.warn('auth', 'App-token error', { error: e.message }); - ctx.errorResponse(res, 500, e.message); + log.warn('auth', 'App-token error', { error: e.message }); + errorResponse(res, 500, e.message); } }, 'auth-app-token')); diff --git a/dashcaddy-api/routes/auth/totp.js b/dashcaddy-api/routes/auth/totp.js index f927142..05cb660 100644 --- a/dashcaddy-api/routes/auth/totp.js +++ b/dashcaddy-api/routes/auth/totp.js @@ -2,11 +2,20 @@ const express = require('express'); const { renewCSRFToken } = require('../../csrf-protection'); const { ValidationError, AuthenticationError } = require('../../errors'); -module.exports = function(ctx) { +module.exports = function({ authManager, asyncHandler, errorResponse, log }) { const router = express.Router(); +/** + * Auth TOTP routes factory + * @param {Object} deps - Explicit dependencies + * @param {Object} deps.authManager - Auth manager + * @param {Function} deps.asyncHandler - Async route handler wrapper + * @param {Function} deps.errorResponse - Error response helper + * @param {Object} deps.log - Logger instance + * @returns {express.Router} + */ // Get current TOTP config (public route) - router.get('/totp/config', ctx.asyncHandler(async (req, res) => { + router.get('/totp/config', asyncHandler(async (req, res) => { res.json({ success: true, config: { @@ -18,7 +27,7 @@ module.exports = function(ctx) { }, 'totp-config-get')); // Generate new TOTP secret + QR code - router.post('/totp/setup', ctx.asyncHandler(async (req, res) => { + router.post('/totp/setup', asyncHandler(async (req, res) => { const { authenticator } = require('otplib'); const QRCode = require('qrcode'); @@ -46,7 +55,7 @@ module.exports = function(ctx) { }, 'totp-setup')); // Verify first code to confirm setup, then activate TOTP - router.post('/totp/verify-setup', ctx.asyncHandler(async (req, res) => { + router.post('/totp/verify-setup', asyncHandler(async (req, res) => { const { authenticator } = require('otplib'); const { code } = req.body; @@ -83,7 +92,7 @@ module.exports = function(ctx) { }, 'totp-verify-setup')); // Login: verify TOTP code and set session cookie - router.post('/totp/verify', ctx.asyncHandler(async (req, res) => { + router.post('/totp/verify', asyncHandler(async (req, res) => { const { authenticator } = require('otplib'); const { code } = req.body; @@ -105,19 +114,19 @@ module.exports = function(ctx) { throw new AuthenticationError('[DC-111] Invalid code'); } - ctx.log.info('auth', 'TOTP verified, creating session', { ip: ctx.session.getClientIP(req), duration: ctx.totpConfig.sessionDuration }); + log.info('auth', 'TOTP verified, creating session', { ip: ctx.session.getClientIP(req), duration: ctx.totpConfig.sessionDuration }); ctx.session.create(req, ctx.totpConfig.sessionDuration); ctx.session.setCookie(res, ctx.totpConfig.sessionDuration); // Rotate CSRF token for the new session const newCsrfToken = renewCSRFToken(res, req.secure || req.protocol === 'https'); - ctx.log.debug('auth', 'Session created', { sessions: ctx.session.ipSessions.size }); + log.debug('auth', 'Session created', { sessions: ctx.session.ipSessions.size }); res.json({ success: true, message: 'Authenticated successfully', sessionDuration: ctx.totpConfig.sessionDuration, csrfToken: newCsrfToken }); }, 'totp-verify')); // Check session validity (used by Caddy forward_auth) - router.get('/totp/check-session', ctx.asyncHandler(async (req, res) => { + router.get('/totp/check-session', asyncHandler(async (req, res) => { // Never cache session checks — stale cached 200s cause auth loops res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); res.setHeader('Pragma', 'no-cache'); @@ -127,7 +136,7 @@ module.exports = function(ctx) { } const valid = ctx.session.isValid(req); - ctx.log.debug('auth', 'Session check', { ip: ctx.session.getClientIP(req), valid, sessions: ctx.session.ipSessions.size }); + log.debug('auth', 'Session check', { ip: ctx.session.getClientIP(req), valid, sessions: ctx.session.ipSessions.size }); if (valid) { return res.status(200).json({ authenticated: true }); } @@ -136,7 +145,7 @@ module.exports = function(ctx) { }, 'totp-check-session')); // Disable TOTP - router.post('/totp/disable', ctx.asyncHandler(async (req, res) => { + router.post('/totp/disable', asyncHandler(async (req, res) => { const { code } = req.body; // Always require a valid TOTP code when TOTP is active @@ -169,7 +178,7 @@ module.exports = function(ctx) { }, 'totp-disable')); // Update TOTP settings (session duration) - router.post('/totp/config', ctx.asyncHandler(async (req, res) => { + router.post('/totp/config', asyncHandler(async (req, res) => { const { sessionDuration } = req.body; if (sessionDuration && !ctx.session.durations.hasOwnProperty(sessionDuration)) {