diff --git a/dashcaddy-api/routes/browse.js b/dashcaddy-api/routes/browse.js index 8f8919b..40d4ed4 100644 --- a/dashcaddy-api/routes/browse.js +++ b/dashcaddy-api/routes/browse.js @@ -15,7 +15,7 @@ const { ValidationError, ForbiddenError } = require('../errors'); * @param {Object} deps.docker - Docker client * @returns {express.Router} */ -module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docker }) { +module.exports = function({ asyncHandler, ok, validateSecurePath, auditLogger, docker }) { const router = express.Router(); // Parse browse roots from environment @@ -44,7 +44,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke } } - res.json({ success: true, roots }); + ok(res, { roots }); }, 'browse-roots')); // Browse directory contents @@ -64,7 +64,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke roots.push(r); } } - return res.json({ success: true, path: '', items: roots }); + return ok(res, { path: '', items: roots }); } const matchingRoot = BROWSE_ROOTS.find(r => diff --git a/dashcaddy-api/routes/logs.js b/dashcaddy-api/routes/logs.js index b56f944..e753e6b 100644 --- a/dashcaddy-api/routes/logs.js +++ b/dashcaddy-api/routes/logs.js @@ -15,7 +15,7 @@ const { NotFoundError, ValidationError, ForbiddenError } = require('../errors'); * @param {Object} deps.dockerMaintenance - Docker maintenance module (optional) * @returns {express.Router} */ -module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance }) { +module.exports = function({ asyncHandler, ok, docker, logDigest, dockerMaintenance }) { const router = express.Router(); // List containers with logs @@ -31,7 +31,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance } const paginationParams = parsePaginationParams(req.query); const result = paginate(containerList, paginationParams); - res.json({ success: true, containers: result.data, ...(result.pagination && { pagination: result.pagination }) }); + ok(res, { containers: result.data, ...(result.pagination && { pagination: result.pagination }) }); }, 'logs-containers')); // Get logs for a specific container @@ -81,8 +81,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance } offset += 8 + size; } - res.json({ - success: true, + ok(res, { containerId, containerName, logs: lines, count: lines.length @@ -153,23 +152,23 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance } if (!logDigest) throw new Error('Log digest not available'); const digest = await logDigest.getLatestDigest(); if (!digest) { - return res.json({ success: true, digest: null, message: 'No digest available yet. First digest is generated at midnight.' }); + return ok(res, { digest: null, message: 'No digest available yet. First digest is generated at midnight.' }); } - res.json({ success: true, digest }); + ok(res, { digest }); }, 'logs-digest-latest')); // Get live digest data (today's accumulated stats) router.get('/logs/digest/live', asyncHandler(async (req, res) => { if (!logDigest) throw new Error('Log digest not available'); const live = logDigest.getLiveData(); - res.json({ success: true, ...live }); + ok(res, { ...live }); }, 'logs-digest-live')); // List available digest dates router.get('/logs/digest/history', asyncHandler(async (req, res) => { if (!logDigest) throw new Error('Log digest not available'); const dates = await logDigest.listDigests(); - res.json({ success: true, dates }); + ok(res, { dates }); }, 'logs-digest-history')); // Generate digest on demand (for today or a specific date) @@ -177,7 +176,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance } if (!logDigest) throw new Error('Log digest not available'); const date = req.body.date || new Date().toISOString().slice(0, 10); const digest = await logDigest.generateDailyDigest(date); - res.json({ success: true, digest }); + ok(res, { digest }); }, 'logs-digest-generate')); // Get digest for a specific date (JSON) @@ -196,7 +195,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance } } const digest = await logDigest.getDigestByDate(date); if (!digest) throw new NotFoundError(`Digest for ${date}`); - res.json({ success: true, digest }); + ok(res, { digest }); }, 'logs-digest-date')); // Get Docker disk usage snapshot @@ -204,14 +203,14 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance } if (!dockerMaintenance) throw new Error('Docker maintenance not available'); const diskUsage = await dockerMaintenance.getDiskUsage(); const status = dockerMaintenance.getStatus(); - res.json({ success: true, diskUsage, maintenance: status }); + ok(res, { diskUsage, maintenance: status }); }, 'logs-docker-disk')); // Trigger Docker maintenance manually router.post('/logs/docker-maintenance', asyncHandler(async (req, res) => { if (!dockerMaintenance) throw new Error('Docker maintenance not available'); const result = await dockerMaintenance.runMaintenance(); - res.json({ success: true, result }); + ok(res, { result }); }, 'logs-docker-maintenance')); // Get logs from a file path (for native applications) @@ -261,8 +260,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance } timestamp: extractTimestamp(line) })); - res.json({ - success: true, + ok(res, { logPath: normalizedPath, logs, count: logs.length, diff --git a/dashcaddy-api/routes/sites.js b/dashcaddy-api/routes/sites.js index 89557f9..f4c19a7 100644 --- a/dashcaddy-api/routes/sites.js +++ b/dashcaddy-api/routes/sites.js @@ -17,20 +17,20 @@ const { validateURL } = require('../input-validator'); * @param {Object} deps.log - Logger instance * @returns {express.Router} */ -module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addServiceToConfig, siteConfig, log }) { +module.exports = function({ asyncHandler, ok, caddy, dns, fetchT, buildDomain, addServiceToConfig, siteConfig, log }) { const router = express.Router(); // Get Caddyfile contents router.get('/caddyfile', asyncHandler(async (req, res) => { const content = await caddy.read(); - res.json({ success: true, content }); + ok(res, { content }); }, 'caddyfile-get')); // Get current Caddy config (from admin API) 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 }); + ok(res, { config }); }, 'caddy-config')); // Reload Caddy configuration via admin API @@ -49,7 +49,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe throw new Error('Caddy reload failed. Check server logs for details.'); } - res.json({ success: true, message: 'Caddy configuration reloaded successfully' }); + ok(res, { message: 'Caddy configuration reloaded successfully' }); }, 'caddy-reload')); // Get Certificate Authorities from Caddyfile @@ -152,7 +152,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe throw new NotFoundError(`Site block for "" in Caddyfile`); } - res.json({ success: true, message: `Site "${domain}" removed from Caddyfile and Caddy reloaded` }); + ok(res, { message: `Site "${domain}" removed from Caddyfile and Caddy reloaded` }); }, 'site-delete')); // Add a new site to Caddyfile and reload @@ -180,7 +180,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe result.rolledBack ? { note: 'Caddyfile was rolled back to previous state' } : {}); } - res.json({ success: true, message: `Site "${domain}" added to Caddyfile and Caddy reloaded successfully` }); + ok(res, { message: `Site "${domain}" added to Caddyfile and Caddy reloaded successfully` }); }, 'site-add')); // Add external service reverse proxy to Caddyfile @@ -260,12 +260,11 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe } } - const response = { - success: true, + const data = { message: `External service proxy for ${domain} -> ${externalUrl} created${shouldReload ? ' and Caddy reloaded' : ''}` }; - if (dnsWarning) response.warning = dnsWarning; - res.json(response); + if (dnsWarning) data.warning = dnsWarning; + ok(res, data); }, 'site-external')); return router; diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index e7f1b34..672f5cb 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -458,6 +458,7 @@ function createApp() { })); apiRouter.use(sitesRoutes({ asyncHandler: ctx.asyncHandler, + ok: ctx.ok, caddy: ctx.caddy, dns: ctx.dns, fetchT: ctx.fetchT, @@ -475,6 +476,7 @@ function createApp() { apiRouter.use('/openclaw', openClawRoutes(ctx)); apiRouter.use(logsRoutes({ asyncHandler: ctx.asyncHandler, + ok: ctx.ok, docker: ctx.docker, logDigest: ctx.logDigest, dockerMaintenance: ctx.dockerMaintenance diff --git a/dashcaddy-api/src/config/site.js b/dashcaddy-api/src/config/site.js index a03c6ef..c13be63 100644 --- a/dashcaddy-api/src/config/site.js +++ b/dashcaddy-api/src/config/site.js @@ -19,6 +19,21 @@ const siteConfig = { routingMode: 'subdomain' }; +function applyRawConfig(raw) { + siteConfig.tld = raw.tld || '.home'; + if (!siteConfig.tld.startsWith('.')) siteConfig.tld = '.' + siteConfig.tld; + siteConfig.caName = raw.caName || ''; + siteConfig.dnsServerIp = (raw.dns && raw.dns.ip) || ''; + siteConfig.dnsServerPort = (raw.dns && raw.dns.port) || CADDY.DEFAULT_DNS_PORT; + siteConfig.dashboardHost = raw.dashboardHost || `status${siteConfig.tld}`; + siteConfig.timezone = raw.timezone || 'UTC'; + siteConfig.dnsServers = raw.dnsServers || {}; + siteConfig.configurationType = raw.configurationType || 'homelab'; + siteConfig.domain = raw.domain || ''; + siteConfig.routingMode = raw.routingMode || 'subdomain'; + siteConfig.pylon = raw.pylon || null; +} + function loadSiteConfig(CONFIG_FILE, log) { try { if (fs.existsSync(CONFIG_FILE)) { @@ -35,18 +50,7 @@ function loadSiteConfig(CONFIG_FILE, log) { } } - siteConfig.tld = raw.tld || '.home'; - if (!siteConfig.tld.startsWith('.')) siteConfig.tld = '.' + siteConfig.tld; - siteConfig.caName = raw.caName || ''; - siteConfig.dnsServerIp = (raw.dns && raw.dns.ip) || ''; - siteConfig.dnsServerPort = (raw.dns && raw.dns.port) || CADDY.DEFAULT_DNS_PORT; - siteConfig.dashboardHost = raw.dashboardHost || `status${siteConfig.tld}`; - siteConfig.timezone = raw.timezone || 'UTC'; - siteConfig.dnsServers = raw.dnsServers || {}; - siteConfig.configurationType = raw.configurationType || 'homelab'; - siteConfig.domain = raw.domain || ''; - siteConfig.routingMode = raw.routingMode || 'subdomain'; - siteConfig.pylon = raw.pylon || null; + applyRawConfig(raw); } } catch (e) { if (log && log.error) { diff --git a/dashcaddy-api/src/context/caddy.js b/dashcaddy-api/src/context/caddy.js index 04837ff..642884d 100644 --- a/dashcaddy-api/src/context/caddy.js +++ b/dashcaddy-api/src/context/caddy.js @@ -44,7 +44,7 @@ async function modifyCaddyfile(CADDYFILE_PATH, reloadCaddy, modifyFn) { * Read the current Caddyfile content */ async function readCaddyfile(CADDYFILE_PATH) { - return fsp.readFile(CADDYFILE_PATH, 'utf8'); + return await fsp.readFile(CADDYFILE_PATH, 'utf8'); } /** diff --git a/dashcaddy-api/src/context/dns.js b/dashcaddy-api/src/context/dns.js index 7dec976..3cad495 100644 --- a/dashcaddy-api/src/context/dns.js +++ b/dashcaddy-api/src/context/dns.js @@ -73,6 +73,25 @@ async function refreshDnsToken(username, password, server, fetchT, log) { } } +/** + * Try to refresh the DNS token using per-server (dns..) credentials. + * Returns the refresh result on success, or null if no per-server credentials match. + */ +async function refreshWithPerServerCredentials(dnsId, serverIp, credentialManager, fetchT, log) { + for (const role of ['admin', 'readonly']) { + try { + const username = await credentialManager.retrieve(`dns.${dnsId}.${role}.username`); + const password = await credentialManager.retrieve(`dns.${dnsId}.${role}.password`); + if (username && password) { + return await refreshDnsToken(username, password, serverIp, fetchT, log); + } + } catch (err) { + log.error('dns', `Per-server ${role} credential error`, { dnsId, error: err.message }); + } + } + return null; +} + /** * Ensure we have a valid DNS token (auto-refresh if needed) */ @@ -86,17 +105,8 @@ async function ensureValidDnsToken(siteConfig, credentialManager, fetchT, log) { if (primaryIp) { const dnsId = dnsIpToDnsId(primaryIp, siteConfig); if (dnsId) { - for (const role of ['admin', 'readonly']) { - try { - const username = await credentialManager.retrieve(`dns.${dnsId}.${role}.username`); - const password = await credentialManager.retrieve(`dns.${dnsId}.${role}.password`); - if (username && password) { - return await refreshDnsToken(username, password, primaryIp, fetchT, log); - } - } catch (err) { - log.error('dns', `Per-server ${role} credential error`, { dnsId, error: err.message }); - } - } + const result = await refreshWithPerServerCredentials(dnsId, primaryIp, credentialManager, fetchT, log); + if (result) return result; } } diff --git a/dashcaddy-api/src/utils/http.js b/dashcaddy-api/src/utils/http.js index f82b8b2..97154ea 100644 --- a/dashcaddy-api/src/utils/http.js +++ b/dashcaddy-api/src/utils/http.js @@ -90,7 +90,8 @@ function _httpsFetch(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) { get: (k) => res.headers[k.toLowerCase()], getSetCookie: () => { const sc = res.headers['set-cookie']; - return sc ? (Array.isArray(sc) ? sc : [sc]) : []; + if (!sc) return []; + return Array.isArray(sc) ? sc : [sc]; } }, }); @@ -152,7 +153,8 @@ function _httpFetch(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) { get: (k) => res.headers[k.toLowerCase()], getSetCookie: () => { const sc = res.headers['set-cookie']; - return sc ? (Array.isArray(sc) ? sc : [sc]) : []; + if (!sc) return []; + return Array.isArray(sc) ? sc : [sc]; } }, }); diff --git a/dashcaddy-api/src/utils/logging.js b/dashcaddy-api/src/utils/logging.js index 993d93f..8e1ee7c 100644 --- a/dashcaddy-api/src/utils/logging.js +++ b/dashcaddy-api/src/utils/logging.js @@ -14,7 +14,6 @@ * log.audit({ action: 'service.create', resource: 'nginx', outcome: 'success', ip, details }); */ -const fs = require('fs'); const fsp = require('fs').promises; const path = require('path'); const crypto = require('crypto'); @@ -49,8 +48,6 @@ const C = { cyan: '\x1b[36m', }; -const LEVEL_COLOUR = { debug: C.dim, info: C.green, warn: C.yellow, error: C.red }; - const LEVEL_PREFIX = { debug: `${C.dim}[DBG]${C.reset}`, info: `${C.green}[INF]${C.reset}`, @@ -71,7 +68,6 @@ function formatTime() { function consoleWrite(level, ctx, msg, data) { if (GLOBAL_LEVEL > LEVELS[level]) return; if (IS_DEV) { - const colour = LEVEL_COLOUR[level] || C.reset; const parts = [ `${C.dim}${formatTime()}${C.reset}`, LEVEL_PREFIX[level], @@ -81,15 +77,20 @@ function consoleWrite(level, ctx, msg, data) { if (data && typeof data === 'object' && !(data instanceof Error)) { parts.push(`${C.dim}${JSON.stringify(data)}${C.reset}`); } - const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log; + let fn = console.log; + if (level === 'error') fn = console.error; + else if (level === 'warn') fn = console.warn; fn(parts.join(' ')); } else { - const entry = { - t: new Date().toISOString(), level, ctx, msg, - ...(data instanceof Error - ? { error: { message: data.message, code: data.code, stack: data.stack } } - : (data && typeof data === 'object' ? { data } : {})), - }; + let extra; + if (data instanceof Error) { + extra = { error: { message: data.message, code: data.code, stack: data.stack } }; + } else if (data && typeof data === 'object') { + extra = { data }; + } else { + extra = {}; + } + const entry = { t: new Date().toISOString(), level, ctx, msg, ...extra }; (level === 'error' ? console.error : console.info)(JSON.stringify(entry)); } } @@ -197,7 +198,13 @@ function sanitize(obj) { if (!obj || typeof obj !== 'object') return obj; const clean = Array.isArray(obj) ? [] : {}; for (const [k, v] of Object.entries(obj)) { - clean[k] = SENSITIVE_KEYS.includes(k) ? '***' : v && typeof v === 'object' ? sanitize(v) : v; + if (SENSITIVE_KEYS.includes(k)) { + clean[k] = '***'; + } else if (v && typeof v === 'object') { + clean[k] = sanitize(v); + } else { + clean[k] = v; + } } return clean; } @@ -270,7 +277,14 @@ class Logger extends EventEmitter { consoleWrite(level, ctx, msg, data); if (level === 'error') { - const errObj = data instanceof Error ? data : (data && data.message ? new Error(data.message) : new Error(msg)); + let errObj; + if (data instanceof Error) { + errObj = data; + } else if (data && data.message) { + errObj = new Error(data.message); + } else { + errObj = new Error(msg); + } // Await the error log write so callers using await on log.error() // can rely on the file being flushed before proceeding. return writeErrorLog(ctx, errObj, req, payload);