From ac23b2e09358659621450e931b8ffb98c98750fe Mon Sep 17 00:00:00 2001 From: Krystie Date: Sun, 29 Mar 2026 20:16:08 -0700 Subject: [PATCH] refactor(routes): Phase 3.6 - standardize sites.js --- dashcaddy-api/routes/sites.js | 88 ++++++++++++++++++++--------------- dashcaddy-api/src/app.js | 11 ++++- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/dashcaddy-api/routes/sites.js b/dashcaddy-api/routes/sites.js index da2e966..212b7fb 100644 --- a/dashcaddy-api/routes/sites.js +++ b/dashcaddy-api/routes/sites.js @@ -1,29 +1,43 @@ const express = require('express'); const fs = require('fs'); const { CADDY, REGEX, LIMITS } = require('../constants'); -const { ValidationError } = require('../errors'); +const { ValidationError, ConflictError, NotFoundError } = require('../errors'); +const { validateURL } = require('../input-validator'); -module.exports = function(ctx) { +/** + * Sites route factory + * @param {Object} deps - Explicit dependencies + * @param {Function} deps.asyncHandler - Async route handler wrapper + * @param {Object} deps.caddy - Caddy manager + * @param {Object} deps.dns - DNS manager + * @param {Function} deps.fetchT - Fetch with timeout + * @param {Function} deps.buildDomain - Domain builder function + * @param {Function} deps.addServiceToConfig - Service config adder + * @param {Object} deps.siteConfig - Site configuration + * @param {Object} deps.log - Logger instance + * @returns {express.Router} + */ +module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addServiceToConfig, siteConfig, log }) { const router = express.Router(); // Get Caddyfile contents - router.get('/caddyfile', ctx.asyncHandler(async (req, res) => { - const content = await ctx.caddy.read(); + router.get('/caddyfile', asyncHandler(async (req, res) => { + const content = await caddy.read(); res.json({ success: true, content }); }, 'caddyfile-get')); // Get current Caddy config (from admin API) - router.get('/caddy/config', ctx.asyncHandler(async (req, res) => { - const response = await ctx.fetchT(`${ctx.caddy.adminUrl}/config/`); + router.get('/caddy/config', asyncHandler(async (req, res) => { + const response = await fetchT(`${caddy.adminUrl}/config/`); const config = await response.json(); res.json({ success: true, config }); }, 'caddy-config')); // Reload Caddy configuration via admin API - router.post('/caddy/reload', ctx.asyncHandler(async (req, res) => { - const caddyfileContent = await ctx.caddy.read(); + router.post('/caddy/reload', asyncHandler(async (req, res) => { + const caddyfileContent = await caddy.read(); - const response = await ctx.fetchT(`${ctx.caddy.adminUrl}/load`, { + const response = await fetchT(`${caddy.adminUrl}/load`, { method: 'POST', headers: { 'Content-Type': CADDY.CONTENT_TYPE }, body: caddyfileContent @@ -31,16 +45,16 @@ module.exports = function(ctx) { if (!response.ok) { const errorText = await response.text(); - ctx.log.error('caddy', 'Caddy reload failed', { error: errorText }); - return ctx.errorResponse(res, 500, '[DC-303] Caddy reload failed. Check server logs for details.'); + log.error('caddy', 'Caddy reload failed', { error: errorText }); + throw new Error('Caddy reload failed. Check server logs for details.'); } res.json({ success: true, message: 'Caddy configuration reloaded successfully' }); }, 'caddy-reload')); // Get Certificate Authorities from Caddyfile - router.get('/caddy/cas', ctx.asyncHandler(async (req, res) => { - const content = await ctx.caddy.read(); + router.get('/caddy/cas', asyncHandler(async (req, res) => { + const content = await caddy.read(); const cas = []; const pkiRegex = /pki\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs; @@ -117,11 +131,11 @@ module.exports = function(ctx) { }, 'caddy-get-cas')); // Remove a site from Caddyfile - router.delete('/site/:domain', ctx.asyncHandler(async (req, res) => { + router.delete('/site/:domain', asyncHandler(async (req, res) => { const { domain } = req.params; if (!domain) throw new ValidationError('Domain is required'); - const result = await ctx.caddy.modify((content) => { + const result = await caddy.modify((content) => { const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const siteBlockRegex = new RegExp( `\\n?${escapedDomain}\\s*\\{[^{}]*(?:\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\}[^{}]*)*\\}\\s*`, 'g' @@ -133,16 +147,16 @@ module.exports = function(ctx) { if (!result.success) { if (result.rolledBack) { - return ctx.errorResponse(res, 500, `Removed "${domain}" but Caddy reload failed (rolled back): ${result.error}`); + throw new Error( `Removed "${domain}" but Caddy reload failed (rolled back): ${result.error}`); } - return ctx.errorResponse(res, 404, `Site block for "${domain}" not found in Caddyfile`); + throw new NotFoundError(`Site block for "" in Caddyfile`); } res.json({ success: true, message: `Site "${domain}" removed from Caddyfile and Caddy reloaded` }); }, 'site-delete')); // Add a new site to Caddyfile and reload - router.post('/site', ctx.asyncHandler(async (req, res) => { + router.post('/site', asyncHandler(async (req, res) => { const { domain, upstream, config } = req.body; if (!domain || !upstream) throw new ValidationError('Domain and upstream are required'); if (!REGEX.DOMAIN.test(domain)) throw new ValidationError('[DC-301] Invalid domain format'); @@ -150,27 +164,27 @@ module.exports = function(ctx) { const upstreamRegex = /^[a-z0-9.-]+:\d{1,5}$/i; if (!upstreamRegex.test(upstream)) throw new ValidationError('Invalid upstream format. Use host:port'); - let content = await ctx.caddy.read(); + let content = await caddy.read(); const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const siteBlockRegex = new RegExp(`\\n?${escapedDomain}\\s*\\{`, 'g'); if (siteBlockRegex.test(content)) { - return ctx.errorResponse(res, 409, `[DC-302] Site block for "${domain}" already exists in Caddyfile`); + throw new ConflictError(`Site block for "" already exists in Caddyfile`); } // Always generate structured config — never allow raw Caddy config injection const newSiteBlock = `\n${domain} {\n reverse_proxy ${upstream}\n tls internal\n}\n`; - const result = await ctx.caddy.modify(c => c + newSiteBlock); + const result = await caddy.modify(c => c + newSiteBlock); if (!result.success) { - return ctx.errorResponse(res, 500, `[DC-303] Site added to Caddyfile but reload failed: ${result.error}`, + throw new Error( `[DC-303] Site added to Caddyfile but reload failed: ${result.error}`, result.rolledBack ? { note: 'Caddyfile was rolled back to previous state' } : {}); } - ctx.ok(res, { message: `Site "${domain}" added to Caddyfile and Caddy reloaded successfully` }); + res.json({ success: true, message: `Site "${domain}" added to Caddyfile and Caddy reloaded successfully` }); }, 'site-add')); // Add external service reverse proxy to Caddyfile - router.post('/site/external', ctx.asyncHandler(async (req, res) => { + router.post('/site/external', asyncHandler(async (req, res) => { const { subdomain, externalUrl, preserveHost, followRedirects, sslType, caddyfilePath, reloadCaddy: shouldReload, createDns, serviceName, logo } = req.body; if (!subdomain || !externalUrl) { @@ -181,22 +195,22 @@ module.exports = function(ctx) { } try { - ctx.validateURL(externalUrl); + validateURL(externalUrl); } catch (validationErr) { - return ctx.errorResponse(res, 400, validationErr.message); + throw new ValidationError(validationErr.message); } - const domain = ctx.buildDomain(subdomain); + const domain = buildDomain(subdomain); let dnsWarning = null; try { if (createDns) { try { - await ctx.dns.createRecord(subdomain, ctx.siteConfig.dnsServerIp); - ctx.log.info('dns', 'DNS record created for external proxy', { domain, ip: ctx.siteConfig.dnsServerIp }); + await dns.createRecord(subdomain, siteConfig.dnsServerIp); + log.info('dns', 'DNS record created for external proxy', { domain, ip: siteConfig.dnsServerIp }); } catch (dnsError) { dnsWarning = `DNS creation failed: ${dnsError.message}. You may need to create the DNS record manually.`; - ctx.log.warn('dns', 'DNS creation failed for external proxy', { domain, error: dnsError.message }); + log.warn('dns', 'DNS creation failed for external proxy', { domain, error: dnsError.message }); } } @@ -221,29 +235,29 @@ module.exports = function(ctx) { proxyConfig = `\n${domain} {\n ${sslConfig}\n\n reverse_proxy ${externalUrl} {${hostHeader}\n transport http {\n tls\n }\n }\n}\n`; } - const caddyResult = await ctx.caddy.modify(c => { + const caddyResult = await caddy.modify(c => { const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (new RegExp(`\\n?${escapedDomain}\\s*\\{`, 'g').test(c)) return null; return c + proxyConfig; }); if (!caddyResult.success && !caddyResult.rolledBack) { - return ctx.errorResponse(res, 409, `[DC-302] Site block for "${domain}" already exists in Caddyfile`); + throw new ConflictError(`Site block for "" already exists in Caddyfile`); } if (!caddyResult.success) { - return ctx.errorResponse(res, 500, `[DC-303] External proxy added but Caddy reload failed (rolled back): ${caddyResult.error}`); + throw new Error( `[DC-303] External proxy added but Caddy reload failed (rolled back): ${caddyResult.error}`); } if (serviceName && logo) { try { - await ctx.addServiceToConfig({ + await addServiceToConfig({ id: subdomain, name: serviceName, logo, isExternal: true, externalUrl, deployedAt: new Date().toISOString() }); - ctx.log.info('deploy', 'Service added to dashboard', { subdomain }); + log.info('deploy', 'Service added to dashboard', { subdomain }); } catch (serviceError) { - ctx.log.warn('deploy', 'Failed to add service to dashboard', { subdomain, error: serviceError.message }); + log.warn('deploy', 'Failed to add service to dashboard', { subdomain, error: serviceError.message }); } } @@ -254,7 +268,7 @@ module.exports = function(ctx) { if (dnsWarning) response.warning = dnsWarning; res.json(response); } catch (error) { - ctx.errorResponse(res, 500, ctx.safeErrorMessage(error)); + throw error; } }, 'site-external')); diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index b48ce62..e7d4a73 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -358,7 +358,16 @@ async function createApp() { SERVICES_FILE: ctx.SERVICES_FILE, log: ctx.log })); - apiRouter.use(sitesRoutes(ctx)); + apiRouter.use(sitesRoutes({ + asyncHandler: ctx.asyncHandler, + caddy: ctx.caddy, + dns: ctx.dns, + fetchT: ctx.fetchT, + buildDomain: ctx.buildDomain, + addServiceToConfig: ctx.addServiceToConfig, + siteConfig: ctx.siteConfig, + log: ctx.log + })); apiRouter.use(credentialsRoutes({ credentialManager: ctx.credentialManager, asyncHandler: ctx.asyncHandler