Phase 2 (WIP): Extract config and utils modules
- Created src/config/ (env.js, site-config.js) - Created src/utils/ (async-handler.js, responses.js, safe-error.js) - server.js not yet modified (backward-compatible extraction)
This commit is contained in:
50
dashcaddy-api/src/config/env.js
Normal file
50
dashcaddy-api/src/config/env.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Environment variable loading and validation
|
||||
* Central place for all process.env reads
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const platformPaths = require('../../platform-paths');
|
||||
const { APP, LIMITS, CADDY } = require('../../constants');
|
||||
|
||||
// Resolve services directory from SERVICES_FILE env var
|
||||
const SERVICES_FILE = process.env.SERVICES_FILE || platformPaths.servicesFile;
|
||||
const SERVICES_DIR = path.dirname(SERVICES_FILE);
|
||||
|
||||
/**
|
||||
* Application configuration loaded from environment variables
|
||||
*/
|
||||
const config = {
|
||||
// Server
|
||||
port: APP.PORT,
|
||||
|
||||
// Caddy paths
|
||||
caddyfilePath: process.env.CADDYFILE_PATH || platformPaths.caddyfile,
|
||||
caddyAdminUrl: process.env.CADDY_ADMIN_URL || platformPaths.caddyAdminUrl,
|
||||
|
||||
// State files
|
||||
servicesFile: SERVICES_FILE,
|
||||
configFile: process.env.CONFIG_FILE || path.join(SERVICES_DIR, 'config.json'),
|
||||
dnsCredentialsFile: process.env.DNS_CREDENTIALS_FILE || path.join(SERVICES_DIR, 'dns-credentials.json'),
|
||||
tailscaleConfigFile: process.env.TAILSCALE_CONFIG_FILE || path.join(SERVICES_DIR, 'tailscale-config.json'),
|
||||
notificationsFile: process.env.NOTIFICATIONS_FILE || path.join(SERVICES_DIR, 'notifications.json'),
|
||||
totpConfigFile: process.env.TOTP_CONFIG_FILE || path.join(SERVICES_DIR, 'totp-config.json'),
|
||||
errorLogFile: process.env.ERROR_LOG_FILE || path.join(__dirname, '../../dashcaddy-errors.log'),
|
||||
licenseSecretFile: process.env.LICENSE_SECRET_FILE || path.join(__dirname, '../../.license-secret'),
|
||||
|
||||
// Limits
|
||||
maxErrorLogSize: LIMITS.ERROR_LOG_SIZE,
|
||||
|
||||
// Media browse roots (optional feature)
|
||||
browseRoots: (process.env.MEDIA_BROWSE_ROOTS || '')
|
||||
.split(',')
|
||||
.filter((r) => r.includes('='))
|
||||
.map((r) => {
|
||||
const eqIndex = r.indexOf('=');
|
||||
const containerPath = r.slice(0, eqIndex).trim();
|
||||
const hostPath = r.slice(eqIndex + 1).trim();
|
||||
return { containerPath, hostPath };
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
23
dashcaddy-api/src/config/index.js
Normal file
23
dashcaddy-api/src/config/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Configuration module
|
||||
* Central exports for all configuration loading
|
||||
*/
|
||||
|
||||
const envConfig = require('./env');
|
||||
const {
|
||||
siteConfig,
|
||||
loadSiteConfig,
|
||||
buildDomain,
|
||||
buildServiceUrl,
|
||||
} = require('./site-config');
|
||||
|
||||
module.exports = {
|
||||
// Environment config
|
||||
...envConfig,
|
||||
|
||||
// Site config
|
||||
siteConfig,
|
||||
loadSiteConfig,
|
||||
buildDomain,
|
||||
buildServiceUrl,
|
||||
};
|
||||
103
dashcaddy-api/src/config/site-config.js
Normal file
103
dashcaddy-api/src/config/site-config.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Site configuration management
|
||||
* Loads and validates site-wide settings from config.json
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const { validateConfig } = require('../../config-schema');
|
||||
const { CADDY } = require('../../constants');
|
||||
|
||||
/**
|
||||
* Site configuration state
|
||||
* Modified by loadSiteConfig()
|
||||
*/
|
||||
const siteConfig = {
|
||||
tld: '.home',
|
||||
caName: '',
|
||||
dnsServerIp: '',
|
||||
dnsServerPort: CADDY.DEFAULT_DNS_PORT,
|
||||
dashboardHost: '',
|
||||
timezone: 'UTC',
|
||||
dnsServers: {},
|
||||
configurationType: 'homelab',
|
||||
domain: '',
|
||||
routingMode: 'subdomain',
|
||||
};
|
||||
|
||||
/**
|
||||
* Load site configuration from config.json
|
||||
* @param {string} configFilePath - Path to config.json
|
||||
* @param {object} log - Logger instance (optional, may not be available at startup)
|
||||
*/
|
||||
function loadSiteConfig(configFilePath, log) {
|
||||
try {
|
||||
if (fs.existsSync(configFilePath)) {
|
||||
const raw = JSON.parse(fs.readFileSync(configFilePath, 'utf8'));
|
||||
|
||||
// Validate config and log any issues
|
||||
const { valid, errors: configErrors, warnings: configWarnings } = validateConfig(raw);
|
||||
if (log && log.warn) {
|
||||
if (!valid) {
|
||||
log.warn('config', 'Config validation errors', { errors: configErrors });
|
||||
}
|
||||
for (const w of configWarnings) {
|
||||
log.warn('config', w);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply config values
|
||||
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';
|
||||
}
|
||||
} catch (e) {
|
||||
if (log && log.error) {
|
||||
log.error('config', 'Failed to load site config', { error: e.message });
|
||||
} else {
|
||||
console.error('[ERROR] Failed to load site config:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a domain from subdomain + configured TLD or public domain
|
||||
* @param {string} subdomain - Service subdomain (e.g., 'sonarr')
|
||||
* @returns {string} Full domain (e.g., 'sonarr.home' or 'sonarr.example.com')
|
||||
*/
|
||||
function buildDomain(subdomain) {
|
||||
if (siteConfig.configurationType === 'public' && siteConfig.domain) {
|
||||
return `${subdomain}.${siteConfig.domain}`;
|
||||
}
|
||||
return `${subdomain}${siteConfig.tld}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full service URL (protocol + host + path) for a given subdomain
|
||||
* Subdirectory mode: https://example.com/sonarr
|
||||
* Subdomain mode: https://sonarr.example.com
|
||||
* @param {string} subdomain - Service subdomain
|
||||
* @returns {string} Full service URL
|
||||
*/
|
||||
function buildServiceUrl(subdomain) {
|
||||
if (siteConfig.routingMode === 'subdirectory' && siteConfig.domain) {
|
||||
return `https://${siteConfig.domain}/${subdomain}`;
|
||||
}
|
||||
return `https://${buildDomain(subdomain)}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
siteConfig,
|
||||
loadSiteConfig,
|
||||
buildDomain,
|
||||
buildServiceUrl,
|
||||
};
|
||||
41
dashcaddy-api/src/utils/async-handler.js
Normal file
41
dashcaddy-api/src/utils/async-handler.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Async route handler wrapper
|
||||
* Catches async errors and passes them to Express error middleware
|
||||
*/
|
||||
|
||||
const { AppError } = require('../../errors');
|
||||
const { safeErrorMessage } = require('./safe-error');
|
||||
|
||||
/**
|
||||
* Wrap async Express route handlers to catch errors
|
||||
* @param {Function} fn - async (req, res, next) handler
|
||||
* @param {string} [context] - label for logError (defaults to req.path)
|
||||
* @returns {Function} Express middleware
|
||||
*/
|
||||
function asyncHandler(fn, context) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
await fn(req, res, next);
|
||||
} catch (error) {
|
||||
// Let typed errors (AppError subclasses) propagate to the global error handler
|
||||
if (error instanceof AppError) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
// Log error (requires logger to be injected)
|
||||
if (req.app.locals.logError) {
|
||||
await req.app.locals.logError(context || req.path, error);
|
||||
}
|
||||
|
||||
// Send error response if headers haven't been sent
|
||||
if (!res.headersSent && req.app.locals.errorResponse) {
|
||||
req.app.locals.errorResponse(res, 500, safeErrorMessage(error));
|
||||
} else if (!res.headersSent) {
|
||||
// Fallback if errorResponse not available
|
||||
res.status(500).json({ success: false, error: safeErrorMessage(error) });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = asyncHandler;
|
||||
15
dashcaddy-api/src/utils/index.js
Normal file
15
dashcaddy-api/src/utils/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Utility functions
|
||||
* Common helpers used across the API
|
||||
*/
|
||||
|
||||
const asyncHandler = require('./async-handler');
|
||||
const { errorResponse, ok } = require('./responses');
|
||||
const { safeErrorMessage } = require('./safe-error');
|
||||
|
||||
module.exports = {
|
||||
asyncHandler,
|
||||
errorResponse,
|
||||
ok,
|
||||
safeErrorMessage,
|
||||
};
|
||||
30
dashcaddy-api/src/utils/responses.js
Normal file
30
dashcaddy-api/src/utils/responses.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Standard HTTP response helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Standard error response — always returns { success: false, error, ...extras }
|
||||
* @param {object} res - Express response object
|
||||
* @param {number} statusCode - HTTP status code
|
||||
* @param {string} message - Error message
|
||||
* @param {object} extras - Additional fields to include
|
||||
* @returns {object} Express response
|
||||
*/
|
||||
function errorResponse(res, statusCode, message, extras = {}) {
|
||||
return res.status(statusCode).json({ success: false, error: message, ...extras });
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard success response — always returns { success: true, ...data }
|
||||
* @param {object} res - Express response object
|
||||
* @param {object} data - Data to include in response
|
||||
* @returns {object} Express response
|
||||
*/
|
||||
function ok(res, data = {}) {
|
||||
return res.json({ success: true, ...data });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
errorResponse,
|
||||
ok,
|
||||
};
|
||||
31
dashcaddy-api/src/utils/safe-error.js
Normal file
31
dashcaddy-api/src/utils/safe-error.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Safe error message sanitization
|
||||
* Prevents leaking internal paths, stack traces, etc. to clients
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return a safe error message to the client without leaking internals
|
||||
* @param {Error|string} error - Error object or string
|
||||
* @returns {string} Sanitized error message safe for client consumption
|
||||
*/
|
||||
function safeErrorMessage(error) {
|
||||
const msg = error.message || String(error);
|
||||
|
||||
// Detect port conflict errors from Docker
|
||||
const portMatch = msg.match(/exposing port TCP [^:]*:(\d+)/);
|
||||
if (portMatch || msg.includes('port is already allocated') || msg.includes('ports are not available')) {
|
||||
const port = portMatch ? portMatch[1] : 'requested';
|
||||
return `[DC-200] Port ${port} is already in use. Try a different port in Advanced Options, or stop the service using that port first.`;
|
||||
}
|
||||
|
||||
// Only expose messages that are clearly user-facing (short, no paths/stack frames)
|
||||
if (msg.length < 200 && !msg.includes('/') && !msg.includes('\\') && !msg.includes(' at ')) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
return 'An internal error occurred';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
safeErrorMessage,
|
||||
};
|
||||
Reference in New Issue
Block a user