Refactor apps routes: explicit dependency injection

- Updated all apps route modules to use destructured dependencies
- Added JSDoc comments for factory functions
- Replaced ctx. references with direct parameter access
- Updated apps/index.js to extract and pass explicit dependencies
- All files pass syntax validation

Files refactored:
- routes/apps/deploy.js (18k lines)
- routes/apps/helpers.js (17k lines)
- routes/apps/removal.js
- routes/apps/restore.js
- routes/apps/templates.js
- routes/apps/index.js (orchestrator)
This commit is contained in:
Krystie
2026-03-29 21:36:15 -07:00
parent 6bde2eb62e
commit a4788c3f28
6 changed files with 229 additions and 153 deletions

View File

@@ -6,12 +6,23 @@ const { REGEX, DOCKER } = require('../../constants');
const { exists } = require('../../fs-helpers');
const platformPaths = require('../../platform-paths');
module.exports = function(ctx) {
/**
* Apps helpers factory
* @param {Object} deps - Explicit dependencies
* @param {Object} deps.docker - Docker client wrapper
* @param {Object} deps.caddy - Caddy client
* @param {Object} deps.credentialManager - Credential manager
* @param {Object} deps.servicesStateManager - Services state manager
* @param {Function} deps.fetchT - Timeout-wrapped fetch
* @param {Object} deps.log - Logger instance
* @returns {Object} Helper functions
*/
module.exports = function({ docker, caddy, credentialManager, servicesStateManager, fetchT, log }) {
async function checkPortConflicts(ports, excludeContainerName = null) {
const conflicts = [];
try {
const containers = await ctx.docker.client.listContainers({ all: true });
const containers = await docker.client.listContainers({ all: true });
for (const container of containers) {
if (excludeContainerName && container.Names.some(n => n === `/${excludeContainerName}`)) continue;
if (container.State !== 'running') continue;
@@ -27,14 +38,14 @@ module.exports = function(ctx) {
}
}
} catch (e) {
ctx.log.warn('docker', 'Could not check port conflicts', { error: e.message });
log.warn('docker', 'Could not check port conflicts', { error: e.message });
}
return conflicts;
}
async function findExistingContainerByImage(template) {
try {
const containers = await ctx.docker.client.listContainers({ all: false });
const containers = await docker.client.listContainers({ all: false });
const templateImage = template.docker.image.split(':')[0];
for (const container of containers) {
const containerImage = container.Image.split(':')[0];
@@ -53,7 +64,7 @@ module.exports = function(ctx) {
}
return null;
} catch (e) {
ctx.log.warn('docker', 'Could not check for existing containers', { error: e.message });
log.warn('docker', 'Could not check for existing containers', { error: e.message });
return null;
}
}
@@ -140,7 +151,7 @@ module.exports = function(ctx) {
normalizedHost === root || normalizedHost.startsWith(root + path.sep)
);
if (!isAllowed) {
ctx.log.warn('deploy', 'Custom volume host path rejected', { hostPath: override.hostPath, allowed: allowedRoots });
log.warn('deploy', 'Custom volume host path rejected', { hostPath: override.hostPath, allowed: allowedRoots });
return vol; // Keep original volume, don't apply unsafe override
}
return `${toDockerDesktopPath(override.hostPath)}:${containerPath}`;
@@ -243,39 +254,39 @@ module.exports = function(ctx) {
for (let i = 0; i < maxAttempts; i++) {
try {
const container = ctx.docker.client.getContainer(containerId);
const container = docker.client.getContainer(containerId);
const info = await container.inspect();
if (info.State.Running) {
if (info.State.Health) {
if (info.State.Health.Status === 'healthy') {
ctx.log.info('docker', 'Container is healthy (Docker health check)', { containerId });
log.info('docker', 'Container is healthy (Docker health check)', { containerId });
return true;
}
} else if (healthPath && port && httpCheckFailed < 5) {
try {
const response = await ctx.fetchT(`http://localhost:${port}${healthPath}`, {
const response = await fetchT(`http://localhost:${port}${healthPath}`, {
signal: AbortSignal.timeout(3000), redirect: 'manual'
});
if (response.ok || (response.status >= 300 && response.status < 400)) {
ctx.log.info('docker', 'Health check passed', { containerId, status: response.status });
log.info('docker', 'Health check passed', { containerId, status: response.status });
return true;
}
} catch (e) {
httpCheckFailed++;
ctx.log.debug('docker', 'HTTP health check failed', { attempt: httpCheckFailed, error: e.message });
log.debug('docker', 'HTTP health check failed', { attempt: httpCheckFailed, error: e.message });
}
} else {
if (i >= 5) {
ctx.log.info('docker', 'Container is running', { containerId, waitedSeconds: i * delay / 1000 });
log.info('docker', 'Container is running', { containerId, waitedSeconds: i * delay / 1000 });
return true;
}
}
}
} catch (e) {
ctx.log.debug('docker', 'Health check attempt failed', { attempt: i + 1, error: e.message });
log.debug('docker', 'Health check attempt failed', { attempt: i + 1, error: e.message });
}
if (i < maxAttempts - 1) {
ctx.log.debug('docker', 'Waiting for container to be healthy', { attempt: i + 1, maxAttempts });
log.debug('docker', 'Waiting for container to be healthy', { attempt: i + 1, maxAttempts });
await new Promise(resolve => setTimeout(resolve, delay));
}
}
@@ -284,15 +295,15 @@ module.exports = function(ctx) {
async function addCaddyConfig(subdomain, config) {
const domain = ctx.buildDomain(subdomain);
const existing = await ctx.caddy.read();
const existing = await caddy.read();
if (existing.includes(`${domain} {`)) {
ctx.log.info('caddy', 'Caddy config already exists, skipping add', { domain });
await ctx.caddy.reload(existing);
log.info('caddy', 'Caddy config already exists, skipping add', { domain });
await caddy.reload(existing);
return;
}
const result = await ctx.caddy.modify(c => c + `\n${config}\n`);
const result = await caddy.modify(c => c + `\n${config}\n`);
if (!result.success) throw new Error(`[DC-303] Failed to add Caddy config for ${domain}: ${result.error}`);
await ctx.caddy.verifySite(domain);
await caddy.verifySite(domain);
}
// Reserved paths that cannot be used as subpath names in subdirectory mode
@@ -303,7 +314,7 @@ module.exports = function(ctx) {
async function ensureMainDomainBlock() {
if (ctx.siteConfig.routingMode !== 'subdirectory' || !ctx.siteConfig.domain) return;
const content = await ctx.caddy.read();
const content = await caddy.read();
const domain = ctx.siteConfig.domain;
const ROUTE_MARKER = '# === DashCaddy App Routes ===';
@@ -312,7 +323,7 @@ module.exports = function(ctx) {
// Domain block exists but lacks markers — inject them
if (content.includes(`${domain} {`)) {
const result = await ctx.caddy.modify(c => {
const result = await caddy.modify(c => {
// Insert markers before the final catch-all handle block inside the domain block
const domainStart = c.indexOf(`${domain} {`);
// Find standalone "handle {" (catch-all SPA fallback) — match tabs or spaces
@@ -325,7 +336,7 @@ module.exports = function(ctx) {
return c.slice(0, handleIdx) + markerBlock + c.slice(handleIdx);
});
if (result.success) {
ctx.log.info('caddy', 'Injected route markers into existing domain block', { domain });
log.info('caddy', 'Injected route markers into existing domain block', { domain });
}
return;
}
@@ -335,9 +346,9 @@ module.exports = function(ctx) {
const apiPort = process.env.PORT || 3001;
const block = `\n${domain} {\n root * ${dashboardRoot}\n encode gzip\n\n handle /api/* {\n reverse_proxy localhost:${apiPort}\n }\n\n handle /probe/* {\n reverse_proxy localhost:${apiPort}\n }\n\n ${ROUTE_MARKER}\n # === End App Routes ===\n\n handle {\n @notFile not file {path}\n rewrite @notFile /index.html\n file_server\n }\n}\n`;
const result = await ctx.caddy.modify(c => c + block);
const result = await caddy.modify(c => c + block);
if (result.success) {
ctx.log.info('caddy', 'Created main domain block with route markers', { domain });
log.info('caddy', 'Created main domain block with route markers', { domain });
} else {
throw new Error(`[DC-303] Failed to create main domain block for ${domain}: ${result.error}`);
}
@@ -349,9 +360,9 @@ module.exports = function(ctx) {
const endMarker = `# --- End: ${subdomain} ---`;
const END_ROUTE_MARKER = '# === End App Routes ===';
const result = await ctx.caddy.modify(content => {
const result = await caddy.modify(content => {
if (content.includes(marker)) {
ctx.log.info('caddy', 'Subpath config already exists, skipping', { subdomain });
log.info('caddy', 'Subpath config already exists, skipping', { subdomain });
return null;
}
@@ -378,7 +389,7 @@ module.exports = function(ctx) {
const marker = `# --- DashCaddy: ${subdomain} ---`;
const endMarker = `# --- End: ${subdomain} ---`;
return await ctx.caddy.modify(content => {
return await caddy.modify(content => {
const startIdx = content.indexOf(marker);
if (startIdx === -1) return null;