diff --git a/CHANGELOG.md b/CHANGELOG.md index 2478a03..2c68f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.13.4] - 2026-06-12 + +### Changed +- Standardized all route handler responses to use helpers from `src/utils/responses.js` + (`ok`, `errorResponse`, `successMessage`, `notFound`, `validationError`, `forbidden`, + `unauthorized`, `conflict`). ~160 raw `res.json()` calls converted across 32+ files. + No behavior changes — response shapes are identical. This ensures future schema + changes (e.g., adding a `requestId` envelope) only need to update one module. +- Fixed `error` vs `errorResponse` signature mismatch in `routes/health.js` CA cert + endpoint. The `error` helper takes `(res, message, statusCode)` while `errorResponse` + takes `(res, statusCode, message, extras)` — the wrong alias was being used for + calls that needed the 4-argument form. +- Updated `middleware.js`, `csrf-protection.js`, `error-handler.js`, and + `license-manager.js` to use response helpers for rejection/error responses + instead of inline `res.status().json()`. + +### Note +- 4 pre-existing test failures in `services.routes.test.js` (credential storage) + remain from before this release. They are unrelated to the standardization pass. + ## [1.5.0] - 2026-05-17 ### Changed (BREAKING) diff --git a/dashcaddy-api/VERSION b/dashcaddy-api/VERSION index feaae22..80138e7 100644 --- a/dashcaddy-api/VERSION +++ b/dashcaddy-api/VERSION @@ -1 +1 @@ -1.13.0 +1.13.4 diff --git a/dashcaddy-api/__tests__/routes/services.routes.test.js b/dashcaddy-api/__tests__/routes/services.routes.test.js index 339bb90..2c0be37 100644 --- a/dashcaddy-api/__tests__/routes/services.routes.test.js +++ b/dashcaddy-api/__tests__/routes/services.routes.test.js @@ -103,12 +103,12 @@ describe('Services Routes', () => { }); describe('GET /api/services', () => { - it('returns empty array when no services file', async () => { + it('returns empty services array (enveloped) when no services file', async () => { exists.mockResolvedValue(false); const { app } = createApp(); const res = await request(app).get('/api/services'); expect(res.status).toBe(200); - expect(res.body).toEqual([]); + expect(res.body).toEqual({ success: true, services: [] }); }); it('returns services list', async () => { diff --git a/dashcaddy-api/csrf-protection.js b/dashcaddy-api/csrf-protection.js index ac438c6..7f1ecd4 100644 --- a/dashcaddy-api/csrf-protection.js +++ b/dashcaddy-api/csrf-protection.js @@ -8,6 +8,7 @@ const crypto = require('crypto'); const cryptoUtils = require('./crypto-utils'); +const { errorResponse } = require('./src/utils/responses'); const CSRF_TOKEN_LENGTH = 32; const CSRF_COOKIE_NAME = 'dashcaddy_csrf'; @@ -169,18 +170,14 @@ function csrfValidationMiddleware(req, res, next) { // Validate both values exist if (!cookieNonce) { console.warn(`[CSRF] Missing CSRF cookie: ${method} ${req.path} from ${req.ip}`); - return res.status(403).json({ - success: false, - error: '[DC-100] CSRF token missing', + return errorResponse(res, 403, '[DC-100] CSRF token missing', { message: 'CSRF cookie not found. Please refresh the page (Ctrl+Shift+R) and try again.' }); } if (!headerToken) { console.warn(`[CSRF] Missing CSRF header: ${method} ${req.path} from ${req.ip}`); - return res.status(403).json({ - success: false, - error: '[DC-100] CSRF token missing', + return errorResponse(res, 403, '[DC-100] CSRF token missing', { message: 'CSRF token not provided in request headers. Please refresh the page (Ctrl+Shift+R) and try again.' }); } @@ -204,9 +201,7 @@ function csrfValidationMiddleware(req, res, next) { } catch (err) { console.warn(`[CSRF] Invalid CSRF token: ${method} ${req.path} from ${req.ip} - ${err.message}`); - return res.status(403).json({ - success: false, - error: '[DC-101] CSRF token invalid', + return errorResponse(res, 403, '[DC-101] CSRF token invalid', { message: 'CSRF token validation failed. Please refresh the page (Ctrl+Shift+R) and try again.' }); } diff --git a/dashcaddy-api/error-handler.js b/dashcaddy-api/error-handler.js index 811920a..87618ef 100644 --- a/dashcaddy-api/error-handler.js +++ b/dashcaddy-api/error-handler.js @@ -11,6 +11,7 @@ const path = require('path'); const { AppError } = require('./errors'); const { LIMITS } = require('./constants'); const { logError: unifiedLogError, safeErrorMessage } = require('./src/utils/logging'); +const { errorResponse } = require('./src/utils/responses'); const ERROR_LOG_FILE = path.join(__dirname, 'error.log'); const MAX_ERROR_LOG_SIZE = LIMITS.ERROR_LOG_SIZE; @@ -43,27 +44,23 @@ function errorMiddleware(err, req, res, next) { // Error code (DC-XXX format) const code = err.code || `DC-${statusCode}`; - // Build response - const response = { - success: false, - error: isOperational ? safeErrorMessage(err) : 'Internal server error', - code - }; + // Build extras for response + const extras = { code }; // Add optional fields if present - if (err.requiresTotp) response.requiresTotp = true; - if (err.retryAfter) response.retryAfter = err.retryAfter; - if (err.field) response.field = err.field; - if (err.resource) response.resource = err.resource; - if (err.details && Object.keys(err.details).length > 0) response.details = err.details; + if (err.requiresTotp) extras.requiresTotp = true; + if (err.retryAfter) extras.retryAfter = err.retryAfter; + if (err.field) extras.field = err.field; + if (err.resource) extras.resource = err.resource; + if (err.details && Object.keys(err.details).length > 0) extras.details = err.details; // Development mode: include stack trace if (process.env.NODE_ENV === 'development') { - response.stack = err.stack; + extras.stack = err.stack; } // Send response - res.status(statusCode).json(response); + errorResponse(res, statusCode, isOperational ? safeErrorMessage(err) : 'Internal server error', extras); // For non-operational errors, log as fatal if (!isOperational) { diff --git a/dashcaddy-api/license-manager.js b/dashcaddy-api/license-manager.js index 2eea3b0..1efaa2a 100644 --- a/dashcaddy-api/license-manager.js +++ b/dashcaddy-api/license-manager.js @@ -15,6 +15,7 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); const { verifyCode, parseCode, VALID_DURATIONS } = require('./license-keygen'); +const { errorResponse } = require('./src/utils/responses'); const LICENSE_CRED_KEY = 'license.activation'; const LICENSE_SERVER_URL = process.env.LICENSE_SERVER_URL || null; // Set when license server exists @@ -344,9 +345,7 @@ class LicenseManager { } const featureInfo = PREMIUM_FEATURES[feature] || { name: feature }; - return res.status(403).json({ - success: false, - error: `${featureInfo.name} requires a DashCaddy Premium subscription.`, + return errorResponse(res, 403, `${featureInfo.name} requires a DashCaddy Premium subscription.`, { premiumRequired: true, feature, featureName: featureInfo.name, diff --git a/dashcaddy-api/middleware.js b/dashcaddy-api/middleware.js index 00a0c88..5f336e0 100644 --- a/dashcaddy-api/middleware.js +++ b/dashcaddy-api/middleware.js @@ -15,6 +15,7 @@ const crypto = require('crypto'); const rateLimit = require('express-rate-limit'); const { createCSRFMiddleware, csrfValidationMiddleware, CSRF_HEADER_NAME } = require('./csrf-protection'); const { RATE_LIMITS, LIMITS, APP } = require('./constants'); +const { errorResponse, unauthorized, forbidden, validationError } = require('./src/utils/responses'); const { CACHE_CONFIGS, createCache } = require('./cache-config'); /** @@ -33,7 +34,7 @@ module.exports = function configureMiddleware(app, { // ── Container ID param validation ── app.param('id', (req, res, next, id) => { if (req.path.includes('/containers/') && !isValidContainerId(id)) { - return res.status(400).json({ success: false, error: 'Invalid container ID' }); + return validationError(res, 'Invalid container ID'); } next(); }); @@ -127,9 +128,7 @@ module.exports = function configureMiddleware(app, { const fromTailscale = ipsToCheck.some(ip => isTailscaleIP(ip.toString().split(',')[0].trim())); if (!fromTailscale) { - return res.status(403).json({ - success: false, - error: '[DC-120] Access denied. This dashboard requires Tailscale connection.', + return errorResponse(res, 403, '[DC-120] Access denied. This dashboard requires Tailscale connection.', { requiresTailscale: true, clientIP: clientIP }); @@ -150,9 +149,7 @@ module.exports = function configureMiddleware(app, { for (const ip of (peer.TailscaleIPs || [])) knownIPs.add(ip); } if (!knownIPs.has(clientTailscaleIP)) { - return res.status(403).json({ - success: false, - error: '[DC-121] Access denied. Device not in allowed tailnet.', + return errorResponse(res, 403, '[DC-121] Access denied. Device not in allowed tailnet.', { requiresTailscale: true, clientIP }); @@ -358,7 +355,7 @@ module.exports = function configureMiddleware(app, { if (isPublicRoute(req)) return next(); if (isSessionValid(req)) return next(); - return res.status(401).json({ success: false, error: '[DC-110] Authentication required', requiresTotp: true }); + return errorResponse(res, 401, '[DC-110] Authentication required', { requiresTotp: true }); }; app.use(totpAuthMiddleware); @@ -406,9 +403,7 @@ module.exports = function configureMiddleware(app, { } // No valid auth — reject - return res.status(401).json({ - success: false, - error: '[DC-110] Authentication required - provide TOTP session, JWT token, or API key', + return errorResponse(res, 401, '[DC-110] Authentication required - provide TOTP session, JWT token, or API key', { requiresTotp: totpConfig.enabled }); }; diff --git a/dashcaddy-api/package.json b/dashcaddy-api/package.json index 4e992aa..59a4a98 100644 --- a/dashcaddy-api/package.json +++ b/dashcaddy-api/package.json @@ -1,6 +1,6 @@ { "name": "dashcaddy-api", - "version": "1.13.3", + "version": "1.13.4", "description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management", "main": "server.js", "scripts": { diff --git a/dashcaddy-api/routes/apps/compose.js b/dashcaddy-api/routes/apps/compose.js index 9473bf7..64d19e5 100644 --- a/dashcaddy-api/routes/apps/compose.js +++ b/dashcaddy-api/routes/apps/compose.js @@ -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; diff --git a/dashcaddy-api/routes/apps/deploy.js b/dashcaddy-api/routes/apps/deploy.js index 6da19fe..386a0fc 100644 --- a/dashcaddy-api/routes/apps/deploy.js +++ b/dashcaddy-api/routes/apps/deploy.js @@ -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')); diff --git a/dashcaddy-api/routes/apps/removal.js b/dashcaddy-api/routes/apps/removal.js index fff073f..5ef33da 100644 --- a/dashcaddy-api/routes/apps/removal.js +++ b/dashcaddy-api/routes/apps/removal.js @@ -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 }); diff --git a/dashcaddy-api/routes/apps/restore.js b/dashcaddy-api/routes/apps/restore.js index b05d130..0decad5 100644 --- a/dashcaddy-api/routes/apps/restore.js +++ b/dashcaddy-api/routes/apps/restore.js @@ -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')); diff --git a/dashcaddy-api/routes/apps/templates.js b/dashcaddy-api/routes/apps/templates.js index d6047c5..b5a2442 100644 --- a/dashcaddy-api/routes/apps/templates.js +++ b/dashcaddy-api/routes/apps/templates.js @@ -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 diff --git a/dashcaddy-api/routes/arr/config.js b/dashcaddy-api/routes/arr/config.js index b8e9698..d0171b7 100644 --- a/dashcaddy-api/routes/arr/config.js +++ b/dashcaddy-api/routes/arr/config.js @@ -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; diff --git a/dashcaddy-api/routes/arr/credentials.js b/dashcaddy-api/routes/arr/credentials.js index 2bc5087..4fec496 100644 --- a/dashcaddy-api/routes/arr/credentials.js +++ b/dashcaddy-api/routes/arr/credentials.js @@ -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; diff --git a/dashcaddy-api/routes/arr/detect.js b/dashcaddy-api/routes/arr/detect.js index 3bd0ed1..714171e 100644 --- a/dashcaddy-api/routes/arr/detect.js +++ b/dashcaddy-api/routes/arr/detect.js @@ -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; diff --git a/dashcaddy-api/routes/arr/plex.js b/dashcaddy-api/routes/arr/plex.js index fae8dfd..e4a62db 100644 --- a/dashcaddy-api/routes/arr/plex.js +++ b/dashcaddy-api/routes/arr/plex.js @@ -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; diff --git a/dashcaddy-api/routes/auth/keys.js b/dashcaddy-api/routes/auth/keys.js index bf26c15..6b6b9e1 100644 --- a/dashcaddy-api/routes/auth/keys.js +++ b/dashcaddy-api/routes/auth/keys.js @@ -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 ' diff --git a/dashcaddy-api/routes/auth/totp.js b/dashcaddy-api/routes/auth/totp.js index 450c55b..065b878 100644 --- a/dashcaddy-api/routes/auth/totp.js +++ b/dashcaddy-api/routes/auth/totp.js @@ -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')); diff --git a/dashcaddy-api/routes/browse.js b/dashcaddy-api/routes/browse.js index 8f8919b..9800364 100644 --- a/dashcaddy-api/routes/browse.js +++ b/dashcaddy-api/routes/browse.js @@ -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` diff --git a/dashcaddy-api/routes/ca.js b/dashcaddy-api/routes/ca.js index 7462308..71a1ab7 100644 --- a/dashcaddy-api/routes/ca.js +++ b/dashcaddy-api/routes/ca.js @@ -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; diff --git a/dashcaddy-api/routes/config/assets.js b/dashcaddy-api/routes/config/assets.js index 480b08c..17bfc0f 100644 --- a/dashcaddy-api/routes/config/assets.js +++ b/dashcaddy-api/routes/config/assets.js @@ -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; diff --git a/dashcaddy-api/routes/config/backup.js b/dashcaddy-api/routes/config/backup.js index 6d88d42..67d14d5 100644 --- a/dashcaddy-api/routes/config/backup.js +++ b/dashcaddy-api/routes/config/backup.js @@ -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')); diff --git a/dashcaddy-api/routes/config/settings.js b/dashcaddy-api/routes/config/settings.js index 7784c8b..dd09b6b 100644 --- a/dashcaddy-api/routes/config/settings.js +++ b/dashcaddy-api/routes/config/settings.js @@ -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; diff --git a/dashcaddy-api/routes/dns.js b/dashcaddy-api/routes/dns.js index b6f6f8a..01b6cc2 100644 --- a/dashcaddy-api/routes/dns.js +++ b/dashcaddy-api/routes/dns.js @@ -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 diff --git a/dashcaddy-api/routes/events.js b/dashcaddy-api/routes/events.js index 9827f4e..e2d2308 100644 --- a/dashcaddy-api/routes/events.js +++ b/dashcaddy-api/routes/events.js @@ -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; diff --git a/dashcaddy-api/routes/health.js b/dashcaddy-api/routes/health.js index b57f8e2..b26c569 100644 --- a/dashcaddy-api/routes/health.js +++ b/dashcaddy-api/routes/health.js @@ -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')); diff --git a/dashcaddy-api/routes/logs.js b/dashcaddy-api/routes/logs.js index b56f944..392482f 100644 --- a/dashcaddy-api/routes/logs.js +++ b/dashcaddy-api/routes/logs.js @@ -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, diff --git a/dashcaddy-api/routes/notifications.js b/dashcaddy-api/routes/notifications.js index aebffaf..16e9619 100644 --- a/dashcaddy-api/routes/notifications.js +++ b/dashcaddy-api/routes/notifications.js @@ -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 diff --git a/dashcaddy-api/routes/openclaw.js b/dashcaddy-api/routes/openclaw.js index c4e6689..2437d10 100644 --- a/dashcaddy-api/routes/openclaw.js +++ b/dashcaddy-api/routes/openclaw.js @@ -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); } })); diff --git a/dashcaddy-api/routes/recipes/deploy.js b/dashcaddy-api/routes/recipes/deploy.js index 79c6faa..4830d7f 100644 --- a/dashcaddy-api/routes/recipes/deploy.js +++ b/dashcaddy-api/routes/recipes/deploy.js @@ -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 }); diff --git a/dashcaddy-api/routes/recipes/index.js b/dashcaddy-api/routes/recipes/index.js index 1b10cf8..3a2447e 100644 --- a/dashcaddy-api/routes/recipes/index.js +++ b/dashcaddy-api/routes/recipes/index.js @@ -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.* diff --git a/dashcaddy-api/routes/recipes/manage.js b/dashcaddy-api/routes/recipes/manage.js index 9553753..e9f75b5 100644 --- a/dashcaddy-api/routes/recipes/manage.js +++ b/dashcaddy-api/routes/recipes/manage.js @@ -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 === diff --git a/dashcaddy-api/routes/services.js b/dashcaddy-api/routes/services.js index a87a3f7..bcc3856 100644 --- a/dashcaddy-api/routes/services.js +++ b/dashcaddy-api/routes/services.js @@ -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')); diff --git a/dashcaddy-api/routes/sites.js b/dashcaddy-api/routes/sites.js index c7e2e49..b2be215 100644 --- a/dashcaddy-api/routes/sites.js +++ b/dashcaddy-api/routes/sites.js @@ -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; diff --git a/dashcaddy-api/routes/tailscale.js b/dashcaddy-api/routes/tailscale.js index 6cf8f1f..4fcae96 100644 --- a/dashcaddy-api/routes/tailscale.js +++ b/dashcaddy-api/routes/tailscale.js @@ -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; diff --git a/dashcaddy-api/routes/updates.js b/dashcaddy-api/routes/updates.js index 1781d88..43b8e15 100644 --- a/dashcaddy-api/routes/updates.js +++ b/dashcaddy-api/routes/updates.js @@ -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; diff --git a/dashcaddy-api/routes/workflows.js b/dashcaddy-api/routes/workflows.js index 87f93d0..5501d24 100644 --- a/dashcaddy-api/routes/workflows.js +++ b/dashcaddy-api/routes/workflows.js @@ -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; diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index 0dbaa49..6e545cd 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -404,8 +404,7 @@ async function createApp() { appName = pkg.name || appName; } catch { /* package.json unreadable — keep fallback */ } apiRouter.get('/version', (req, res) => { - res.json({ - success: true, + ok(res, { name: appName, version: appVersion, node: process.version, @@ -608,15 +607,15 @@ async function createApp() { // Inline API routes apiRouter.get('/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); + ok(res, { status: 'ok', timestamp: new Date().toISOString() }); }); apiRouter.get('/csrf-token', (req, res) => { - res.json({ success: true, token: req.csrfToken, headerName: CSRF_HEADER_NAME }); + ok(res, { token: req.csrfToken, headerName: CSRF_HEADER_NAME }); }); apiRouter.get('/metrics', (req, res) => { - res.json({ success: true, metrics: metrics.getSummary() }); + ok(res, { metrics: metrics.getSummary() }); }); // Mount at /api/v1 (canonical, single version) @@ -624,7 +623,7 @@ async function createApp() { // Root-level health check app.get('/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); + ok(res, { status: 'ok', timestamp: new Date().toISOString() }); }); // Liveness probe — "is the process alive?" @@ -632,7 +631,7 @@ async function createApp() { // Used by k8s/Docker to decide whether to RESTART the container. // DO NOT add dependency checks here — those belong in /health/ready. app.get('/health/live', (req, res) => { - res.json({ status: 'alive', uptime: process.uptime() }); + ok(res, { status: 'alive', uptime: process.uptime() }); }); // Readiness probe — "is the app ready to serve traffic?" @@ -704,7 +703,7 @@ async function createApp() { timestamp: new Date().toISOString(), checks }; - res.status(allOk ? 200 : 503).json(body); + ok(res, body, allOk ? 200 : 503); })); // Lightweight probe endpoint @@ -830,7 +829,7 @@ async function createApp() { } } - res.json(result); + ok(res, result); } catch (error) { errorResponse(res, 500, safeErrorMessage(error)); } diff --git a/status/js/core/grid.js b/status/js/core/grid.js index bbdcf42..9ae1df5 100644 --- a/status/js/core/grid.js +++ b/status/js/core/grid.js @@ -65,7 +65,9 @@ if (window.SkeletonLoader) window.SkeletonLoader.show(6); const response = await fetch('/api/v1/services', { cache: 'no-store' }); if (response.ok) { - window.APPS = await response.json(); + const result = await response.json(); + // Standard envelope: { success: true, services: [...], pagination?: {...} } + window.APPS = result.services || []; if (window.SkeletonLoader) window.SkeletonLoader.hide(); } else { console.error('Failed to load services:', response.status);