The modular refactor changed function signatures to destructured deps but left internal ctx.* references intact, causing "ctx is not defined" errors on /api/config, /api/logo, and many other endpoints. Also implements loadTotpConfig and saveTotpConfig which were left as stubs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
157 lines
6.3 KiB
JavaScript
157 lines
6.3 KiB
JavaScript
const express = require('express');
|
|
const { exists } = require('../../fs-helpers');
|
|
/**
|
|
* Apps templates routes factory
|
|
* @param {Object} deps - Explicit dependencies
|
|
* @param {Object} deps.servicesStateManager - Services state manager
|
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
|
* @param {Object} deps.helpers - Apps helpers module
|
|
* @returns {express.Router}
|
|
*/
|
|
const { REGEX } = require('../../constants');
|
|
|
|
module.exports = function(ctx) {
|
|
const { servicesStateManager, asyncHandler, helpers, docker, caddy, log, errorResponse } = ctx;
|
|
const router = express.Router();
|
|
|
|
// Get available app templates
|
|
router.get('/apps/templates', asyncHandler(async (req, res) => {
|
|
res.json({
|
|
success: true,
|
|
templates: ctx.APP_TEMPLATES,
|
|
categories: ctx.TEMPLATE_CATEGORIES,
|
|
difficultyLevels: ctx.DIFFICULTY_LEVELS
|
|
});
|
|
}, 'apps-templates'));
|
|
|
|
// Get specific app template
|
|
router.get('/apps/templates/:appId', asyncHandler(async (req, res) => {
|
|
const { appId } = req.params;
|
|
const template = ctx.APP_TEMPLATES[appId];
|
|
if (!template) {
|
|
const { NotFoundError } = require('../../errors');
|
|
throw new NotFoundError('App template');
|
|
}
|
|
res.json({ success: true, template });
|
|
}, 'apps-template-detail'));
|
|
|
|
// Check port availability
|
|
router.get('/apps/ports/:port/check', asyncHandler(async (req, res) => {
|
|
const port = req.params.port;
|
|
const conflicts = await helpers.checkPortConflicts([port]);
|
|
if (conflicts.length > 0) {
|
|
const conflict = conflicts[0];
|
|
res.json({ available: false, port, conflict: { usedBy: conflict.usedBy, app: conflict.app, containerId: conflict.containerId } });
|
|
} else {
|
|
res.json({ available: true, port });
|
|
}
|
|
}, 'check-port'));
|
|
|
|
// Get suggested available port
|
|
router.get('/apps/ports/:basePort/suggest', asyncHandler(async (req, res) => {
|
|
const basePort = parseInt(req.params.basePort) || 8080;
|
|
const maxAttempts = 100;
|
|
const usedPorts = await docker.getUsedPorts();
|
|
for (let port = basePort; port < basePort + maxAttempts; port++) {
|
|
if (!usedPorts.has(port)) {
|
|
res.json({ success: true, suggestedPort: port, basePort });
|
|
return;
|
|
}
|
|
}
|
|
errorResponse(res, 400, `No available ports found in range ${basePort}-${basePort + maxAttempts}`);
|
|
}, 'suggest-port'));
|
|
|
|
// Update subdomain for deployed app
|
|
router.post('/apps/update-subdomain', asyncHandler(async (req, res) => {
|
|
const { serviceId, oldSubdomain, newSubdomain, containerId, ip } = req.body;
|
|
if (!oldSubdomain || typeof oldSubdomain !== 'string') {
|
|
throw new ValidationError('oldSubdomain is required');
|
|
}
|
|
if (!newSubdomain || typeof newSubdomain !== 'string') {
|
|
throw new ValidationError('newSubdomain is required');
|
|
}
|
|
if (!REGEX.SUBDOMAIN.test(newSubdomain)) {
|
|
throw new ValidationError('[DC-301] Invalid subdomain format for newSubdomain');
|
|
}
|
|
log.info('deploy', 'Updating subdomain', { oldSubdomain, newSubdomain });
|
|
const results = { oldDns: null, newDns: null, caddy: null, service: null };
|
|
|
|
if (oldSubdomain && ctx.dns.getToken()) {
|
|
try {
|
|
const oldDomain = oldSubdomain.includes('.') ? oldSubdomain : ctx.buildDomain(oldSubdomain);
|
|
const result = await ctx.dns.call(ctx.siteConfig.dnsServerIp, '/api/zones/records/delete', {
|
|
token: ctx.dns.getToken(), domain: oldDomain, type: 'A', ipAddress: ip || 'localhost'
|
|
});
|
|
results.oldDns = result.status === 'ok' ? 'deleted' : result.errorMessage;
|
|
log.info('dns', 'Old DNS record deleted', { domain: oldDomain });
|
|
} catch (error) {
|
|
results.oldDns = `failed: ${error.message}`;
|
|
log.warn('dns', 'Old DNS deletion warning', { error: error.message });
|
|
}
|
|
}
|
|
|
|
if (newSubdomain && ctx.dns.getToken()) {
|
|
try {
|
|
await ctx.dns.createRecord(newSubdomain, ip || 'localhost');
|
|
results.newDns = 'created';
|
|
log.info('dns', 'New DNS record created', { domain: ctx.buildDomain(newSubdomain) });
|
|
} catch (error) {
|
|
results.newDns = `failed: ${error.message}`;
|
|
log.warn('dns', 'New DNS creation warning', { error: error.message });
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (await exists(caddy.filePath)) {
|
|
const oldDomain = oldSubdomain.includes('.') ? oldSubdomain : ctx.buildDomain(oldSubdomain);
|
|
const newDomain = newSubdomain.includes('.') ? newSubdomain : ctx.buildDomain(newSubdomain);
|
|
const escapedOld = oldDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const oldBlockRegex = new RegExp(`${escapedOld}(?::\\d+)?\\s*\\{[^{}]*(?:\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\}[^{}]*)*\\}`, 'g');
|
|
const content = await caddy.read();
|
|
if (oldBlockRegex.test(content)) {
|
|
const caddyResult = await caddy.modify(c => {
|
|
const re = new RegExp(`${escapedOld}(?::\\d+)?\\s*\\{[^{}]*(?:\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\}[^{}]*)*\\}`, 'g');
|
|
return c.replace(re, match => match.replace(oldDomain, newDomain));
|
|
});
|
|
results.caddy = caddyResult.success ? 'updated' : 'updated (reload failed)';
|
|
} else {
|
|
results.caddy = 'old config not found';
|
|
}
|
|
} else {
|
|
results.caddy = 'caddyfile not found';
|
|
}
|
|
} catch (error) {
|
|
results.caddy = `failed: ${error.message}`;
|
|
log.error('caddy', 'Caddy update error', { error: error.message });
|
|
}
|
|
|
|
try {
|
|
if (await exists(ctx.SERVICES_FILE)) {
|
|
await servicesStateManager.update(services => {
|
|
const serviceIndex = services.findIndex(s => s.id === oldSubdomain || s.id === serviceId);
|
|
if (serviceIndex !== -1) {
|
|
services[serviceIndex].id = newSubdomain;
|
|
results.service = 'updated';
|
|
log.info('deploy', 'Service config updated in services.json');
|
|
} else {
|
|
results.service = 'not found';
|
|
}
|
|
return services;
|
|
});
|
|
}
|
|
} catch (error) {
|
|
results.service = `failed: ${error.message}`;
|
|
log.warn('deploy', 'Service update warning', { error: error.message || String(error) });
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Subdomain updated: ${oldSubdomain} -> ${newSubdomain}`,
|
|
newUrl: `https://${ctx.buildDomain(newSubdomain)}`,
|
|
results
|
|
});
|
|
}, 'update-subdomain'));
|
|
|
|
return router;
|
|
};
|