fix(routes): complete post-refactor dependency wiring cleanup

This commit is contained in:
Krystie
2026-05-02 20:43:39 -07:00
parent 4eebb3ce7a
commit 0c658a26a8
32 changed files with 495 additions and 396 deletions

View File

@@ -53,5 +53,16 @@ module.exports = {
'max-depth': 'off', 'max-depth': 'off',
}, },
}, },
{
// Frontend assets use browser globals
files: ['assets/**/*.js', 'frontend/**/*.js'],
env: {
browser: true,
es2021: true,
},
rules: {
'no-undef': 'warn',
},
},
], ],
}; };

View File

@@ -1,308 +1,308 @@
/** /**
* Theme Adapter * Theme Adapter
* Ensures tooltips match the current dashboard theme * Ensures tooltips match the current dashboard theme
* Integrates with Driver.js to apply theme-specific styling * Integrates with Driver.js to apply theme-specific styling
*/ */
(function(window) { (function(window) {
'use strict'; 'use strict';
/** /**
* Theme configuration mapping for Driver.js * Theme configuration mapping for Driver.js
* Maps dashboard themes to Driver.js styling * Maps dashboard themes to Driver.js styling
*/ */
const THEME_CONFIGS = { const THEME_CONFIGS = {
dark: { dark: {
backgroundColor: 'var(--card-base)', backgroundColor: 'var(--card-base)',
textColor: 'var(--fg)', textColor: 'var(--fg)',
primaryColor: 'var(--accent)', primaryColor: 'var(--accent)',
overlayColor: 'rgba(0, 0, 0, 0.7)', overlayColor: 'rgba(0, 0, 0, 0.7)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
highlightColor: 'var(--accent)', highlightColor: 'var(--accent)',
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
}, },
light: { light: {
backgroundColor: 'var(--card-base)', backgroundColor: 'var(--card-base)',
textColor: 'var(--fg)', textColor: 'var(--fg)',
primaryColor: 'var(--accent-strong)', primaryColor: 'var(--accent-strong)',
overlayColor: 'rgba(0, 0, 0, 0.5)', overlayColor: 'rgba(0, 0, 0, 0.5)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
highlightColor: 'var(--accent-strong)', highlightColor: 'var(--accent-strong)',
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
}, },
blue: { blue: {
backgroundColor: 'var(--card-base)', backgroundColor: 'var(--card-base)',
textColor: 'var(--fg)', textColor: 'var(--fg)',
primaryColor: 'var(--accent)', primaryColor: 'var(--accent)',
overlayColor: 'rgba(25, 8, 172, 0.7)', overlayColor: 'rgba(25, 8, 172, 0.7)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
highlightColor: 'var(--accent)', highlightColor: 'var(--accent)',
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
}, },
nord: { nord: {
backgroundColor: 'var(--card-base)', backgroundColor: 'var(--card-base)',
textColor: 'var(--fg)', textColor: 'var(--fg)',
primaryColor: 'var(--accent)', primaryColor: 'var(--accent)',
overlayColor: 'rgba(46, 52, 64, 0.7)', overlayColor: 'rgba(46, 52, 64, 0.7)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
highlightColor: 'var(--accent)', highlightColor: 'var(--accent)',
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
}, },
dracula: { dracula: {
backgroundColor: 'var(--card-base)', backgroundColor: 'var(--card-base)',
textColor: 'var(--fg)', textColor: 'var(--fg)',
primaryColor: 'var(--accent)', primaryColor: 'var(--accent)',
overlayColor: 'rgba(40, 42, 54, 0.7)', overlayColor: 'rgba(40, 42, 54, 0.7)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
highlightColor: 'var(--accent)', highlightColor: 'var(--accent)',
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
}, },
'solarized-dark': { 'solarized-dark': {
backgroundColor: 'var(--card-base)', backgroundColor: 'var(--card-base)',
textColor: 'var(--fg)', textColor: 'var(--fg)',
primaryColor: 'var(--accent)', primaryColor: 'var(--accent)',
overlayColor: 'rgba(0, 43, 54, 0.7)', overlayColor: 'rgba(0, 43, 54, 0.7)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
highlightColor: 'var(--accent)', highlightColor: 'var(--accent)',
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
}, },
'solarized-light': { 'solarized-light': {
backgroundColor: 'var(--card-base)', backgroundColor: 'var(--card-base)',
textColor: 'var(--fg)', textColor: 'var(--fg)',
primaryColor: 'var(--accent)', primaryColor: 'var(--accent)',
overlayColor: 'rgba(253, 246, 227, 0.7)', overlayColor: 'rgba(253, 246, 227, 0.7)',
borderColor: 'var(--border)', borderColor: 'var(--border)',
highlightColor: 'var(--accent)', highlightColor: 'var(--accent)',
fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fontFamily: "'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"
} }
}; };
/** /**
* ThemeAdapter class * ThemeAdapter class
* Manages theme integration for the tooltip system * Manages theme integration for the tooltip system
*/ */
class ThemeAdapter { class ThemeAdapter {
constructor() { constructor() {
this.currentTheme = this.getCurrentTheme(); this.currentTheme = this.getCurrentTheme();
this.themeChangeCallbacks = []; this.themeChangeCallbacks = [];
this._setupThemeChangeListener(); this._setupThemeChangeListener();
} }
/** /**
* Get the current theme name from document root class * Get the current theme name from document root class
* @returns {string} Current theme name (e.g., 'dark', 'light', 'blue') * @returns {string} Current theme name (e.g., 'dark', 'light', 'blue')
*/ */
getCurrentTheme() { getCurrentTheme() {
const root = document.documentElement; const root = document.documentElement;
const classList = Array.from(root.classList); const classList = Array.from(root.classList);
// Check for theme classes // Check for theme classes
const themeClasses = ['light', 'blue', 'nord', 'dracula', 'solarized-dark', 'solarized-light']; const themeClasses = ['light', 'blue', 'nord', 'dracula', 'solarized-dark', 'solarized-light'];
const foundTheme = themeClasses.find(theme => classList.includes(theme)); const foundTheme = themeClasses.find(theme => classList.includes(theme));
// Default to 'dark' if no theme class found // Default to 'dark' if no theme class found
return foundTheme || 'dark'; return foundTheme || 'dark';
} }
/** /**
* Get Driver.js theme configuration for current theme * Get Driver.js theme configuration for current theme
* @returns {Object} Theme configuration object * @returns {Object} Theme configuration object
*/ */
getDriverTheme() { getDriverTheme() {
const themeName = this.getCurrentTheme(); const themeName = this.getCurrentTheme();
const config = THEME_CONFIGS[themeName] || THEME_CONFIGS.dark; const config = THEME_CONFIGS[themeName] || THEME_CONFIGS.dark;
// Resolve CSS variables to actual values // Resolve CSS variables to actual values
const resolvedConfig = {}; const resolvedConfig = {};
for (const [key, value] of Object.entries(config)) { for (const [key, value] of Object.entries(config)) {
if (typeof value === 'string' && value.startsWith('var(')) { if (typeof value === 'string' && value.startsWith('var(')) {
// Extract CSS variable name // Extract CSS variable name
const varName = value.match(/var\((--[^)]+)\)/)?.[1]; const varName = value.match(/var\((--[^)]+)\)/)?.[1];
if (varName) { if (varName) {
const computedValue = getComputedStyle(document.documentElement) const computedValue = getComputedStyle(document.documentElement)
.getPropertyValue(varName) .getPropertyValue(varName)
.trim(); .trim();
resolvedConfig[key] = computedValue || value; resolvedConfig[key] = computedValue || value;
} else { } else {
resolvedConfig[key] = value; resolvedConfig[key] = value;
} }
} else { } else {
resolvedConfig[key] = value; resolvedConfig[key] = value;
} }
} }
return resolvedConfig; return resolvedConfig;
} }
/** /**
* Register a callback for theme changes * Register a callback for theme changes
* @param {Function} callback - Function to call when theme changes * @param {Function} callback - Function to call when theme changes
*/ */
onThemeChange(callback) { onThemeChange(callback) {
if (typeof callback === 'function') { if (typeof callback === 'function') {
this.themeChangeCallbacks.push(callback); this.themeChangeCallbacks.push(callback);
} }
} }
/** /**
* Setup theme change listener using MutationObserver * Setup theme change listener using MutationObserver
* @private * @private
*/ */
_setupThemeChangeListener() { _setupThemeChangeListener() {
const root = document.documentElement; const root = document.documentElement;
// Create observer to watch for class changes on root element // Create observer to watch for class changes on root element
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const newTheme = this.getCurrentTheme(); const newTheme = this.getCurrentTheme();
if (newTheme !== this.currentTheme) { if (newTheme !== this.currentTheme) {
const oldTheme = this.currentTheme; const oldTheme = this.currentTheme;
this.currentTheme = newTheme; this.currentTheme = newTheme;
this._notifyThemeChange(newTheme, oldTheme); this._notifyThemeChange(newTheme, oldTheme);
} }
} }
}); });
}); });
// Start observing // Start observing
observer.observe(root, { observer.observe(root, {
attributes: true, attributes: true,
attributeFilter: ['class'] attributeFilter: ['class']
}); });
console.log('[ThemeAdapter] Theme change listener initialized'); console.log('[ThemeAdapter] Theme change listener initialized');
} }
/** /**
* Notify all registered callbacks of theme change * Notify all registered callbacks of theme change
* @private * @private
* @param {string} newTheme - New theme name * @param {string} newTheme - New theme name
* @param {string} oldTheme - Old theme name * @param {string} oldTheme - Old theme name
*/ */
_notifyThemeChange(newTheme, oldTheme) { _notifyThemeChange(newTheme, oldTheme) {
console.log(`[ThemeAdapter] Theme changed: ${oldTheme}${newTheme}`); console.log(`[ThemeAdapter] Theme changed: ${oldTheme}${newTheme}`);
this.themeChangeCallbacks.forEach(callback => { this.themeChangeCallbacks.forEach(callback => {
try { try {
callback(newTheme, oldTheme); callback(newTheme, oldTheme);
} catch (error) { } catch (error) {
console.error('[ThemeAdapter] Error in theme change callback:', error); console.error('[ThemeAdapter] Error in theme change callback:', error);
} }
}); });
} }
/** /**
* Apply theme to Driver.js instance * Apply theme to Driver.js instance
* @param {Object} driver - Driver.js instance * @param {Object} driver - Driver.js instance
*/ */
applyTheme(driver) { applyTheme(driver) {
if (!driver) { if (!driver) {
console.warn('[ThemeAdapter] No driver instance provided'); console.warn('[ThemeAdapter] No driver instance provided');
return; return;
} }
const themeConfig = this.getDriverTheme(); const themeConfig = this.getDriverTheme();
// Apply theme configuration to driver // Apply theme configuration to driver
// Note: Driver.js v1.0+ uses CSS variables, so we inject a style element // Note: Driver.js v1.0+ uses CSS variables, so we inject a style element
this._injectDriverStyles(themeConfig); this._injectDriverStyles(themeConfig);
console.log('[ThemeAdapter] Theme applied to driver:', this.currentTheme); console.log('[ThemeAdapter] Theme applied to driver:', this.currentTheme);
} }
/** /**
* Inject custom styles for Driver.js based on theme * Inject custom styles for Driver.js based on theme
* @private * @private
* @param {Object} themeConfig - Theme configuration * @param {Object} themeConfig - Theme configuration
*/ */
_injectDriverStyles(themeConfig) { _injectDriverStyles(themeConfig) {
// Remove existing theme styles // Remove existing theme styles
const existingStyle = document.getElementById('driver-theme-styles'); const existingStyle = document.getElementById('driver-theme-styles');
if (existingStyle) { if (existingStyle) {
existingStyle.remove(); existingStyle.remove();
} }
// Create new style element // Create new style element
const style = document.createElement('style'); const style = document.createElement('style');
style.id = 'driver-theme-styles'; style.id = 'driver-theme-styles';
style.textContent = ` style.textContent = `
.driver-popover { .driver-popover {
background: ${themeConfig.backgroundColor} !important; background: ${themeConfig.backgroundColor} !important;
color: ${themeConfig.textColor} !important; color: ${themeConfig.textColor} !important;
border: 1px solid ${themeConfig.borderColor} !important; border: 1px solid ${themeConfig.borderColor} !important;
border-radius: 12px !important; border-radius: 12px !important;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4) !important; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4) !important;
font-family: ${themeConfig.fontFamily} !important; font-family: ${themeConfig.fontFamily} !important;
} }
.driver-popover-title { .driver-popover-title {
color: ${themeConfig.textColor} !important; color: ${themeConfig.textColor} !important;
font-weight: 600 !important; font-weight: 600 !important;
font-family: ${themeConfig.fontFamily} !important; font-family: ${themeConfig.fontFamily} !important;
} }
.driver-popover-description { .driver-popover-description {
color: ${themeConfig.textColor} !important; color: ${themeConfig.textColor} !important;
font-family: ${themeConfig.fontFamily} !important; font-family: ${themeConfig.fontFamily} !important;
} }
.driver-popover-footer button { .driver-popover-footer button {
background: ${themeConfig.primaryColor} !important; background: ${themeConfig.primaryColor} !important;
color: ${themeConfig.backgroundColor} !important; color: ${themeConfig.backgroundColor} !important;
border: none !important; border: none !important;
font-family: ${themeConfig.fontFamily} !important; font-family: ${themeConfig.fontFamily} !important;
font-weight: 500 !important; font-weight: 500 !important;
} }
.driver-popover-footer button:hover { .driver-popover-footer button:hover {
opacity: 0.9 !important; opacity: 0.9 !important;
} }
.driver-popover-close-btn { .driver-popover-close-btn {
color: ${themeConfig.textColor} !important; color: ${themeConfig.textColor} !important;
} }
.driver-overlay { .driver-overlay {
background: ${themeConfig.overlayColor} !important; background: ${themeConfig.overlayColor} !important;
} }
.driver-highlighted-element { .driver-highlighted-element {
outline: 2px solid ${themeConfig.highlightColor} !important; outline: 2px solid ${themeConfig.highlightColor} !important;
outline-offset: 4px !important; outline-offset: 4px !important;
} }
.driver-popover-progress-text { .driver-popover-progress-text {
color: ${themeConfig.textColor} !important; color: ${themeConfig.textColor} !important;
opacity: 0.7 !important; opacity: 0.7 !important;
font-family: ${themeConfig.fontFamily} !important; font-family: ${themeConfig.fontFamily} !important;
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
} }
/** /**
* Get all available theme names * Get all available theme names
* @returns {string[]} Array of theme names * @returns {string[]} Array of theme names
*/ */
getAvailableThemes() { getAvailableThemes() {
return Object.keys(THEME_CONFIGS); return Object.keys(THEME_CONFIGS);
} }
/** /**
* Check if a theme is available * Check if a theme is available
* @param {string} themeName - Theme name to check * @param {string} themeName - Theme name to check
* @returns {boolean} True if theme is available * @returns {boolean} True if theme is available
*/ */
isThemeAvailable(themeName) { isThemeAvailable(themeName) {
return THEME_CONFIGS.hasOwnProperty(themeName); return Object.hasOwn(THEME_CONFIGS, themeName);
} }
} }
// Export to global scope // Export to global scope
window.ThemeAdapter = ThemeAdapter; window.ThemeAdapter = ThemeAdapter;
console.log('[ThemeAdapter] Module loaded'); console.log('[ThemeAdapter] Module loaded');
})(window); })(window);

