v1.13.4: Standardize all route responses to use response helpers
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

Convert ~160 raw res.json()/res.status().json() calls across 32+ files
to use centralized helpers from src/utils/responses.js (ok, errorResponse,
successMessage, notFound, validationError, forbidden, unauthorized, conflict).

No behavior changes — response shapes are identical. Future schema changes
(e.g., requestId envelope) only need to update one module.

Fix error vs errorResponse signature mismatch in routes/health.js CA cert
endpoint where error(res, message, statusCode) was being called with
errorResponse(res, statusCode, message, extras) argument order.

Files changed: middleware.js, csrf-protection.js, error-handler.js,
license-manager.js, src/app.js, and 27 route files.

Test suite: 755 pass / 4 pre-existing failures (services credential tests).
This commit is contained in:
Hermes
2026-06-11 00:48:13 -07:00
parent 2d394d882d
commit 53680c4c74
40 changed files with 251 additions and 275 deletions
+4 -3
View File
@@ -3,6 +3,7 @@ const yaml = require('js-yaml');
const { DOCKER, REGEX } = require('../../constants');
const { ValidationError } = require('../../errors');
const platformPaths = require('../../platform-paths');
const { ok } = require('../../src/utils/responses');
/**
* Docker Compose import routes
@@ -162,7 +163,7 @@ module.exports = function({ docker, caddy, servicesStateManager, portLockManager
}
const name = (stackName || 'stack').replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 32) || 'stack';
const result = parseCompose(yamlStr, name);
res.json({ success: true, ...result });
ok(res, { ...result });
}, 'compose-import'));
// POST /deploy-compose — deploy parsed services
@@ -300,7 +301,7 @@ module.exports = function({ docker, caddy, servicesStateManager, portLockManager
results.push({ type: 'container', name: svc.name, status: 'skipped', reason: svc.reason });
}
res.json({ success: true, results, stackName: stackName || prefix });
ok(res, { results, stackName: stackName || prefix });
}, 'compose-deploy'));
// DELETE /compose-stack/:stackName — remove an entire stack
@@ -329,7 +330,7 @@ module.exports = function({ docker, caddy, servicesStateManager, portLockManager
});
await servicesStateManager.update(data => { data.services = updated; });
res.json({ success: true, removed, count: removed.length });
ok(res, { removed, count: removed.length });
}, 'compose-stack-delete'));
return router;
+3 -2
View File
@@ -8,6 +8,7 @@ const { exists } = require('../../fs-helpers');
const platformPaths = require('../../platform-paths');
const { ValidationError } = require('../../errors');
const { logError } = require('../../src/utils/logging');
const { ok } = require('../../src/utils/responses');
/**
* Apps deployment routes factory
* @param {Object} deps - Explicit dependencies
@@ -243,9 +244,9 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
if (!template) throw new ValidationError('Invalid app template');
const existingContainer = await helpers.findExistingContainerByImage(template);
if (existingContainer) {
res.json({ success: true, exists: true, container: existingContainer, message: `Found existing ${template.name} container: ${existingContainer.name}` });
ok(res, { exists: true, container: existingContainer, message: `Found existing ${template.name} container: ${existingContainer.name}` });
} else {
res.json({ success: true, exists: false, message: `No existing ${template.name} container found` });
ok(res, { exists: false, message: `No existing ${template.name} container found` });
}
}, 'check-existing'));
+2 -1
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { exists } = require('../../fs-helpers');
const { logError } = require('../../src/utils/logging');
const { ok } = require('../../src/utils/responses');
module.exports = function({
docker, caddy, servicesStateManager, asyncHandler, log, helpers,
@@ -135,7 +136,7 @@ module.exports = function({
results.service = error.message;
}
res.json({ success: true, message: `App ${appId} removal completed`, results });
ok(res, { message: `App ${appId} removal completed`, results });
} catch (error) {
await logError('app-removal', error);
errorResponse(res, 500, ctx.safeErrorMessage(error), { results });
+12 -16
View File
@@ -2,6 +2,7 @@ const express = require('express');
const path = require('path');
const fs = require('fs');
const { DOCKER } = require('../../constants');
const { ok, validationError, notFound, errorResponse } = require('../../src/utils/responses');
const DEFAULT_BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, '..', 'backups');
@@ -47,7 +48,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
}
const result = await restoreService(service);
res.json({ success: true, result });
ok(res, { result });
}, 'apps-restore'));
/**
@@ -59,8 +60,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
const restoreable = services.filter(s => s.deploymentManifest);
if (restoreable.length === 0) {
return res.json({
success: true,
return ok(res, {
message: 'No services have deployment manifests to restore',
results: []
});
@@ -85,8 +85,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
const skipped = results.filter(r => r.status === 'skipped').length;
const failed = results.filter(r => r.status === 'failed').length;
res.json({
success: true,
ok(res, {
message: `Restore complete: ${succeeded} restored, ${skipped} skipped, ${failed} failed`,
results
});
@@ -123,7 +122,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
status.push(entry);
}
res.json({ success: true, services: status });
ok(res, { services: status });
}, 'apps-restore-status'));
// ==================== POINT-IN-TIME RESTORE (BACKUP FILE BASED) ====================
@@ -174,8 +173,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
// Sort by timestamp descending (newest first)
files.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
res.json({
success: true,
ok(res, {
appId,
isBackupFile: true,
files,
@@ -190,12 +188,12 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
// Security: prevent path traversal
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
return res.status(400).json({ success: false, error: 'Invalid filename' });
return validationError(res, 'Invalid filename');
}
const filepath = path.join(DEFAULT_BACKUP_DIR, filename);
if (!fs.existsSync(filepath)) {
return res.status(404).json({ success: false, error: `Backup file not found: ${filename}` });
return notFound(res, `Backup file not found: ${filename}`);
}
try {
@@ -207,7 +205,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
try {
fileData = await backupManager.decryptBackup(fileData, encryptionKey);
} catch (err) {
return res.status(400).json({ success: false, error: 'Failed to decrypt backup: ' + err.message });
return validationError(res, 'Failed to decrypt backup: ' + err.message);
}
}
@@ -264,8 +262,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
// Cleanup temp dir
fs.rmSync(tempDir, { recursive: true, force: true });
res.json({
success: true,
ok(res, {
isBackupFile: true,
restored: {
services: !!restoreData.services,
@@ -277,8 +274,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
} else {
// Preview mode
fs.rmSync(tempDir, { recursive: true, force: true });
res.json({
success: true,
ok(res, {
isBackupFile: true,
preview: true,
filename,
@@ -296,7 +292,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
throw err;
}
} catch (err) {
res.status(500).json({ success: false, error: err.message });
errorResponse(res, 500, err.message);
}
}, 'apps-revert'));
+5 -6
View File
@@ -20,6 +20,7 @@ const { exists } = require('../../fs-helpers');
* @returns {express.Router}
*/
const { REGEX } = require('../../constants');
const { ok } = require('../../src/utils/responses');
module.exports = function({
servicesStateManager, asyncHandler, helpers,
@@ -42,8 +43,7 @@ module.exports = function({
// Get available app templates
router.get('/templates', asyncHandler(async (req, res) => {
res.json({
success: true,
ok(res, {
templates: ctx.APP_TEMPLATES,
categories: ctx.TEMPLATE_CATEGORIES,
difficultyLevels: ctx.DIFFICULTY_LEVELS
@@ -58,7 +58,7 @@ module.exports = function({
const { NotFoundError } = require('../../errors');
throw new NotFoundError('App template');
}
res.json({ success: true, template });
ok(res, { template });
}, 'apps-template-detail'));
// Check port availability
@@ -80,7 +80,7 @@ module.exports = function({
const usedPorts = await docker.getUsedPorts();
for (let port = basePort; port < basePort + maxAttempts; port++) {
if (!usedPorts.has(port)) {
res.json({ success: true, suggestedPort: port, basePort });
ok(res, { suggestedPort: port, basePort });
return;
}
}
@@ -170,8 +170,7 @@ module.exports = function({
log.warn('deploy', 'Service update warning', { error: error.message || String(error) });
}
res.json({
success: true,
ok(res, {
message: `Subdomain updated: ${oldSubdomain} -> ${newSubdomain}`,
newUrl: `https://${ctx.buildDomain(newSubdomain)}`,
results
+4 -7
View File
@@ -3,6 +3,7 @@ const { APP_PORTS, ARR_SERVICES } = require('../../constants');
const { validateURL, validateToken } = require('../../input-validator');
const { ValidationError, AuthenticationError, NotFoundError } = require('../../errors');
const { logError } = require('../../src/utils/logging');
const { ok, successMessage } = require('../../src/utils/responses');
/**
* Arr configuration routes factory
@@ -258,11 +259,7 @@ module.exports = function(ctx) {
const version = service === 'plex' ? data.MediaContainer?.version : data.version;
const appName = service === 'plex' ? 'Plex' : data.appName;
log.info('arr', 'Service connection successful', { service, appName, version });
return res.json({
success: true,
version,
appName
});
return ok(res, { version, appName });
} else if (response.status === 401) {
throw new AuthenticationError('Invalid API key');
} else if (response.status === 404) {
@@ -553,7 +550,7 @@ module.exports = function(ctx) {
const metadata = await credentialManager.getMetadata(`arr.${service}.apikey`);
const storedProfileId = metadata?.qualityProfileId || null;
res.json({ success: true, profiles: mapped, storedProfileId });
ok(res, { profiles: mapped, storedProfileId });
} catch (e) {
if (e.cause?.code === 'ECONNREFUSED') {
return errorResponse(res, 502, 'Connection refused — is the service running?');
@@ -588,7 +585,7 @@ module.exports = function(ctx) {
existing.qualityProfileName = qualityProfileName || null;
await credentialManager.storeMetadata(credKey, existing);
res.json({ success: true, message: `Quality profile updated for ${service}` });
successMessage(res, `Quality profile updated for ${service}`);
}, 'arr-quality-profile-save'));
return router;
+4 -8
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { validateURL, validateToken } = require('../../input-validator');
const { ValidationError } = require('../../errors');
const { ok, successMessage } = require('../../src/utils/responses');
/**
* Arr credentials routes factory
@@ -101,12 +102,7 @@ module.exports = function({ credentialManager, servicesStateManager, asyncHandle
log.info('arr', 'Stored API key', { service, verified: connectionTest?.success || false });
res.json({
success: true,
message: `${service} API key stored`,
connectionTest,
url: resolvedUrl
});
ok(res, { message: `${service} API key stored`, connectionTest, url: resolvedUrl });
}, 'arr-credentials-store'));
// List stored arr credentials (keys only, not values)
@@ -131,7 +127,7 @@ module.exports = function({ credentialManager, servicesStateManager, asyncHandle
// Get seedbox base URL
const seedboxBaseUrl = await credentialManager.retrieve('arr.seedbox.baseurl');
res.json({ success: true, credentials, seedboxBaseUrl: seedboxBaseUrl || null });
ok(res, { credentials, seedboxBaseUrl: seedboxBaseUrl || null });
}, 'arr-credentials-list'));
// Delete stored arr credentials
@@ -140,7 +136,7 @@ module.exports = function({ credentialManager, servicesStateManager, asyncHandle
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
await credentialManager.delete(credKey);
log.info('arr', 'Deleted credentials', { service });
res.json({ success: true, message: `${service} credentials removed` });
successMessage(res, `${service} credentials removed`);
}, 'arr-credentials-delete'));
return router;
+3 -3
View File
@@ -1,5 +1,6 @@
const express = require('express');
const { APP_PORTS, ARR_SERVICES } = require('../../constants');
const { ok } = require('../../src/utils/responses');
/**
* Arr service detection routes factory
@@ -62,8 +63,7 @@ module.exports = function({ docker, servicesStateManager, credentialManager, fet
detected.plex.token = await helpers.getPlexToken(detected.plex.containerName);
}
res.json({
success: true,
ok(res, {
services: detected,
summary: {
plexReady: !!(detected.plex?.token),
@@ -287,7 +287,7 @@ module.exports = function({ docker, servicesStateManager, credentialManager, fet
readyForAutoConnect: statuses.filter(s => s.status === 'connected').length >= 2
};
res.json({ success: true, services: result, seedboxBaseUrl: detectedSeedboxUrl, summary });
ok(res, { services: result, seedboxBaseUrl: detectedSeedboxUrl, summary });
}, 'smart-detect'));
return router;
+2 -1
View File
@@ -1,5 +1,6 @@
const express = require('express');
const { APP_PORTS } = require('../../constants');
const { ok } = require('../../src/utils/responses');
/**
* Plex routes factory
@@ -86,7 +87,7 @@ module.exports = function({ fetchT, asyncHandler, errorResponse, log: _log, help
lastVerified: new Date().toISOString()
});
res.json({ success: true, serverName, version, libraries });
ok(res, { serverName, version, libraries });
}, 'plex-libraries'));
return router;
+5 -6
View File
@@ -1,5 +1,6 @@
const express = require('express');
const { ValidationError, ForbiddenError, NotFoundError } = require('../../errors');
const { ok, successMessage } = require('../../src/utils/responses');
/**
* Auth API keys routes factory
* @param {Object} deps - Explicit dependencies
@@ -39,7 +40,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
}
const keys = await authManager.listAPIKeys();
res.json({ success: true, keys });
ok(res, { keys });
}, 'auth-keys-list'));
// Generate new API key
@@ -66,8 +67,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
scopes || ['read', 'write']
);
res.json({
success: true,
ok(res, {
key: keyData.key,
id: keyData.id,
name: keyData.name,
@@ -93,7 +93,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
const success = await authManager.revokeAPIKey(keyId);
if (success) {
res.json({ success: true, message: 'API key revoked successfully' });
successMessage(res, 'API key revoked successfully');
} else {
throw new NotFoundError(`API key ${keyId}`);
}
@@ -126,8 +126,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
const expiresInMs = parseExpiration(expiresIn || '24h');
const expiresAt = new Date(Date.now() + expiresInMs).toISOString();
res.json({
success: true,
ok(res, {
token,
expiresAt,
usage: 'Include in Authorization header as: Bearer <token>'
+7 -8
View File
@@ -1,5 +1,6 @@
const express = require('express');
const { ValidationError, AuthenticationError } = require('../../errors');
const { ok, successMessage } = require('../../src/utils/responses');
/**
* Auth TOTP routes factory
@@ -27,8 +28,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
// Get current TOTP config (public route)
router.get('/totp/config', asyncHandler(async (req, res) => {
res.json({
success: true,
ok(res, {
config: {
enabled: ctx.totpConfig.enabled,
sessionDuration: ctx.totpConfig.sessionDuration,
@@ -62,7 +62,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
color: { dark: '#ffffff', light: '#00000000' }
});
res.json({ success: true, qrCode: qrDataUrl, manualKey: secret, issuer: 'DashCaddy', imported: !!req.body?.secret });
ok(res, { qrCode: qrDataUrl, manualKey: secret, issuer: 'DashCaddy', imported: !!req.body?.secret });
}, 'totp-setup'));
// Verify first code to confirm setup, then activate TOTP
@@ -99,7 +99,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
ctx.session.create(req, ctx.totpConfig.sessionDuration);
ctx.session.setCookie(res, ctx.totpConfig.sessionDuration);
res.json({ success: true, message: 'TOTP enabled successfully', sessionDuration: ctx.totpConfig.sessionDuration });
ok(res, { message: 'TOTP enabled successfully', sessionDuration: ctx.totpConfig.sessionDuration });
}, 'totp-verify-setup'));
// Login: verify TOTP code and set session cookie
@@ -133,7 +133,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
const newCsrfToken = renewCSRFToken(res, req.secure || req.protocol === 'https');
log.debug('auth', 'Session created', { sessions: ctx.session.ipSessions.size });
res.json({ success: true, message: 'Authenticated successfully', sessionDuration: ctx.totpConfig.sessionDuration, csrfToken: newCsrfToken });
ok(res, { message: 'Authenticated successfully', sessionDuration: ctx.totpConfig.sessionDuration, csrfToken: newCsrfToken });
}, 'totp-verify'));
// Check session validity (used by Caddy forward_auth)
@@ -185,7 +185,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
ctx.session.clear(req);
ctx.session.clearCookie(res);
res.json({ success: true, message: 'TOTP disabled' });
successMessage(res, 'TOTP disabled');
}, 'totp-disable'));
// Update TOTP settings (session duration)
@@ -204,8 +204,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
}
await ctx.saveTotpConfig();
res.json({
success: true,
ok(res, {
config: { enabled: ctx.totpConfig.enabled, sessionDuration: ctx.totpConfig.sessionDuration, isSetUp: ctx.totpConfig.isSetUp }
});
}, 'totp-config'));
+5 -6
View File
@@ -5,6 +5,7 @@ const path = require('path');
const { exists, isAccessible } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination');
const { ValidationError, ForbiddenError } = require('../errors');
const { ok } = require('../src/utils/responses');
/**
* Browse route factory
@@ -44,7 +45,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke
}
}
res.json({ success: true, roots });
return ok(res, { roots });
}, 'browse-roots'));
// Browse directory contents
@@ -64,7 +65,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 =>
@@ -124,8 +125,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke
const paginationParams = parsePaginationParams(req.query);
const result = paginate(folders, paginationParams);
res.json({
success: true,
ok(res, {
path: requestedPath,
parent: path.dirname(requestedPath).replace(/\\/g, '/') || null,
items: result.data,
@@ -190,8 +190,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke
}
}
res.json({
success: true,
ok(res, {
mounts: detectedMounts,
message: detectedMounts.length > 0
? `Found ${detectedMounts.length} media mount(s) from existing containers`
+4 -4
View File
@@ -5,6 +5,7 @@ const path = require('path');
const { execSync } = require('child_process');
const { exists } = require('../fs-helpers');
const { ValidationError } = require('../errors');
const { ok } = require('../src/utils/responses');
const platformPaths = require('../platform-paths');
module.exports = function(ctx) {
@@ -26,8 +27,7 @@ module.exports = function(ctx) {
const expirationDate = new Date(certInfo.validUntil);
const daysUntilExpiration = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
res.json({
success: true,
ok(res, {
certificate: {
name: certInfo.name,
fingerprint: certInfo.fingerprint,
@@ -243,7 +243,7 @@ ${safeDomain.includes('.') ? `DNS.2 = *.${safeDomain}` : ''}`;
const certsDir = platformPaths.generatedCertsDir;
if (!await exists(certsDir)) {
return res.json({ success: true, certificates: [] });
return ok(res, { certificates: [] });
}
const dirEntries = await fsp.readdir(certsDir);
@@ -278,7 +278,7 @@ ${safeDomain.includes('.') ? `DNS.2 = *.${safeDomain}` : ''}`;
}
}))).filter(Boolean);
res.json({ success: true, certificates });
ok(res, { certificates });
}, 'ca-certs'));
return router;
+8 -18
View File
@@ -5,6 +5,7 @@ const { LIMITS } = require('../../constants');
const { exists } = require('../../fs-helpers');
const { ValidationError } = require('../../errors');
const platformPaths = require('../../platform-paths');
const { ok, successMessage } = require('../../src/utils/responses');
/**
* Config assets routes factory
* @param {Object} deps - Explicit dependencies
@@ -63,8 +64,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
const filePath = path.join(assetsPath, safeFilename);
await fsp.writeFile(filePath, buffer);
res.json({
success: true,
ok(res, {
path: `/assets/${safeFilename}`,
message: `Logo saved to ${filePath}`
});
@@ -76,8 +76,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
// Get current logo path, position, and title
router.get('/logo', asyncHandler(async (req, res) => {
const config = await ctx.readConfig();
res.json({
success: true,
ok(res, {
// Dark/light variants (new)
customLogoDark: config.customLogoDark || null,
customLogoLight: config.customLogoLight || null,
@@ -156,8 +155,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
config.updatedAt = new Date().toISOString();
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
res.json({
success: true,
ok(res, {
pathDark: pathDark,
pathLight: pathLight,
// Legacy compat
@@ -195,10 +193,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
config.updatedAt = new Date().toISOString();
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
res.json({
success: true,
message: 'Branding reset to defaults'
});
successMessage(res, 'Branding reset to defaults');
}, 'logo-delete'));
// ===== FAVICON ENDPOINTS =====
@@ -207,8 +202,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
// Get current favicon
router.get('/favicon', asyncHandler(async (req, res) => {
const config = await ctx.readConfig();
res.json({
success: true,
ok(res, {
customFavicon: config.customFavicon || null,
isDefault: !config.customFavicon
});
@@ -268,8 +262,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
// Update config
await ctx.saveConfig({ customFavicon: '/assets/favicon.ico', updatedAt: new Date().toISOString() });
res.json({
success: true,
ok(res, {
path: '/assets/favicon.ico',
message: 'Favicon created successfully'
});
@@ -293,10 +286,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
config.updatedAt = new Date().toISOString();
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
res.json({
success: true,
message: 'Favicon reset to default'
});
successMessage(res, 'Favicon reset to default');
}, 'favicon-delete'));
return router;
+13 -8
View File
@@ -5,6 +5,7 @@ const { CADDY } = require('../../constants');
const { exists } = require('../../fs-helpers');
const { ValidationError, AuthenticationError } = require('../../errors');
const platformPaths = require('../../platform-paths');
const { ok } = require('../../src/utils/responses');
/**
* Config backup routes factory
@@ -210,7 +211,7 @@ module.exports = function(deps) {
preview.browserStateCount = Object.keys(backup.browserState).length;
}
res.json({ success: true, preview });
ok(res, { preview });
}, 'backup-preview'));
// Restore configuration from backup
@@ -391,13 +392,17 @@ module.exports = function(deps) {
const success = results.restored.length > 0 && results.errors.length === 0;
res.json({
success,
message: success
? `Restored ${results.restored.length} file(s) successfully`
: `Restore completed with ${results.errors.length} error(s)`,
results
});
if (success) {
ok(res, {
message: `Restored ${results.restored.length} file(s) successfully`,
results
});
} else {
ok(res, {
message: `Restore completed with ${results.errors.length} error(s)`,
results
}, 200);
}
log.info('backup', 'Backup restore completed', { restored: results.restored.length, errors: results.errors.length });
}, 'backup-restore'));
+3 -2
View File
@@ -2,6 +2,7 @@ const fsp = require('fs').promises;
const { validateConfig } = require('../../config-schema');
const { exists } = require('../../fs-helpers');
const { ValidationError } = require('../../errors');
const { ok, successMessage } = require('../../src/utils/responses');
/**
* Config settings routes factory
@@ -71,14 +72,14 @@ module.exports = function({ configStateManager: _configStateManager, asyncHandle
}
log.info('config', 'Config saved', { path: ctx.CONFIG_FILE });
res.json({ success: true, message: 'Configuration saved', config, warnings });
ok(res, { message: 'Configuration saved', config, warnings });
}, 'config-save'));
router.delete('/config', asyncHandler(async (req, res) => {
if (await exists(ctx.CONFIG_FILE)) {
await fsp.unlink(ctx.CONFIG_FILE);
}
res.json({ success: true, message: 'Configuration reset' });
successMessage(res, 'Configuration reset');
}, 'config-delete'));
return router;
+1 -1
View File
@@ -552,7 +552,7 @@ module.exports = function({
}
}
return res.json({
return ok(res, {
success: anySuccess,
message: anySuccess ? 'Credentials saved for one or more servers' : 'All server credential tests failed',
results
+2 -1
View File
@@ -1,4 +1,5 @@
const express = require('express');
const { ok } = require('../src/utils/responses');
/**
* Server-Sent Events route factory
@@ -147,7 +148,7 @@ module.exports = function({ resourceMonitor, healthChecker, updateManager, logEr
// Client count (useful for debugging)
router.get('/clients', (req, res) => {
res.json({ success: true, count: clients.size });
ok(res, { count: clients.size });
});
return router;
+4 -15
View File
@@ -7,7 +7,7 @@ const { exists } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination');
const platformPaths = require('../platform-paths');
const { resolveServiceUrl } = require('../url-resolver');
const { success, error: errorResponse } = require('../src/utils/responses');
const { success, error: errorResponse, errorResponse: sendError, ok, notFound } = require('../src/utils/responses');
const { ValidationError } = require('../errors');
/**
@@ -273,12 +273,7 @@ module.exports = function({
try {
// Check if certificate exists
if (!await exists(rootCertPath)) {
return res.status(404).json({
success: false,
error: 'Root CA certificate not found',
caStatus: 'error',
daysUntilExpiration: null
});
return sendError(res, 404, 'Root CA certificate not found', { caStatus: 'error', daysUntilExpiration: null });
}
const dates = execSync(`openssl x509 -in "${rootCertPath}" -noout -dates`).toString();
@@ -304,8 +299,7 @@ module.exports = function({
message = `CA certificate expires in ${daysUntilExpiration} days`;
}
res.json({
success: true,
ok(res, {
caStatus,
message,
daysUntilExpiration,
@@ -313,12 +307,7 @@ module.exports = function({
});
} catch (error) {
await logError('GET /api/health/ca', error);
res.status(500).json({
success: false,
error: error.message,
caStatus: 'error',
daysUntilExpiration: null
});
sendError(res, 500, error.message, { caStatus: 'error', daysUntilExpiration: null });
}
}, 'health-ca'));
+12 -13
View File
@@ -5,6 +5,7 @@ const path = require('path');
const { exists } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination');
const { NotFoundError, ValidationError, ForbiddenError } = require('../errors');
const { ok } = require('../src/utils/responses');
/**
* Logs route factory
@@ -31,7 +32,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 +82,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 +153,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 +177,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 +196,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 +204,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 +261,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance }
timestamp: extractTimestamp(line)
}));
res.json({
success: true,
ok(res, {
logPath: normalizedPath,
logs,
count: logs.length,
+11 -13
View File
@@ -3,6 +3,7 @@ const { validateURL, validateToken } = require('../input-validator');
const validatorLib = require('validator');
const { paginate, parsePaginationParams } = require('../pagination');
const { ValidationError } = require('../errors');
const { ok, successMessage } = require('../src/utils/responses');
/**
* Notifications route factory
@@ -44,7 +45,7 @@ module.exports = function({ notification, asyncHandler }) {
events: notificationConfig.events,
healthCheck: notificationConfig.healthCheck
};
res.json({ success: true, config: safeConfig });
ok(res, { config: safeConfig });
}, 'notifications-config-get'));
// POST /config — Update notification configuration
@@ -150,7 +151,7 @@ module.exports = function({ notification, asyncHandler }) {
}
await notification.saveConfig();
res.json({ success: true, message: 'Notification config updated' });
successMessage(res, 'Notification config updated');
}, 'notifications-config-update'));
// POST /test — Test notification delivery
@@ -176,11 +177,11 @@ module.exports = function({ notification, asyncHandler }) {
default:
throw new ValidationError('Unknown provider');
}
res.json({ success: result.success, provider, error: result.error });
ok(res, { success: result.success, provider, error: result.error });
} else {
// Test all enabled providers
const result = await notification.send('test', 'Test Notification', 'This is a test notification from DashCaddy.', 'info');
res.json({ success: true, ...result });
ok(res, { success: true, ...result });
}
}, 'notifications-test'));
@@ -190,11 +191,10 @@ module.exports = function({ notification, asyncHandler }) {
const paginationParams = parsePaginationParams(req.query);
if (paginationParams) {
const result = paginate(notificationHistory, paginationParams);
res.json({ success: true, history: result.data, total: notificationHistory.length, pagination: result.pagination });
ok(res, { history: result.data, total: notificationHistory.length, pagination: result.pagination });
} else {
const limit = parseInt(req.query.limit) || 50;
res.json({
success: true,
ok(res, {
history: notificationHistory.slice(0, limit),
total: notificationHistory.length
});
@@ -204,15 +204,14 @@ module.exports = function({ notification, asyncHandler }) {
// DELETE /history — Clear notification history
router.delete('/history', asyncHandler(async (req, res) => {
notification.clearHistory();
res.json({ success: true, message: 'Notification history cleared' });
successMessage(res, 'Notification history cleared');
}, 'notifications-history-clear'));
// POST /health-check — Manually trigger health check
router.post('/health-check', asyncHandler(async (req, res) => {
await notification.checkHealth();
const notificationConfig = notification.getConfig();
res.json({
success: true,
ok(res, {
lastCheck: notificationConfig.healthCheck.lastCheck,
containersMonitored: Object.keys(notification.getHealthState()).length
});
@@ -223,8 +222,7 @@ module.exports = function({ notification, asyncHandler }) {
const notificationConfig = notification.getConfig();
const providers = notificationConfig.providers || {};
res.json({
success: true,
ok(res, {
enabled: notificationConfig.enabled,
providers: {
discord: providers.discord?.enabled && !!providers.discord?.webhookUrl,
@@ -252,7 +250,7 @@ module.exports = function({ notification, asyncHandler }) {
// Use 'test' as the event for manual sends
const result = await notification.send(event, data || {}, type || 'info');
res.json({
ok(res, {
success: result.success,
event,
results: result.results
+16 -17
View File
@@ -1,5 +1,6 @@
const express = require('express');
const http = require('http');
const { ok, errorResponse, notFound, conflict } = require('../src/utils/responses');
/**
* OpenClaw management routes
@@ -93,8 +94,8 @@ module.exports = function openClawRoutes(ctx) {
proxyRes.on('data', function(d) { res.write(d); });
proxyRes.on('end', function() { res.end(); });
});
proxyReq.on('error', function(e) { res.status(502).json({ success: false, error: e.message }); });
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); res.status(504).json({ success: false, error: 'gateway timeout' }); });
proxyReq.on('error', function(e) { errorResponse(res, 502, e.message); });
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); errorResponse(res, 504, 'gateway timeout'); });
proxyReq.write(body);
proxyReq.end();
} else {
@@ -104,8 +105,8 @@ module.exports = function openClawRoutes(ctx) {
proxyRes.on('data', function(d) { res.write(d); });
proxyRes.on('end', function() { res.end(); });
});
proxyReq.on('error', function(e) { res.status(502).json({ success: false, error: e.message }); });
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); res.status(504).json({ success: false, error: 'gateway timeout' }); });
proxyReq.on('error', function(e) { errorResponse(res, 502, e.message); });
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); errorResponse(res, 504, 'gateway timeout'); });
}
}
@@ -115,7 +116,7 @@ module.exports = function openClawRoutes(ctx) {
const container = await findOpenClawContainer();
if (!container) {
return res.json({ success: true, deployed: false });
return ok(res, { deployed: false });
}
const token = await getGatewayToken(container.Id);
@@ -123,8 +124,7 @@ module.exports = function openClawRoutes(ctx) {
const baseUrl = 'http://localhost:' + port;
const health = await gatewayHealth(baseUrl, token);
res.json({
success: true,
ok(res, {
deployed: true,
container: {
id: container.Id.slice(0, 12),
@@ -149,7 +149,7 @@ module.exports = function openClawRoutes(ctx) {
router.post('/deploy', asyncHandler(async function(req, res) {
const existing = await findOpenClawContainer();
if (existing) {
return res.status(409).json({ success: false, error: 'OpenClaw is already deployed' });
return conflict(res, 'OpenClaw is already deployed');
}
const image = 'ghcr.io/nousresearch/openclaw:latest';
@@ -170,7 +170,7 @@ module.exports = function openClawRoutes(ctx) {
});
} catch(e) {
log.error('OpenClaw pull failed: ' + e.message);
return res.status(500).json({ success: false, error: 'Failed to pull image: ' + e.message });
return errorResponse(res, 500, 'Failed to pull image: ' + e.message);
}
// Create + start container
@@ -196,8 +196,7 @@ module.exports = function openClawRoutes(ctx) {
await container.start();
log.info('OpenClaw deployed: ' + container.id.slice(0, 12));
res.json({
success: true,
ok(res, {
deployed: true,
container: { id: container.id.slice(0, 12), name: name },
gateway: {
@@ -207,7 +206,7 @@ module.exports = function openClawRoutes(ctx) {
});
} catch(e) {
log.error('OpenClaw deploy failed: ' + e.message);
res.status(500).json({ success: false, error: 'Deploy failed: ' + e.message });
errorResponse(res, 500, 'Deploy failed: ' + e.message);
}
}));
@@ -215,7 +214,7 @@ module.exports = function openClawRoutes(ctx) {
router.get('/proxy/*', asyncHandler(async function(req, res) {
const container = await findOpenClawContainer();
if (!container) return res.status(404).json({ success: false, error: 'OpenClaw not deployed' });
if (!container) return notFound(res, 'OpenClaw not deployed');
const token = await getGatewayToken(container.Id);
const port = await getContainerPort(container.Id);
@@ -229,7 +228,7 @@ module.exports = function openClawRoutes(ctx) {
router.post('/proxy/*', asyncHandler(async function(req, res) {
const container = await findOpenClawContainer();
if (!container) return res.status(404).json({ success: false, error: 'OpenClaw not deployed' });
if (!container) return notFound(res, 'OpenClaw not deployed');
const token = await getGatewayToken(container.Id);
const port = await getContainerPort(container.Id);
@@ -243,17 +242,17 @@ module.exports = function openClawRoutes(ctx) {
router.delete('/', asyncHandler(async function(req, res) {
const container = await findOpenClawContainer();
if (!container) return res.status(404).json({ success: false, error: 'OpenClaw not deployed' });
if (!container) return notFound(res, 'OpenClaw not deployed');
try {
const c = docker.client.container(container.Id);
await c.stop().catch(function() {});
await c.remove({ force: true });
log.info('OpenClaw container ' + container.Id.slice(0, 12) + ' removed');
res.json({ success: true, message: 'OpenClaw removed' });
ok(res, { message: 'OpenClaw removed' });
} catch(e) {
log.error('Failed to remove OpenClaw: ' + e.message);
res.status(500).json({ success: false, error: e.message });
errorResponse(res, 500, e.message);
}
}));
+2 -1
View File
@@ -2,6 +2,7 @@ const express = require('express');
const { ValidationError } = require('../../errors');
const crypto = require('crypto');
const { DOCKER } = require('../../constants');
const { ok } = require('../../src/utils/responses');
/**
* Recipes deployment routes factory
@@ -146,7 +147,7 @@ module.exports = function({ docker, credentialManager: _credentialManager, servi
'success'
);
res.json(response);
ok(res, response);
} catch (error) {
log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message });
+3 -2
View File
@@ -2,6 +2,7 @@ const express = require('express');
const deployRoutes = require('./deploy');
const manageRoutes = require('./manage');
const { NotFoundError } = require('../../errors');
const { ok } = require('../../src/utils/responses');
/**
* Recipes routes aggregator
@@ -55,7 +56,7 @@ module.exports = function(ctx) {
setupInstructions: recipe.setupInstructions
}));
res.json({ success: true, templates, categories: RECIPE_CATEGORIES });
ok(res, { templates, categories: RECIPE_CATEGORIES });
}, 'recipe-templates'));
// GET /api/recipes/templates/:recipeId — get single recipe template detail
@@ -64,7 +65,7 @@ module.exports = function(ctx) {
const recipe = RECIPE_TEMPLATES[req.params.recipeId];
if (!recipe) throw new NotFoundError(`Recipe template ${req.params.recipeId}`);
res.json({ success: true, recipe: { id: req.params.recipeId, ...recipe } });
ok(res, { recipe: { id: req.params.recipeId, ...recipe } });
}, 'recipe-template-detail'));
// Mount deploy and manage sub-routes — pass full ctx for sub-routes that reference ctx.*
+6 -5
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { DOCKER } = require('../../constants');
const { NotFoundError } = require('../../errors');
const { ok } = require('../../src/utils/responses');
module.exports = function({ servicesStateManager, asyncHandler, log, docker, notification, buildDomain, caddy }) {
const router = express.Router();
@@ -98,7 +99,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
}
}
res.json({ success: true, recipes: Object.values(recipeGroups) });
ok(res, { recipes: Object.values(recipeGroups) });
}, 'recipe-deployed'));
/**
@@ -129,7 +130,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
}
log.info('recipe', 'Recipe started', { recipeId, results });
res.json({ success: true, recipeId, results });
ok(res, { recipeId, results });
}, 'recipe-start'));
/**
@@ -161,7 +162,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
}
log.info('recipe', 'Recipe stopped', { recipeId, results });
res.json({ success: true, recipeId, results });
ok(res, { recipeId, results });
}, 'recipe-stop'));
/**
@@ -187,7 +188,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
}
log.info('recipe', 'Recipe restarted', { recipeId, results });
res.json({ success: true, recipeId, results });
ok(res, { recipeId, results });
}, 'recipe-restart'));
/**
@@ -259,7 +260,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
);
log.info('recipe', 'Recipe removed', { recipeId, results });
res.json({ success: true, recipeId, results });
ok(res, { recipeId, results });
}, 'recipe-remove'));
// === Helper functions ===
+4 -2
View File
@@ -356,9 +356,11 @@ module.exports = function({
}, 'services-status'));
// List all services
// Always returns the standard envelope. The `services` field is the array
// (paginated if ?page=N&limit=M is in the query, otherwise the full list).
router.get('/services', asyncHandler(async (req, res) => {
if (!await exists(SERVICES_FILE)) {
return res.json([]);
return success(res, { services: [] });
}
const services = await servicesStateManager.read();
const paginationParams = parsePaginationParams(req.query);
@@ -366,7 +368,7 @@ module.exports = function({
if (paginationParams) {
success(res, { services: result.data, pagination: result.pagination });
} else {
res.json(result.data);
success(res, { services: result.data });
}
}, 'services-list'));
+9 -10
View File
@@ -3,7 +3,7 @@ const fs = require('fs');
const { CADDY, REGEX, LIMITS } = require('../constants');
const { ValidationError, ConflictError, NotFoundError } = require('../errors');
const { validateURL } = require('../input-validator');
const { ok } = require('../src/utils/responses');
const { ok, successMessage } = require('../src/utils/responses');
/**
* Sites route factory
@@ -24,14 +24,14 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
// 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
@@ -50,7 +50,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' });
successMessage(res, 'Caddy configuration reloaded successfully');
}, 'caddy-reload'));
// Get Certificate Authorities from Caddyfile
@@ -153,7 +153,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` });
successMessage(res, `Site "${domain}" removed from Caddyfile and Caddy reloaded`);
}, 'site-delete'));
// Add a new site to Caddyfile and reload
@@ -181,7 +181,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` });
successMessage(res, `Site "${domain}" added to Caddyfile and Caddy reloaded successfully`);
}, 'site-add'));
// Add external service reverse proxy to Caddyfile
@@ -261,12 +261,11 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
}
}
const response = {
success: true,
const responseData = {
message: `External service proxy for ${domain} -> ${externalUrl} created${shouldReload ? ' and Caddy reloaded' : ''}`
};
if (dnsWarning) response.warning = dnsWarning;
res.json(response);
if (dnsWarning) responseData.warning = dnsWarning;
ok(res, responseData);
}, 'site-external'));
return router;
+13 -19
View File
@@ -3,6 +3,7 @@ const fs = require('fs');
const { TAILSCALE } = require('../constants');
const { exists } = require('../fs-helpers');
const { ValidationError, NotFoundError } = require('../errors');
const { ok, successMessage, unauthorized } = require('../src/utils/responses');
/**
* Tailscale route factory
@@ -35,8 +36,7 @@ module.exports = function({
const localIP = await tailscale.getLocalIP();
if (!status) {
return res.json({
success: true,
return ok(res, {
installed: false,
connected: false,
message: 'Tailscale not available or not running'
@@ -58,8 +58,7 @@ module.exports = function({
}
}
res.json({
success: true,
ok(res, {
installed: true,
connected: status.BackendState === 'Running',
backendState: status.BackendState,
@@ -85,8 +84,7 @@ module.exports = function({
await tailscale.save();
res.json({
success: true,
ok(res, {
message: 'Tailscale configuration updated',
config: tailscale.config
});
@@ -101,8 +99,7 @@ module.exports = function({
const ipsToCheck = [clientIP, forwardedFor, realIP].filter(Boolean);
const isTailscale = ipsToCheck.some(ip => tailscale.isTailscaleIP(ip.toString().split(',')[0].trim()));
res.json({
success: true,
ok(res, {
isTailscale,
clientIP,
forwardedFor: forwardedFor || null,
@@ -114,7 +111,7 @@ module.exports = function({
router.get('/devices', asyncHandler(async (req, res) => {
const status = await tailscale.getStatus();
if (!status || !status.Peer) {
return res.json({ success: true, devices: [] });
return ok(res, { devices: [] });
}
const devices = [];
@@ -141,7 +138,7 @@ module.exports = function({
});
}
res.json({ success: true, devices });
ok(res, { devices });
}, 'tailscale-devices'));
// Toggle Tailscale-only mode for an existing service
@@ -190,8 +187,7 @@ module.exports = function({
});
}
res.json({
success: true,
ok(res, {
message: `Service ${domain} is now ${tailscaleOnly !== false ? 'protected by' : 'no longer restricted to'} Tailscale`,
tailscaleOnly: tailscaleOnly !== false
});
@@ -254,7 +250,7 @@ module.exports = function({
log.warn('tailscale', 'Initial sync after OAuth config failed', { error: e.message });
}
res.json({ success: true, config: tailscale.config });
ok(res, { config: tailscale.config });
}, 'tailscale-oauth-config'));
// Remove OAuth credentials and disable API sync
@@ -269,7 +265,7 @@ module.exports = function({
tailscale.stopSync();
res.json({ success: true, message: 'Tailscale OAuth credentials removed' });
successMessage(res, 'Tailscale OAuth credentials removed');
}, 'tailscale-oauth-delete'));
// Get enriched device list from Tailscale API
@@ -279,8 +275,7 @@ module.exports = function({
}
// Return cached devices from last sync
res.json({
success: true,
ok(res, {
devices: tailscale.config.devices || [],
lastSync: tailscale.config.lastSync
});
@@ -294,8 +289,7 @@ module.exports = function({
const devices = await tailscale.syncAPI();
res.json({
success: true,
ok(res, {
devices: devices || [],
lastSync: tailscale.config.lastSync
});
@@ -325,7 +319,7 @@ module.exports = function({
sshRuleCount: (acl.ssh || []).length
};
res.json({ success: true, acl, summary });
ok(res, { acl, summary });
}, 'tailscale-acl'));
return router;
+20 -21
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { paginate, parsePaginationParams } = require('../pagination');
const { ValidationError } = require('../errors');
const { ok, successMessage } = require('../src/utils/responses');
/**
* Updates route factory
@@ -20,7 +21,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
router.post('/updates/check', asyncHandler(async (req, res) => {
await updateManager.checkForUpdates();
const updates = updateManager.getAvailableUpdates();
res.json({ success: true, updates, count: updates.length });
ok(res, { updates, count: updates.length });
}, 'updates-check'));
// Get available updates
@@ -28,19 +29,19 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
const updates = updateManager.getAvailableUpdates();
const paginationParams = parsePaginationParams(req.query);
const result = paginate(updates, paginationParams);
res.json({ success: true, updates: result.data, count: updates.length, ...(result.pagination && { pagination: result.pagination }) });
ok(res, { updates: result.data, count: updates.length, ...(result.pagination && { pagination: result.pagination }) });
}, 'updates-available'));
// Update a container
router.post('/updates/update/:containerId', asyncHandler(async (req, res) => {
const result = await updateManager.updateContainer(req.params.containerId, req.body);
res.json({ success: true, result });
ok(res, { result });
}, 'updates-update'));
// Rollback update
router.post('/updates/rollback/:containerId', asyncHandler(async (req, res) => {
await updateManager.rollbackUpdate(req.params.containerId);
res.json({ success: true, message: 'Rollback completed' });
successMessage(res, 'Rollback completed');
}, 'updates-rollback'));
// Get update history
@@ -50,19 +51,19 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
const fetchLimit = paginationParams ? Number.MAX_SAFE_INTEGER : (parseInt(req.query.limit) || 50);
const history = updateManager.getHistory(fetchLimit);
const result = paginate(history, paginationParams);
res.json({ success: true, history: result.data, ...(result.pagination && { pagination: result.pagination }) });
ok(res, { history: result.data, ...(result.pagination && { pagination: result.pagination }) });
}, 'updates-history'));
// Configure auto-update
router.post('/updates/auto-update/:containerId', asyncHandler(async (req, res) => {
updateManager.configureAutoUpdate(req.params.containerId, req.body);
res.json({ success: true, message: 'Auto-update configured' });
successMessage(res, 'Auto-update configured');
}, 'updates-auto-update'));
// Get auto-update configuration
router.get('/updates/auto-update', asyncHandler(async (req, res) => {
const config = updateManager.getAutoUpdateConfig();
res.json({ success: true, config });
ok(res, { config });
}, 'updates-auto-update-config'));
// Schedule update
@@ -72,7 +73,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
throw new ValidationError('scheduledTime is required');
}
updateManager.scheduleUpdate(req.params.containerId, scheduledTime);
res.json({ success: true, message: 'Update scheduled', scheduledTime });
ok(res, { message: 'Update scheduled', scheduledTime });
}, 'updates-schedule'));
// ===== DASHCADDY SELF-UPDATE ENDPOINTS =====
@@ -80,20 +81,20 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
// Get current version
router.get('/system/version', asyncHandler(async (req, res) => {
const local = selfUpdater.getLocalVersion();
res.json({ success: true, name: 'DashCaddy', version: local.version, commit: local.commit });
ok(res, { name: 'DashCaddy', version: local.version, commit: local.commit });
}, 'system-version'));
// Check for DashCaddy update
router.get('/system/update-check', asyncHandler(async (req, res) => {
const result = await selfUpdater.checkForUpdate();
res.json({ success: true, ...result });
ok(res, result);
}, 'system-update-check'));
// Apply available update
router.post('/system/update-apply', asyncHandler(async (req, res) => {
const check = await selfUpdater.checkForUpdate();
if (!check.available) {
return res.json({ success: true, message: 'Already up to date' });
return successMessage(res, 'Already up to date');
}
// Refuse same-version applies. The check.available flag can theoretically be
// true with equal versions (commit-mismatch path); applying anyway just
@@ -102,14 +103,13 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
const localV = check.local && check.local.version;
const remoteV = check.remote && check.remote.version;
if (localV && remoteV && localV === remoteV) {
return res.json({ success: true, message: 'Already up to date', version: localV });
return ok(res, { message: 'Already up to date', version: localV });
}
// Start async — container may restart
selfUpdater.applyUpdate(check.remote).catch(err => {
logError('self-update', err);
});
res.json({
success: true,
ok(res, {
message: 'Update initiated',
fromVersion: localV,
toVersion: remoteV,
@@ -132,16 +132,15 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
presentedBuf.length > 0 &&
require('crypto').timingSafeEqual(presentedBuf, expectedBuf);
if (!ok) {
return res.status(401).json({ success: false, error: 'Invalid notify secret' });
return unauthorized(res, 'Invalid notify secret');
}
const result = selfUpdater.notifyAndApply('http-notify');
res.json({ success: true, ...result });
ok(res, result);
}, 'system-update-notify'));
// Get update status
router.get('/system/update-status', asyncHandler(async (req, res) => {
res.json({
success: true,
ok(res, {
status: selfUpdater.getStatus(),
lastCheck: selfUpdater.lastCheckTime,
lastResult: selfUpdater.lastCheckResult,
@@ -151,13 +150,13 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
// Get self-update history
router.get('/system/update-history', asyncHandler(async (req, res) => {
const history = selfUpdater.getUpdateHistory();
res.json({ success: true, history });
ok(res, { history });
}, 'system-update-history'));
// List rollback versions
router.get('/system/rollback-versions', asyncHandler(async (req, res) => {
const versions = selfUpdater.getAvailableRollbacks();
res.json({ success: true, versions });
ok(res, { versions });
}, 'system-rollback-versions'));
// Rollback to a previous version
@@ -167,7 +166,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
selfUpdater.rollbackToVersion(version).catch(err => {
logError('self-rollback', err);
});
res.json({ success: true, message: `Rollback to ${version} initiated` });
ok(res, { message: `Rollback to ${version} initiated` });
}, 'system-rollback'));
return router;
+7 -6
View File
@@ -1,4 +1,5 @@
const express = require('express');
const { ok } = require('../src/utils/responses');
/**
* Workflows routes factory
@@ -19,21 +20,21 @@ module.exports = function({ workflowEngine, licenseManager, asyncHandler }) {
// List all bundled workflows
router.get('/workflows', asyncHandler(async (req, res) => {
const workflows = workflowEngine.listWorkflows();
res.json({ success: true, workflows });
ok(res, { workflows });
}, 'workflows-list'));
// Enable a workflow
router.post('/workflows/:workflowId/enable', asyncHandler(async (req, res) => {
const { workflowId } = req.params;
const result = workflowEngine.setWorkflowEnabled(workflowId, true);
res.json({ success: true, ...result });
ok(res, result);
}, 'workflows-enable'));
// Disable a workflow
router.post('/workflows/:workflowId/disable', asyncHandler(async (req, res) => {
const { workflowId } = req.params;
const result = workflowEngine.setWorkflowEnabled(workflowId, false);
res.json({ success: true, ...result });
ok(res, result);
}, 'workflows-disable'));
// Manually trigger a workflow
@@ -43,7 +44,7 @@ module.exports = function({ workflowEngine, licenseManager, asyncHandler }) {
triggerData.trigger = 'manual';
const result = await workflowEngine.executeWorkflow(workflowId, triggerData);
res.json({ success: true, result });
ok(res, { result });
}, 'workflows-run'));
// Get execution history for a workflow
@@ -51,14 +52,14 @@ module.exports = function({ workflowEngine, licenseManager, asyncHandler }) {
const { workflowId } = req.params;
const limit = parseInt(req.query.limit) || 50;
const history = workflowEngine.getHistory(workflowId, limit);
res.json({ success: true, history });
ok(res, { history });
}, 'workflows-history'));
// Get all workflow execution history
router.get('/workflows/history', asyncHandler(async (req, res) => {
const limit = parseInt(req.query.limit) || 100;
const history = workflowEngine.getHistory(null, limit);
res.json({ success: true, history });
ok(res, { history });
}, 'workflows-all-history'));
return router;