View File

@@ -75,7 +75,7 @@ class BackupManager extends EventEmitter {
case 'monthly': case 'monthly':
intervalMs = 30 * 24 * 60 * 60 * 1000; intervalMs = 30 * 24 * 60 * 60 * 1000;
break; break;
default: default: {
// Custom interval in minutes // Custom interval in minutes
const minutes = parseInt(backup.schedule, 10); const minutes = parseInt(backup.schedule, 10);
if (!isNaN(minutes) && minutes > 0) { if (!isNaN(minutes) && minutes > 0) {
@@ -84,6 +84,7 @@ class BackupManager extends EventEmitter {
console.error(`[BackupManager] Invalid schedule for ${name}: ${backup.schedule}`); console.error(`[BackupManager] Invalid schedule for ${name}: ${backup.schedule}`);
return; return;
} }
}
} }
// Schedule the job // Schedule the job

View File

@@ -20,7 +20,7 @@ const colors = {
magenta: '\x1b[35m' magenta: '\x1b[35m'
}; };
let testResults = { const testResults = {
passed: 0, passed: 0,
failed: 0, failed: 0,
warnings: 0, warnings: 0,

View File

@@ -507,7 +507,7 @@ async function validateSecurePath(requestedPath, allowedRoots, auditLogger = nul
const suspiciousPatterns = [ const suspiciousPatterns = [
/\.\./, // .. /\.\./, // ..
/%2e%2e/i, // URL encoded .. /%2e%2e/i, // URL encoded ..
/\.\%2f/i, // .%2F (encoded ./) /\.%2f/i, // .%2F (encoded ./)
/%2e\./i, // %2E. /%2e\./i, // %2E.
/\.\\/, // .\ (Windows) /\.\\/, // .\ (Windows)
/%5c/i // URL encoded backslash /%5c/i // URL encoded backslash

View File

@@ -17,8 +17,7 @@ const platformPaths = require('../../platform-paths');
* @param {Object} deps.log - Logger instance * @param {Object} deps.log - Logger instance
* @returns {Object} Helper functions * @returns {Object} Helper functions
*/ */
module.exports = function(ctx) { module.exports = function({ docker, caddy, credentialManager, servicesStateManager, fetchT, log, ctx }) {
const { docker, caddy, credentialManager, servicesStateManager, fetchT, log } = ctx;
async function checkPortConflicts(ports, excludeContainerName = null) { async function checkPortConflicts(ports, excludeContainerName = null) {
const conflicts = []; const conflicts = [];

View File

@@ -27,17 +27,21 @@ module.exports = function(ctx) {
log: ctx.log, log: ctx.log,
// Additional context properties needed by routes // Additional context properties needed by routes
APP_TEMPLATES: ctx.APP_TEMPLATES, APP_TEMPLATES: ctx.APP_TEMPLATES,
TEMPLATE_CATEGORIES: ctx.TEMPLATE_CATEGORIES,
DIFFICULTY_LEVELS: ctx.DIFFICULTY_LEVELS,
siteConfig: ctx.siteConfig, siteConfig: ctx.siteConfig,
buildDomain: ctx.buildDomain, buildDomain: ctx.buildDomain,
buildServiceUrl: ctx.buildServiceUrl, buildServiceUrl: ctx.buildServiceUrl,
addServiceToConfig: ctx.addServiceToConfig, addServiceToConfig: ctx.addServiceToConfig,
dns: ctx.dns, dns: ctx.dns,
notification: ctx.notification, notification: ctx.notification,
safeErrorMessage: ctx.safeErrorMessage safeErrorMessage: ctx.safeErrorMessage,
SERVICES_FILE: ctx.SERVICES_FILE,
ctx: ctx
}; };
// Initialize helpers with dependencies // Initialize helpers with dependencies (ctx is the Koa context)
const helpers = initHelpers(ctx); const helpers = initHelpers({ ...deps, ctx });
// Mount sub-routes — pass full ctx so sub-routes can reference ctx.* properties // Mount sub-routes — pass full ctx so sub-routes can reference ctx.* properties
const subCtx = Object.assign({}, ctx, { helpers }); const subCtx = Object.assign({}, ctx, { helpers });

View File

@@ -1,20 +1,40 @@
const express = require('express'); const express = require('express');
const { exists } = require('../../fs-helpers'); const { exists } = require('../../fs-helpers');
const { logError } = require('../../src/utils/logging');
module.exports = function(ctx) { module.exports = function({
const { docker, caddy, servicesStateManager, asyncHandler, errorResponse, log, helpers } = ctx; docker, caddy, servicesStateManager, asyncHandler, log, helpers,
errorResponse, dns, siteConfig, buildDomain, SERVICES_FILE, safeErrorMessage
}) {
const router = express.Router(); const router = express.Router();
/**
* Apps removal routes factory // Ctx shim for backward compatibility with existing route code
* @param {Object} deps - Explicit dependencies const ctx = {
* @param {Object} deps.docker - Docker client wrapper dns,
* @param {Object} deps.caddy - Caddy client siteConfig,
* @param {Object} deps.servicesStateManager - Services state manager buildDomain,
* @param {Function} deps.asyncHandler - Async route handler wrapper SERVICES_FILE,
* @param {Object} deps.log - Logger instance safeErrorMessage
* @param {Object} deps.helpers - Apps helpers module };
* @returns {express.Router}
*/ // Remove deployed app
/**
* Apps removal routes factory
* @param {Object} deps - Explicit dependencies
* @param {Object} deps.docker - Docker client wrapper
* @param {Object} deps.caddy - Caddy client
* @param {Object} deps.servicesStateManager - Services state manager
* @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Object} deps.log - Logger instance
* @param {Object} deps.helpers - Apps helpers module
* @param {Function} deps.errorResponse - Error response helper
* @param {Object} deps.dns - DNS context
* @param {Object} deps.siteConfig - Site configuration
* @param {Function} deps.buildDomain - Build domain helper
* @param {string} deps.SERVICES_FILE - Services file path
* @param {Function} deps.safeErrorMessage - Safe error message formatter
* @returns {express.Router}
*/
router.delete('/apps/:appId', asyncHandler(async (req, res) => { router.delete('/apps/:appId', asyncHandler(async (req, res) => {
const { appId } = req.params; const { appId } = req.params;
const { containerId, subdomain, ip, deleteContainer } = req.query; const { containerId, subdomain, ip, deleteContainer } = req.query;

View File

@@ -8,14 +8,23 @@ const { DOCKER } = require('../../constants');
* @param {Object} deps.caddy - Caddy client * @param {Object} deps.caddy - Caddy client
* @param {Object} deps.servicesStateManager - Services state manager * @param {Object} deps.servicesStateManager - Services state manager
* @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Function} deps.errorResponse - Error response helper
* @param {Object} deps.log - Logger instance * @param {Object} deps.log - Logger instance
* @param {Object} deps.helpers - Apps helpers module * @param {Object} deps.helpers - Apps helpers module
* @param {Object} deps.APP_TEMPLATES - App templates registry
* @param {Object} deps.dns - DNS client
* @param {Function} deps.buildServiceUrl - Service URL builder
* @returns {express.Router} * @returns {express.Router}
*/ */
module.exports = function(ctx) { module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, errorResponse, log, helpers, APP_TEMPLATES, dns, buildServiceUrl }) {
const { docker, caddy, servicesStateManager, asyncHandler, log, helpers } = ctx;
const router = express.Router(); const router = express.Router();
const ctx = {
APP_TEMPLATES,
dns,
buildServiceUrl
};
/** /**
* Restore a single service from its deployment manifest. * Restore a single service from its deployment manifest.
* Pulls image, creates container, starts it, recreates Caddy config. * Pulls image, creates container, starts it, recreates Caddy config.
@@ -125,11 +134,6 @@ module.exports = function(ctx) {
// Static sites: just recreate Caddy config // Static sites: just recreate Caddy config
if (template?.isStaticSite) { if (template?.isStaticSite) {
log.info('restore', `Restoring static site Caddy config: ${service.name}`); log.info('restore', `Restoring static site Caddy config: ${service.name}`);
const caddyOptions = {
tailscaleOnly: manifest.caddy.tailscaleOnly,
allowedIPs: manifest.caddy.allowedIPs,
subpathSupport: manifest.caddy.subpathSupport,
};
// Static site Caddy config would need to be regenerated // Static site Caddy config would need to be regenerated
// For now, just confirm the service entry exists // For now, just confirm the service entry exists
return { return {
@@ -288,7 +292,7 @@ module.exports = function(ctx) {
const svc = services.find(s => s.id === service.id); const svc = services.find(s => s.id === service.id);
if (svc) { if (svc) {
svc.containerId = container.id; svc.containerId = container.id;
svc.url = ctx.buildServiceUrl(manifest.config.subdomain); svc.url = buildServiceUrl(manifest.config.subdomain);
} }
return services; return services;
}); });

View File

@@ -6,14 +6,40 @@ const { exists } = require('../../fs-helpers');
* @param {Object} deps.servicesStateManager - Services state manager * @param {Object} deps.servicesStateManager - Services state manager
* @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Object} deps.helpers - Apps helpers module * @param {Object} deps.helpers - Apps helpers module
* @param {Object} deps.APP_TEMPLATES - App templates registry
* @param {Object} deps.TEMPLATE_CATEGORIES - Template categories
* @param {Object} deps.DIFFICULTY_LEVELS - Difficulty levels
* @param {Object} deps.docker - Docker client
* @param {Object} deps.caddy - Caddy client
* @param {Object} deps.dns - DNS context
* @param {Object} deps.siteConfig - Site configuration
* @param {Function} deps.buildDomain - Build domain helper
* @param {Function} deps.errorResponse - Error response helper
* @param {Object} deps.log - Logger instance
* @param {string} deps.SERVICES_FILE - Services file path
* @returns {express.Router} * @returns {express.Router}
*/ */
const { REGEX } = require('../../constants'); const { REGEX } = require('../../constants');
module.exports = function(ctx) { module.exports = function({
const { servicesStateManager, asyncHandler, helpers, docker, caddy, log, errorResponse } = ctx; servicesStateManager, asyncHandler, helpers,
APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS,
docker, caddy, dns, siteConfig, buildDomain,
errorResponse, log, SERVICES_FILE
}) {
const router = express.Router(); const router = express.Router();
// Ctx shim for backward compatibility with existing route code
const ctx = {
APP_TEMPLATES,
TEMPLATE_CATEGORIES,
DIFFICULTY_LEVELS,
dns,
siteConfig,
buildDomain,
SERVICES_FILE
};
// Get available app templates // Get available app templates
router.get('/apps/templates', asyncHandler(async (req, res) => { router.get('/apps/templates', asyncHandler(async (req, res) => {
res.json({ res.json({
@@ -64,6 +90,8 @@ module.exports = function(ctx) {
// Update subdomain for deployed app // Update subdomain for deployed app
router.post('/apps/update-subdomain', asyncHandler(async (req, res) => { router.post('/apps/update-subdomain', asyncHandler(async (req, res) => {
const { serviceId, oldSubdomain, newSubdomain, containerId, ip } = req.body; const { serviceId, oldSubdomain, newSubdomain, containerId, ip } = req.body;
const { ValidationError } = require('../../errors');
if (!oldSubdomain || typeof oldSubdomain !== 'string') { if (!oldSubdomain || typeof oldSubdomain !== 'string') {
throw new ValidationError('oldSubdomain is required'); throw new ValidationError('oldSubdomain is required');
} }

View File

@@ -28,7 +28,7 @@ module.exports = function(ctx) {
const results = { radarr: null, sonarr: null }; const results = { radarr: null, sonarr: null };
// Step 1: Authenticate with Overseerr via Plex token // Step 1: Authenticate with Overseerr via Plex token
let overseerrUrl = `http://host.docker.internal:${APP_PORTS.overseerr}`; const overseerrUrl = `http://host.docker.internal:${APP_PORTS.overseerr}`;
const overseerrSession = await helpers.getOverseerrSession(); const overseerrSession = await helpers.getOverseerrSession();
if (!overseerrSession) { if (!overseerrSession) {
@@ -227,7 +227,7 @@ module.exports = function(ctx) {
} }
// Normalize URL - remove trailing slash // Normalize URL - remove trailing slash
let baseUrl = url.replace(/\/+$/, ''); const baseUrl = url.replace(/\/+$/, '');
// Build the API endpoint // Build the API endpoint
let apiEndpoint; let apiEndpoint;

View File

@@ -9,12 +9,18 @@ const { APP_PORTS } = require('../../constants');
* @param {Function} deps.errorResponse - Error response helper * @param {Function} deps.errorResponse - Error response helper
* @param {Object} deps.log - Logger instance * @param {Object} deps.log - Logger instance
* @param {Object} deps.helpers - Arr helpers module * @param {Object} deps.helpers - Arr helpers module
* @param {Object} deps.credentialManager - Credential manager
* @param {Object} deps.servicesStateManager - Services state manager
* @returns {express.Router} * @returns {express.Router}
*/ */
module.exports = function(ctx) { module.exports = function({ fetchT, asyncHandler, errorResponse, log: _log, helpers, credentialManager, servicesStateManager }) {
const { fetchT, asyncHandler, errorResponse, log, helpers } = ctx;
const router = express.Router(); const router = express.Router();
const ctx = {
credentialManager,
servicesStateManager
};
// Plex Libraries endpoint // Plex Libraries endpoint
router.get('/plex/libraries', asyncHandler(async (req, res) => { router.get('/plex/libraries', asyncHandler(async (req, res) => {
// Get Plex token // Get Plex token

View File

@@ -10,12 +10,18 @@ const { APP_PORTS } = require('../../constants');
* @param {Function} deps.errorResponse - Error response helper * @param {Function} deps.errorResponse - Error response helper
* @param {Object} deps.log - Logger instance * @param {Object} deps.log - Logger instance
* @param {Object} deps.helpers - Arr helpers module * @param {Object} deps.helpers - Arr helpers module
* @param {Object} deps.servicesStateManager - Services state manager
* @param {Object} deps.notification - Notification helper
* @returns {express.Router} * @returns {express.Router}
*/ */
module.exports = function(ctx) { module.exports = function({ credentialManager, servicesStateManager, fetchT, asyncHandler, errorResponse: _errorResponse, log: _log, helpers, notification }) {
const { credentialManager, fetchT, asyncHandler, errorResponse, log, helpers } = ctx;
const router = express.Router(); const router = express.Router();
const ctx = {
servicesStateManager,
notification
};
// Smart Connect: Unified orchestration endpoint // Smart Connect: Unified orchestration endpoint
router.post('/arr/smart-connect', asyncHandler(async (req, res) => { router.post('/arr/smart-connect', asyncHandler(async (req, res) => {
const { services: inputServices, configurePlex, configureProwlarr, configureSeerr, saveCredentials } = req.body; const { services: inputServices, configurePlex, configureProwlarr, configureSeerr, saveCredentials } = req.body;

View File

@@ -1,8 +1,6 @@
const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants'); const { SESSION_TTL, APP, PLEX, TIMEOUTS, buildMediaAuth } = require('../../constants');
const { createCache, CACHE_CONFIGS } = require('../../cache-config'); const { createCache, CACHE_CONFIGS } = require('../../cache-config');
module.exports = function({ authManager, credentialManager, fetchT, asyncHandler, errorResponse, log }) {
// App session cache for auto-login
/** /**
* Auth session handlers routes factory * Auth session handlers routes factory
* @param {Object} deps - Explicit dependencies * @param {Object} deps - Explicit dependencies
@@ -11,8 +9,11 @@ module.exports = function({ authManager, credentialManager, fetchT, asyncHandler
* @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Function} deps.errorResponse - Error response helper * @param {Function} deps.errorResponse - Error response helper
* @param {Object} deps.log - Logger instance * @param {Object} deps.log - Logger instance
* @returns {express.Router} * @param {Function} deps.fetchT - Timeout-wrapped fetch
* @returns {{ getAppSession: Function, appSessionCache: Object }}
*/ */
module.exports = function({ authManager: _authManager, credentialManager: _credentialManager, asyncHandler: _asyncHandler, errorResponse: _errorResponse, log, fetchT }) {
const ctx = { fetchT };
const appSessionCache = createCache(CACHE_CONFIGS.appSessions); const appSessionCache = createCache(CACHE_CONFIGS.appSessions);
async function getAppSession(serviceId, baseUrl, username, password) { async function getAppSession(serviceId, baseUrl, username, password) {

View File

@@ -4,7 +4,7 @@ const fsp = require('fs').promises;
const path = require('path'); const path = require('path');
const { exists, isAccessible } = require('../fs-helpers'); const { exists, isAccessible } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination'); const { paginate, parsePaginationParams } = require('../pagination');
const { ValidationError } = require('../errors'); const { ValidationError, ForbiddenError } = require('../errors');
/** /**
* Browse route factory * Browse route factory

View File

@@ -4,6 +4,7 @@ const fsp = require('fs').promises;
const path = require('path'); const path = require('path');
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const { exists } = require('../fs-helpers'); const { exists } = require('../fs-helpers');
const { ValidationError } = require('../errors');
const platformPaths = require('../platform-paths'); const platformPaths = require('../platform-paths');
module.exports = function(ctx) { module.exports = function(ctx) {

View File

@@ -22,9 +22,9 @@ try {
// Image processing libraries not available — favicon conversion disabled // Image processing libraries not available — favicon conversion disabled
} }
module.exports = function(ctx) { module.exports = function({ servicesStateManager: _servicesStateManager, asyncHandler, log: _log, CONFIG_FILE, readConfig, saveConfig, errorResponse }) {
const { servicesStateManager, asyncHandler, log } = ctx;
const router = express.Router(); const router = express.Router();
const ctx = { CONFIG_FILE, readConfig, saveConfig, errorResponse };
// ===== ASSET UPLOAD ===== // ===== ASSET UPLOAD =====
@@ -47,7 +47,6 @@ module.exports = function(ctx) {
throw new ValidationError('Invalid image data format'); throw new ValidationError('Invalid image data format');
} }
const extension = matches[1] === 'svg+xml' ? 'svg' : matches[1];
const base64Data = matches[2]; const base64Data = matches[2];
const buffer = Buffer.from(base64Data, 'base64'); const buffer = Buffer.from(base64Data, 'base64');
@@ -109,6 +108,7 @@ module.exports = function(ctx) {
// Upload custom logo(s) and/or update position and title // Upload custom logo(s) and/or update position and title
// Supports: dataDark/dataLight (separate variants) or data (single logo for both) // Supports: dataDark/dataLight (separate variants) or data (single logo for both)
// eslint-disable-next-line complexity
router.post('/logo', express.json({ limit: LIMITS.BODY_UPLOAD }), asyncHandler(async (req, res) => { router.post('/logo', express.json({ limit: LIMITS.BODY_UPLOAD }), asyncHandler(async (req, res) => {
const { data, dataDark, dataLight, position, dashboardTitle } = req.body; const { data, dataDark, dataLight, position, dashboardTitle } = req.body;
@@ -231,7 +231,6 @@ module.exports = function(ctx) {
throw new ValidationError('Invalid image data format'); throw new ValidationError('Invalid image data format');
} }
const imageType = matches[1];
const base64Data = matches[2]; const base64Data = matches[2];
const buffer = Buffer.from(base64Data, 'base64'); const buffer = Buffer.from(base64Data, 'base64');

View File

@@ -13,7 +13,10 @@ module.exports = function(ctx) {
configStateManager: ctx.configStateManager, configStateManager: ctx.configStateManager,
servicesStateManager: ctx.servicesStateManager, servicesStateManager: ctx.servicesStateManager,
asyncHandler: ctx.asyncHandler, asyncHandler: ctx.asyncHandler,
log: ctx.log log: ctx.log,
CONFIG_FILE: ctx.CONFIG_FILE,
errorResponse: ctx.errorResponse,
loadSiteConfig: ctx.loadSiteConfig
}; };
// Additional deps for backup route // Additional deps for backup route
@@ -35,8 +38,8 @@ module.exports = function(ctx) {
saveTotpConfig: ctx.saveTotpConfig saveTotpConfig: ctx.saveTotpConfig
}; };
router.use(require('./settings')(ctx)); router.use(require('./settings')(baseDeps));
router.use(require('./assets')(ctx)); router.use(require('./assets')({ ...baseDeps, readConfig: ctx.readConfig, saveConfig: ctx.saveConfig }));
router.use(require('./backup')(backupDeps)); router.use(require('./backup')(backupDeps));
return router; return router;
}; };

View File

@@ -5,13 +5,19 @@ const { ValidationError } = require('../../errors');
/** /**
* Config settings routes factory * Config settings routes factory
* @param {Object} ctx - Application context * @param {Object} deps - Explicit dependencies
* @param {Object} deps.configStateManager - Config state manager
* @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Object} deps.log - Logger instance
* @param {string} deps.CONFIG_FILE - Config file path
* @param {Function} deps.errorResponse - Error response helper
* @param {Function} deps.loadSiteConfig - Site config reload helper
* @returns {express.Router} * @returns {express.Router}
*/ */
module.exports = function(ctx) { module.exports = function({ configStateManager: _configStateManager, asyncHandler, log, CONFIG_FILE, errorResponse, loadSiteConfig }) {
const { configStateManager, asyncHandler, log } = ctx;
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const ctx = { CONFIG_FILE, errorResponse, loadSiteConfig };
// ===== DASHCADDY CONFIG ENDPOINTS ===== // ===== DASHCADDY CONFIG ENDPOINTS =====
// Server-side config storage for setup wizard (shared across all browsers/machines) // Server-side config storage for setup wizard (shared across all browsers/machines)
@@ -60,7 +66,9 @@ module.exports = function(ctx) {
config.updatedAt = new Date().toISOString(); config.updatedAt = new Date().toISOString();
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
ctx.loadSiteConfig(); // Refresh in-memory config if (typeof ctx.loadSiteConfig === 'function') {
ctx.loadSiteConfig(); // Refresh in-memory config
}
log.info('config', 'Config saved', { path: ctx.CONFIG_FILE }); log.info('config', 'Config saved', { path: ctx.CONFIG_FILE });
res.json({ success: true, message: 'Configuration saved', config, warnings }); res.json({ success: true, message: 'Configuration saved', config, warnings });

View File

@@ -8,6 +8,7 @@ const { paginate, parsePaginationParams } = require('../pagination');
const platformPaths = require('../platform-paths'); const platformPaths = require('../platform-paths');
const { resolveServiceUrl } = require('../url-resolver'); const { resolveServiceUrl } = require('../url-resolver');
const { success, error: errorResponse } = require('../response-helpers'); const { success, error: errorResponse } = require('../response-helpers');
const { ValidationError } = require('../errors');
/** /**
* Health routes factory * Health routes factory
@@ -54,7 +55,7 @@ module.exports = function({
url, url,
checkedAt: new Date().toISOString() checkedAt: new Date().toISOString()
}; };
} catch (_) {} } catch { /* ignore */ }
// Fallback to GET // Fallback to GET
try { try {

View File

@@ -4,7 +4,7 @@ const fsp = require('fs').promises;
const path = require('path'); const path = require('path');
const { exists } = require('../fs-helpers'); const { exists } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination'); const { paginate, parsePaginationParams } = require('../pagination');
const { NotFoundError } = require('../errors'); const { NotFoundError, ValidationError, ForbiddenError } = require('../errors');
/** /**
* Logs route factory * Logs route factory

View File

@@ -14,9 +14,9 @@ const { DOCKER } = require('../../constants');
* @param {Object} deps.log - Logger instance * @param {Object} deps.log - Logger instance
* @returns {express.Router} * @returns {express.Router}
*/ */
module.exports = function(ctx) { module.exports = function({ docker, credentialManager: _credentialManager, servicesStateManager: _servicesStateManager, asyncHandler, errorResponse: _errorResponse, log, addServiceToConfig, notification, APP_TEMPLATES, siteConfig, caddy, buildDomain }) {
const { docker, credentialManager, servicesStateManager, asyncHandler, errorResponse, log } = ctx;
const router = express.Router(); const router = express.Router();
const ctx = { addServiceToConfig, notification, APP_TEMPLATES, siteConfig, caddy, buildDomain };
/** /**
* Deploy a recipe — creates multiple containers as a coordinated stack * Deploy a recipe — creates multiple containers as a coordinated stack
@@ -24,6 +24,7 @@ module.exports = function(ctx) {
* POST /api/recipes/deploy * POST /api/recipes/deploy
* Body: { recipeId, config: { selectedComponents, sharedConfig, componentOverrides } } * Body: { recipeId, config: { selectedComponents, sharedConfig, componentOverrides } }
*/ */
// eslint-disable-next-line complexity
router.post('/deploy', asyncHandler(async (req, res) => { router.post('/deploy', asyncHandler(async (req, res) => {
const { recipeId, config } = req.body; const { recipeId, config } = req.body;
const { RECIPE_TEMPLATES } = require('../../recipe-templates'); const { RECIPE_TEMPLATES } = require('../../recipe-templates');
@@ -44,7 +45,6 @@ module.exports = function(ctx) {
// Generate shared passwords for the recipe (consistent across components) // Generate shared passwords for the recipe (consistent across components)
const generatedPasswords = {}; const generatedPasswords = {};
const passwordKey = `recipe-${recipeId}-${Date.now()}`;
generatedPasswords.default = crypto.randomBytes(24).toString('base64url'); generatedPasswords.default = crypto.randomBytes(24).toString('base64url');
// Create Docker network if defined // Create Docker network if defined
@@ -185,6 +185,7 @@ module.exports = function(ctx) {
/** /**
* Deploy a single component of a recipe * Deploy a single component of a recipe
*/ */
// eslint-disable-next-line complexity
async function deployComponent(component, recipe, config, passwords, networkName) { async function deployComponent(component, recipe, config, passwords, networkName) {
const sharedConfig = config.sharedConfig || {}; const sharedConfig = config.sharedConfig || {};
const overrides = config.componentOverrides?.[component.id] || {}; const overrides = config.componentOverrides?.[component.id] || {};
@@ -364,7 +365,7 @@ module.exports = function(ctx) {
/** /**
* Run auto-connect steps after recipe deployment * Run auto-connect steps after recipe deployment
*/ */
async function runAutoConnect(recipe, deployedComponents, config) { async function runAutoConnect(recipe, _deployedComponents, _config) {
if (!recipe.autoConnect?.steps) return; if (!recipe.autoConnect?.steps) return;
// Wait for services to be fully ready // Wait for services to be fully ready

View File

@@ -17,7 +17,13 @@ module.exports = function(ctx) {
servicesStateManager: ctx.servicesStateManager, servicesStateManager: ctx.servicesStateManager,
asyncHandler: ctx.asyncHandler, asyncHandler: ctx.asyncHandler,
errorResponse: ctx.errorResponse, errorResponse: ctx.errorResponse,
log: ctx.log log: ctx.log,
notification: ctx.notification,
buildDomain: ctx.buildDomain,
caddy: ctx.caddy,
addServiceToConfig: ctx.addServiceToConfig,
APP_TEMPLATES: ctx.APP_TEMPLATES,
siteConfig: ctx.siteConfig
}; };
// All recipe routes require premium license // All recipe routes require premium license

View File

@@ -2,9 +2,12 @@ const express = require('express');
const { DOCKER } = require('../../constants'); const { DOCKER } = require('../../constants');
const { NotFoundError } = require('../../errors'); const { NotFoundError } = require('../../errors');
module.exports = function(ctx) { module.exports = function({ servicesStateManager, asyncHandler, log, docker, notification, buildDomain, caddy }) {
const { servicesStateManager, asyncHandler, log } = ctx;
const router = express.Router(); const router = express.Router();
// Ctx shim for backward compatibility
const ctx = { docker, notification, buildDomain, caddy };
/** /**
* Recipes management routes factory * Recipes management routes factory
* @param {Object} deps - Explicit dependencies * @param {Object} deps - Explicit dependencies
@@ -303,7 +306,7 @@ module.exports = function(ctx) {
*/ */
async function removeCaddyBlock(subdomain) { async function removeCaddyBlock(subdomain) {
const domain = ctx.buildDomain(subdomain); const domain = ctx.buildDomain(subdomain);
let content = await ctx.caddy.read(); const content = await ctx.caddy.read();
// Find and remove the block for this domain // Find and remove the block for this domain
const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

View File

@@ -8,6 +8,7 @@ const { APP, REGEX, TIMEOUTS } = require('../constants');
const { validateServiceConfig, isValidPort } = require('../input-validator'); const { validateServiceConfig, isValidPort } = require('../input-validator');
const { exists } = require('../fs-helpers'); const { exists } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination'); const { paginate, parsePaginationParams } = require('../pagination');
const { ValidationError, NotFoundError } = require('../errors');
const { resolveServiceUrl } = require('../url-resolver'); const { resolveServiceUrl } = require('../url-resolver');
const { success, error: errorResponse } = require('../response-helpers'); const { success, error: errorResponse } = require('../response-helpers');
const { ConflictError, ValidationError, NotFoundError } = require('../errors'); const { ConflictError, ValidationError, NotFoundError } = require('../errors');
@@ -470,7 +471,7 @@ module.exports = function({
const oldDomain = buildDomain(oldSubdomain); const oldDomain = buildDomain(oldSubdomain);
const newDomain = buildDomain(newSubdomain); const newDomain = buildDomain(newSubdomain);
let content = await caddy.read(); const content = await caddy.read();
const escapedOldDomain = oldDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedOldDomain = oldDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const siteBlockRegex = new RegExp( const siteBlockRegex = new RegExp(

View File

@@ -164,7 +164,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
const upstreamRegex = /^[a-z0-9.-]+:\d{1,5}$/i; const upstreamRegex = /^[a-z0-9.-]+:\d{1,5}$/i;
if (!upstreamRegex.test(upstream)) throw new ValidationError('Invalid upstream format. Use host:port'); if (!upstreamRegex.test(upstream)) throw new ValidationError('Invalid upstream format. Use host:port');
let content = await caddy.read(); const content = await caddy.read();
const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const siteBlockRegex = new RegExp(`\\n?${escapedDomain}\\s*\\{`, 'g'); const siteBlockRegex = new RegExp(`\\n?${escapedDomain}\\s*\\{`, 'g');
if (siteBlockRegex.test(content)) { if (siteBlockRegex.test(content)) {
@@ -203,7 +203,6 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
const domain = buildDomain(subdomain); const domain = buildDomain(subdomain);
let dnsWarning = null; let dnsWarning = null;
try {
if (createDns) { if (createDns) {
try { try {
await dns.createRecord(subdomain, siteConfig.dnsServerIp); await dns.createRecord(subdomain, siteConfig.dnsServerIp);
@@ -267,9 +266,6 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
}; };
if (dnsWarning) response.warning = dnsWarning; if (dnsWarning) response.warning = dnsWarning;
res.json(response); res.json(response);
} catch (error) {
throw error;
}
}, 'site-external')); }, 'site-external'));
return router; return router;

View File

@@ -152,7 +152,7 @@ module.exports = function({
throw new ValidationError('subdomain is required'); throw new ValidationError('subdomain is required');
} }
let content = await caddy.read(); const content = await caddy.read();
const domain = buildDomain(subdomain); const domain = buildDomain(subdomain);
const blockRegex = new RegExp(`(${domain.replace('.', '\\.')}\\s*\\{[^}]*\\})`, 's'); const blockRegex = new RegExp(`(${domain.replace('.', '\\.')}\\s*\\{[^}]*\\})`, 's');

View File

@@ -88,7 +88,7 @@ class SelfUpdater extends EventEmitter {
let commit = null; let commit = null;
try { try {
commit = fs.readFileSync(path.join(__dirname, 'VERSION'), 'utf8').trim(); commit = fs.readFileSync(path.join(__dirname, 'VERSION'), 'utf8').trim();
} catch (_) {} } catch { /* ignore */ }
return { version: pkg.version, commit }; return { version: pkg.version, commit };
} catch (e) { } catch (e) {
return { version: '0.0.0', commit: null }; return { version: '0.0.0', commit: null };
@@ -164,7 +164,7 @@ class SelfUpdater extends EventEmitter {
} catch (dlErr) { } catch (dlErr) {
console.warn('[SelfUpdater] Primary download failed:', dlErr.message, '— trying mirror'); console.warn('[SelfUpdater] Primary download failed:', dlErr.message, '— trying mirror');
// Ensure file is fully cleaned up before mirror attempt // Ensure file is fully cleaned up before mirror attempt
try { fs.unlinkSync(tarballPath); } catch (_) {} try { fs.unlinkSync(tarballPath); } catch { /* ignore */ }
await this._downloadFile(mirrorUrl, tarballPath); await this._downloadFile(mirrorUrl, tarballPath);
} }
@@ -473,7 +473,7 @@ class SelfUpdater extends EventEmitter {
const sub = path.join(baseDir, entry, name); const sub = path.join(baseDir, entry, name);
if (fs.existsSync(sub)) return sub; if (fs.existsSync(sub)) return sub;
} }
} catch (_) {} } catch { /* ignore */ }
return null; return null;
} }
@@ -512,7 +512,7 @@ class SelfUpdater extends EventEmitter {
async _cleanDir(dir) { async _cleanDir(dir) {
try { try {
await fsp.rm(dir, { recursive: true, force: true }); await fsp.rm(dir, { recursive: true, force: true });
} catch (_) {} } catch { /* ignore */ }
await fsp.mkdir(dir, { recursive: true }); await fsp.mkdir(dir, { recursive: true });
} }
} }

View File

@@ -67,8 +67,8 @@ process.on('uncaughtException', (error) => {
// Optional modules // Optional modules
let dockerMaintenance, logDigest; let dockerMaintenance, logDigest;
try { dockerMaintenance = require('./docker-maintenance'); } catch (_) {} try { dockerMaintenance = require('./docker-maintenance'); } catch { /* optional */ }
try { logDigest = require('./log-digest'); } catch (_) {} try { logDigest = require('./log-digest'); } catch { /* optional */ }
log.info('server', 'Starting feature modules'); log.info('server', 'Starting feature modules');
@@ -188,7 +188,7 @@ process.on('uncaughtException', (error) => {
}); });
// Graceful shutdown // Graceful shutdown
function shutdown(signal) { const shutdown = (signal) => {
log.info('shutdown', `${signal} received, draining connections...`); log.info('shutdown', `${signal} received, draining connections...`);
const resourceMonitor = require('./resource-monitor'); const resourceMonitor = require('./resource-monitor');
@@ -206,12 +206,12 @@ process.on('uncaughtException', (error) => {
try { try {
const dockerMaintenance = require('./docker-maintenance'); const dockerMaintenance = require('./docker-maintenance');
dockerMaintenance.stop(); dockerMaintenance.stop();
} catch (_) {} } catch { /* optional */ }
try { try {
const logDigest = require('./log-digest'); const logDigest = require('./log-digest');
logDigest.stop(); logDigest.stop();
} catch (_) {} } catch { /* optional */ }
server.close(() => { server.close(() => {
log.info('shutdown', 'HTTP server closed'); log.info('shutdown', 'HTTP server closed');
@@ -220,7 +220,7 @@ process.on('uncaughtException', (error) => {
// Force exit after 5s if connections don't drain // Force exit after 5s if connections don't drain
setTimeout(() => process.exit(0), 5000).unref(); setTimeout(() => process.exit(0), 5000).unref();
} };
process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGINT', () => shutdown('SIGINT'));

View File

@@ -36,8 +36,8 @@ const { validateURL } = require('../input-validator');
// Optional modules // Optional modules
let dockerMaintenance, logDigest; let dockerMaintenance, logDigest;
try { dockerMaintenance = require('../docker-maintenance'); } catch (_) {} try { dockerMaintenance = require('../docker-maintenance'); } catch (_) { /* optional module */ }
try { logDigest = require('../log-digest'); } catch (_) {} try { logDigest = require('../log-digest'); } catch (_) { /* optional module */ }
// Templates // Templates
const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('../app-templates'); const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('../app-templates');
@@ -104,8 +104,8 @@ async function createApp() {
log.warn('server', 'CA cert not found — HTTPS calls may fail', { path: CA_CERT_PATH }); log.warn('server', 'CA cert not found — HTTPS calls may fail', { path: CA_CERT_PATH });
} }
// TOTP configuration (defaults, overridden by loadTotpConfig below) // TOTP configuration
let totpConfig = { const totpConfig = {
enabled: false, enabled: false,
sessionDuration: 'never', sessionDuration: 'never',
isSetUp: false isSetUp: false
@@ -124,7 +124,7 @@ async function createApp() {
} }
// Tailscale configuration // Tailscale configuration
let tailscaleConfig = { const tailscaleConfig = {
enabled: false, enabled: false,
requireAuth: false, requireAuth: false,
allowedTailnet: null, allowedTailnet: null,
@@ -137,7 +137,7 @@ async function createApp() {
// Helper functions needed by middleware // Helper functions needed by middleware
function isValidContainerId(id) { function isValidContainerId(id) {
const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.\-]{0,127}$/; const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
return typeof id === 'string' && CONTAINER_ID_RE.test(id); return typeof id === 'string' && CONTAINER_ID_RE.test(id);
} }

View File

@@ -6,7 +6,7 @@ const fs = require('fs');
const { validateConfig } = require('../../config-schema'); const { validateConfig } = require('../../config-schema');
const { CADDY } = require('../../constants'); const { CADDY } = require('../../constants');
let siteConfig = { const siteConfig = {
tld: '.home', tld: '.home',
caName: '', caName: '',
dnsServerIp: '', dnsServerIp: '',

View File

@@ -226,7 +226,7 @@ async function requireDnsToken(providedToken, siteConfig, credentialManager, fet
/** /**
* Create DNS record * Create DNS record
*/ */
async function createDnsRecord(subdomain, ip, siteConfig, buildDomain, fetchT, httpsAgent, log) { async function createDnsRecord(subdomain, ip, siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log) {
const tokenResult = await ensureValidDnsToken(siteConfig, credentialManager, fetchT, log); const tokenResult = await ensureValidDnsToken(siteConfig, credentialManager, fetchT, log);
if (!tokenResult.success) { if (!tokenResult.success) {
throw new Error(`DNS token not available: ${tokenResult.error}`); throw new Error(`DNS token not available: ${tokenResult.error}`);
@@ -285,7 +285,7 @@ function createDnsContext(siteConfig, buildDomain, credentialManager, fetchT, ht
const require = (providedToken) => requireDnsToken(providedToken, siteConfig, credentialManager, fetchT, log); const require = (providedToken) => requireDnsToken(providedToken, siteConfig, credentialManager, fetchT, log);
const getForServer = (server, role) => getTokenForServer(server, siteConfig, credentialManager, fetchT, log, role); const getForServer = (server, role) => getTokenForServer(server, siteConfig, credentialManager, fetchT, log, role);
const refresh = (username, password, server) => refreshDnsToken(username, password, server, fetchT, log); const refresh = (username, password, server) => refreshDnsToken(username, password, server, fetchT, log);
const create = (subdomain, ip) => createDnsRecord(subdomain, ip, siteConfig, buildDomain, fetchT, httpsAgent, log); const create = (subdomain, ip) => createDnsRecord(subdomain, ip, siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log);
const call = (server, apiPath, params) => callDns(server, apiPath, params, fetchT, httpsAgent); const call = (server, apiPath, params) => callDns(server, apiPath, params, fetchT, httpsAgent);
return { return {