Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53680c4c74 | ||
|
|
2d394d882d |
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.5.0] - 2026-05-17
|
||||||
|
|
||||||
### Changed (BREAKING)
|
### Changed (BREAKING)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.13.0
|
1.13.4
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('healthy');
|
expect(res.body.caStatus).toBe('healthy');
|
||||||
expect(res.body.daysUntilExpiration).toBeGreaterThan(90);
|
expect(res.body.daysUntilExpiration).toBeGreaterThan(90);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -551,7 +551,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('warning');
|
expect(res.body.caStatus).toBe('warning');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(90);
|
expect(res.body.daysUntilExpiration).toBeLessThan(90);
|
||||||
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(30);
|
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(30);
|
||||||
});
|
});
|
||||||
@@ -565,7 +565,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('critical');
|
expect(res.body.caStatus).toBe('critical');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(30);
|
expect(res.body.daysUntilExpiration).toBeLessThan(30);
|
||||||
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(0);
|
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(0);
|
||||||
});
|
});
|
||||||
@@ -579,7 +579,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('critical');
|
expect(res.body.caStatus).toBe('critical');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(7);
|
expect(res.body.daysUntilExpiration).toBeLessThan(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -592,7 +592,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('critical');
|
expect(res.body.caStatus).toBe('critical');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(0);
|
expect(res.body.daysUntilExpiration).toBeLessThan(0);
|
||||||
expect(res.body.message).toMatch(/EXPIRED/);
|
expect(res.body.message).toMatch(/EXPIRED/);
|
||||||
});
|
});
|
||||||
@@ -601,9 +601,9 @@ describe('Health Routes', () => {
|
|||||||
exists.mockResolvedValue(false);
|
exists.mockResolvedValue(false);
|
||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(404);
|
||||||
expect(res.body.status).toBe('error');
|
expect(res.body.caStatus).toBe('error');
|
||||||
expect(res.body.message).toMatch(/not found/);
|
expect(res.body.error).toMatch(/not found/);
|
||||||
expect(res.body.daysUntilExpiration).toBeNull();
|
expect(res.body.daysUntilExpiration).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -612,9 +612,9 @@ describe('Health Routes', () => {
|
|||||||
execSync.mockImplementation(() => { throw new Error('openssl not found'); });
|
execSync.mockImplementation(() => { throw new Error('openssl not found'); });
|
||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(500);
|
||||||
expect(res.body.status).toBe('error');
|
expect(res.body.caStatus).toBe('error');
|
||||||
expect(res.body.message).toBe('openssl not found');
|
expect(res.body.error).toBe('openssl not found');
|
||||||
expect(res.body.daysUntilExpiration).toBeNull();
|
expect(res.body.daysUntilExpiration).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -103,12 +103,12 @@ describe('Services Routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /api/services', () => {
|
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);
|
exists.mockResolvedValue(false);
|
||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/services');
|
const res = await request(app).get('/api/services');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body).toEqual([]);
|
expect(res.body).toEqual({ success: true, services: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns services list', async () => {
|
it('returns services list', async () => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const cryptoUtils = require('./crypto-utils');
|
const cryptoUtils = require('./crypto-utils');
|
||||||
|
const { errorResponse } = require('./src/utils/responses');
|
||||||
|
|
||||||
const CSRF_TOKEN_LENGTH = 32;
|
const CSRF_TOKEN_LENGTH = 32;
|
||||||
const CSRF_COOKIE_NAME = 'dashcaddy_csrf';
|
const CSRF_COOKIE_NAME = 'dashcaddy_csrf';
|
||||||
@@ -169,18 +170,14 @@ function csrfValidationMiddleware(req, res, next) {
|
|||||||
// Validate both values exist
|
// Validate both values exist
|
||||||
if (!cookieNonce) {
|
if (!cookieNonce) {
|
||||||
console.warn(`[CSRF] Missing CSRF cookie: ${method} ${req.path} from ${req.ip}`);
|
console.warn(`[CSRF] Missing CSRF cookie: ${method} ${req.path} from ${req.ip}`);
|
||||||
return res.status(403).json({
|
return errorResponse(res, 403, '[DC-100] CSRF token missing', {
|
||||||
success: false,
|
|
||||||
error: '[DC-100] CSRF token missing',
|
|
||||||
message: 'CSRF cookie not found. Please refresh the page (Ctrl+Shift+R) and try again.'
|
message: 'CSRF cookie not found. Please refresh the page (Ctrl+Shift+R) and try again.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!headerToken) {
|
if (!headerToken) {
|
||||||
console.warn(`[CSRF] Missing CSRF header: ${method} ${req.path} from ${req.ip}`);
|
console.warn(`[CSRF] Missing CSRF header: ${method} ${req.path} from ${req.ip}`);
|
||||||
return res.status(403).json({
|
return errorResponse(res, 403, '[DC-100] CSRF token missing', {
|
||||||
success: false,
|
|
||||||
error: '[DC-100] CSRF token missing',
|
|
||||||
message: 'CSRF token not provided in request headers. Please refresh the page (Ctrl+Shift+R) and try again.'
|
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) {
|
} catch (err) {
|
||||||
console.warn(`[CSRF] Invalid CSRF token: ${method} ${req.path} from ${req.ip} - ${err.message}`);
|
console.warn(`[CSRF] Invalid CSRF token: ${method} ${req.path} from ${req.ip} - ${err.message}`);
|
||||||
return res.status(403).json({
|
return errorResponse(res, 403, '[DC-101] CSRF token invalid', {
|
||||||
success: false,
|
|
||||||
error: '[DC-101] CSRF token invalid',
|
|
||||||
message: 'CSRF token validation failed. Please refresh the page (Ctrl+Shift+R) and try again.'
|
message: 'CSRF token validation failed. Please refresh the page (Ctrl+Shift+R) and try again.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const path = require('path');
|
|||||||
const { AppError } = require('./errors');
|
const { AppError } = require('./errors');
|
||||||
const { LIMITS } = require('./constants');
|
const { LIMITS } = require('./constants');
|
||||||
const { logError: unifiedLogError, safeErrorMessage } = require('./src/utils/logging');
|
const { logError: unifiedLogError, safeErrorMessage } = require('./src/utils/logging');
|
||||||
|
const { errorResponse } = require('./src/utils/responses');
|
||||||
|
|
||||||
const ERROR_LOG_FILE = path.join(__dirname, 'error.log');
|
const ERROR_LOG_FILE = path.join(__dirname, 'error.log');
|
||||||
const MAX_ERROR_LOG_SIZE = LIMITS.ERROR_LOG_SIZE;
|
const MAX_ERROR_LOG_SIZE = LIMITS.ERROR_LOG_SIZE;
|
||||||
@@ -43,27 +44,23 @@ function errorMiddleware(err, req, res, next) {
|
|||||||
// Error code (DC-XXX format)
|
// Error code (DC-XXX format)
|
||||||
const code = err.code || `DC-${statusCode}`;
|
const code = err.code || `DC-${statusCode}`;
|
||||||
|
|
||||||
// Build response
|
// Build extras for response
|
||||||
const response = {
|
const extras = { code };
|
||||||
success: false,
|
|
||||||
error: isOperational ? safeErrorMessage(err) : 'Internal server error',
|
|
||||||
code
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add optional fields if present
|
// Add optional fields if present
|
||||||
if (err.requiresTotp) response.requiresTotp = true;
|
if (err.requiresTotp) extras.requiresTotp = true;
|
||||||
if (err.retryAfter) response.retryAfter = err.retryAfter;
|
if (err.retryAfter) extras.retryAfter = err.retryAfter;
|
||||||
if (err.field) response.field = err.field;
|
if (err.field) extras.field = err.field;
|
||||||
if (err.resource) response.resource = err.resource;
|
if (err.resource) extras.resource = err.resource;
|
||||||
if (err.details && Object.keys(err.details).length > 0) response.details = err.details;
|
if (err.details && Object.keys(err.details).length > 0) extras.details = err.details;
|
||||||
|
|
||||||
// Development mode: include stack trace
|
// Development mode: include stack trace
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
response.stack = err.stack;
|
extras.stack = err.stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send response
|
// Send response
|
||||||
res.status(statusCode).json(response);
|
errorResponse(res, statusCode, isOperational ? safeErrorMessage(err) : 'Internal server error', extras);
|
||||||
|
|
||||||
// For non-operational errors, log as fatal
|
// For non-operational errors, log as fatal
|
||||||
if (!isOperational) {
|
if (!isOperational) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const os = require('os');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { verifyCode, parseCode, VALID_DURATIONS } = require('./license-keygen');
|
const { verifyCode, parseCode, VALID_DURATIONS } = require('./license-keygen');
|
||||||
|
const { errorResponse } = require('./src/utils/responses');
|
||||||
|
|
||||||
const LICENSE_CRED_KEY = 'license.activation';
|
const LICENSE_CRED_KEY = 'license.activation';
|
||||||
const LICENSE_SERVER_URL = process.env.LICENSE_SERVER_URL || null; // Set when license server exists
|
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 };
|
const featureInfo = PREMIUM_FEATURES[feature] || { name: feature };
|
||||||
return res.status(403).json({
|
return errorResponse(res, 403, `${featureInfo.name} requires a DashCaddy Premium subscription.`, {
|
||||||
success: false,
|
|
||||||
error: `${featureInfo.name} requires a DashCaddy Premium subscription.`,
|
|
||||||
premiumRequired: true,
|
premiumRequired: true,
|
||||||
feature,
|
feature,
|
||||||
featureName: featureInfo.name,
|
featureName: featureInfo.name,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const crypto = require('crypto');
|
|||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const { createCSRFMiddleware, csrfValidationMiddleware, CSRF_HEADER_NAME } = require('./csrf-protection');
|
const { createCSRFMiddleware, csrfValidationMiddleware, CSRF_HEADER_NAME } = require('./csrf-protection');
|
||||||
const { RATE_LIMITS, LIMITS, APP } = require('./constants');
|
const { RATE_LIMITS, LIMITS, APP } = require('./constants');
|
||||||
|
const { errorResponse, unauthorized, forbidden, validationError } = require('./src/utils/responses');
|
||||||
const { CACHE_CONFIGS, createCache } = require('./cache-config');
|
const { CACHE_CONFIGS, createCache } = require('./cache-config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +34,7 @@ module.exports = function configureMiddleware(app, {
|
|||||||
// ── Container ID param validation ──
|
// ── Container ID param validation ──
|
||||||
app.param('id', (req, res, next, id) => {
|
app.param('id', (req, res, next, id) => {
|
||||||
if (req.path.includes('/containers/') && !isValidContainerId(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();
|
next();
|
||||||
});
|
});
|
||||||
@@ -127,9 +128,7 @@ module.exports = function configureMiddleware(app, {
|
|||||||
const fromTailscale = ipsToCheck.some(ip => isTailscaleIP(ip.toString().split(',')[0].trim()));
|
const fromTailscale = ipsToCheck.some(ip => isTailscaleIP(ip.toString().split(',')[0].trim()));
|
||||||
|
|
||||||
if (!fromTailscale) {
|
if (!fromTailscale) {
|
||||||
return res.status(403).json({
|
return errorResponse(res, 403, '[DC-120] Access denied. This dashboard requires Tailscale connection.', {
|
||||||
success: false,
|
|
||||||
error: '[DC-120] Access denied. This dashboard requires Tailscale connection.',
|
|
||||||
requiresTailscale: true,
|
requiresTailscale: true,
|
||||||
clientIP: clientIP
|
clientIP: clientIP
|
||||||
});
|
});
|
||||||
@@ -150,9 +149,7 @@ module.exports = function configureMiddleware(app, {
|
|||||||
for (const ip of (peer.TailscaleIPs || [])) knownIPs.add(ip);
|
for (const ip of (peer.TailscaleIPs || [])) knownIPs.add(ip);
|
||||||
}
|
}
|
||||||
if (!knownIPs.has(clientTailscaleIP)) {
|
if (!knownIPs.has(clientTailscaleIP)) {
|
||||||
return res.status(403).json({
|
return errorResponse(res, 403, '[DC-121] Access denied. Device not in allowed tailnet.', {
|
||||||
success: false,
|
|
||||||
error: '[DC-121] Access denied. Device not in allowed tailnet.',
|
|
||||||
requiresTailscale: true,
|
requiresTailscale: true,
|
||||||
clientIP
|
clientIP
|
||||||
});
|
});
|
||||||
@@ -358,7 +355,7 @@ module.exports = function configureMiddleware(app, {
|
|||||||
if (isPublicRoute(req)) return next();
|
if (isPublicRoute(req)) return next();
|
||||||
if (isSessionValid(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);
|
app.use(totpAuthMiddleware);
|
||||||
@@ -406,9 +403,7 @@ module.exports = function configureMiddleware(app, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No valid auth — reject
|
// No valid auth — reject
|
||||||
return res.status(401).json({
|
return errorResponse(res, 401, '[DC-110] Authentication required - provide TOTP session, JWT token, or API key', {
|
||||||
success: false,
|
|
||||||
error: '[DC-110] Authentication required - provide TOTP session, JWT token, or API key',
|
|
||||||
requiresTotp: totpConfig.enabled
|
requiresTotp: totpConfig.enabled
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dashcaddy-api",
|
"name": "dashcaddy-api",
|
||||||
"version": "1.13.2",
|
"version": "1.13.4",
|
||||||
"description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management",
|
"description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const yaml = require('js-yaml');
|
|||||||
const { DOCKER, REGEX } = require('../../constants');
|
const { DOCKER, REGEX } = require('../../constants');
|
||||||
const { ValidationError } = require('../../errors');
|
const { ValidationError } = require('../../errors');
|
||||||
const platformPaths = require('../../platform-paths');
|
const platformPaths = require('../../platform-paths');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Docker Compose import routes
|
* 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 name = (stackName || 'stack').replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 32) || 'stack';
|
||||||
const result = parseCompose(yamlStr, name);
|
const result = parseCompose(yamlStr, name);
|
||||||
res.json({ success: true, ...result });
|
ok(res, { ...result });
|
||||||
}, 'compose-import'));
|
}, 'compose-import'));
|
||||||
|
|
||||||
// POST /deploy-compose — deploy parsed services
|
// 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 });
|
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'));
|
}, 'compose-deploy'));
|
||||||
|
|
||||||
// DELETE /compose-stack/:stackName — remove an entire stack
|
// 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; });
|
await servicesStateManager.update(data => { data.services = updated; });
|
||||||
|
|
||||||
res.json({ success: true, removed, count: removed.length });
|
ok(res, { removed, count: removed.length });
|
||||||
}, 'compose-stack-delete'));
|
}, 'compose-stack-delete'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { exists } = require('../../fs-helpers');
|
|||||||
const platformPaths = require('../../platform-paths');
|
const platformPaths = require('../../platform-paths');
|
||||||
const { ValidationError } = require('../../errors');
|
const { ValidationError } = require('../../errors');
|
||||||
const { logError } = require('../../src/utils/logging');
|
const { logError } = require('../../src/utils/logging');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
/**
|
/**
|
||||||
* Apps deployment routes factory
|
* Apps deployment routes factory
|
||||||
* @param {Object} deps - Explicit dependencies
|
* @param {Object} deps - Explicit dependencies
|
||||||
@@ -243,9 +244,9 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
|||||||
if (!template) throw new ValidationError('Invalid app template');
|
if (!template) throw new ValidationError('Invalid app template');
|
||||||
const existingContainer = await helpers.findExistingContainerByImage(template);
|
const existingContainer = await helpers.findExistingContainerByImage(template);
|
||||||
if (existingContainer) {
|
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 {
|
} 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'));
|
}, 'check-existing'));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { exists } = require('../../fs-helpers');
|
const { exists } = require('../../fs-helpers');
|
||||||
const { logError } = require('../../src/utils/logging');
|
const { logError } = require('../../src/utils/logging');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
module.exports = function({
|
module.exports = function({
|
||||||
docker, caddy, servicesStateManager, asyncHandler, log, helpers,
|
docker, caddy, servicesStateManager, asyncHandler, log, helpers,
|
||||||
@@ -135,7 +136,7 @@ module.exports = function({
|
|||||||
results.service = error.message;
|
results.service = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, message: `App ${appId} removal completed`, results });
|
ok(res, { message: `App ${appId} removal completed`, results });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await logError('app-removal', error);
|
await logError('app-removal', error);
|
||||||
errorResponse(res, 500, ctx.safeErrorMessage(error), { results });
|
errorResponse(res, 500, ctx.safeErrorMessage(error), { results });
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { DOCKER } = require('../../constants');
|
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');
|
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);
|
const result = await restoreService(service);
|
||||||
res.json({ success: true, result });
|
ok(res, { result });
|
||||||
}, 'apps-restore'));
|
}, 'apps-restore'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,8 +60,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
|
|||||||
const restoreable = services.filter(s => s.deploymentManifest);
|
const restoreable = services.filter(s => s.deploymentManifest);
|
||||||
|
|
||||||
if (restoreable.length === 0) {
|
if (restoreable.length === 0) {
|
||||||
return res.json({
|
return ok(res, {
|
||||||
success: true,
|
|
||||||
message: 'No services have deployment manifests to restore',
|
message: 'No services have deployment manifests to restore',
|
||||||
results: []
|
results: []
|
||||||
});
|
});
|
||||||
@@ -85,8 +85,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
|
|||||||
const skipped = results.filter(r => r.status === 'skipped').length;
|
const skipped = results.filter(r => r.status === 'skipped').length;
|
||||||
const failed = results.filter(r => r.status === 'failed').length;
|
const failed = results.filter(r => r.status === 'failed').length;
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
message: `Restore complete: ${succeeded} restored, ${skipped} skipped, ${failed} failed`,
|
message: `Restore complete: ${succeeded} restored, ${skipped} skipped, ${failed} failed`,
|
||||||
results
|
results
|
||||||
});
|
});
|
||||||
@@ -123,7 +122,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
|
|||||||
status.push(entry);
|
status.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, services: status });
|
ok(res, { services: status });
|
||||||
}, 'apps-restore-status'));
|
}, 'apps-restore-status'));
|
||||||
|
|
||||||
// ==================== POINT-IN-TIME RESTORE (BACKUP FILE BASED) ====================
|
// ==================== 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)
|
// Sort by timestamp descending (newest first)
|
||||||
files.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
files.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
appId,
|
appId,
|
||||||
isBackupFile: true,
|
isBackupFile: true,
|
||||||
files,
|
files,
|
||||||
@@ -190,12 +188,12 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
|
|||||||
|
|
||||||
// Security: prevent path traversal
|
// Security: prevent path traversal
|
||||||
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
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);
|
const filepath = path.join(DEFAULT_BACKUP_DIR, filename);
|
||||||
if (!fs.existsSync(filepath)) {
|
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 {
|
try {
|
||||||
@@ -207,7 +205,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
|
|||||||
try {
|
try {
|
||||||
fileData = await backupManager.decryptBackup(fileData, encryptionKey);
|
fileData = await backupManager.decryptBackup(fileData, encryptionKey);
|
||||||
} catch (err) {
|
} 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
|
// Cleanup temp dir
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
isBackupFile: true,
|
isBackupFile: true,
|
||||||
restored: {
|
restored: {
|
||||||
services: !!restoreData.services,
|
services: !!restoreData.services,
|
||||||
@@ -277,8 +274,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
|
|||||||
} else {
|
} else {
|
||||||
// Preview mode
|
// Preview mode
|
||||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
isBackupFile: true,
|
isBackupFile: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
filename,
|
filename,
|
||||||
@@ -296,7 +292,7 @@ module.exports = function({ docker, caddy, servicesStateManager, asyncHandler, e
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
errorResponse(res, 500, err.message);
|
||||||
}
|
}
|
||||||
}, 'apps-revert'));
|
}, 'apps-revert'));
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const { exists } = require('../../fs-helpers');
|
|||||||
* @returns {express.Router}
|
* @returns {express.Router}
|
||||||
*/
|
*/
|
||||||
const { REGEX } = require('../../constants');
|
const { REGEX } = require('../../constants');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
module.exports = function({
|
module.exports = function({
|
||||||
servicesStateManager, asyncHandler, helpers,
|
servicesStateManager, asyncHandler, helpers,
|
||||||
@@ -42,8 +43,7 @@ module.exports = function({
|
|||||||
|
|
||||||
// Get available app templates
|
// Get available app templates
|
||||||
router.get('/templates', asyncHandler(async (req, res) => {
|
router.get('/templates', asyncHandler(async (req, res) => {
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
templates: ctx.APP_TEMPLATES,
|
templates: ctx.APP_TEMPLATES,
|
||||||
categories: ctx.TEMPLATE_CATEGORIES,
|
categories: ctx.TEMPLATE_CATEGORIES,
|
||||||
difficultyLevels: ctx.DIFFICULTY_LEVELS
|
difficultyLevels: ctx.DIFFICULTY_LEVELS
|
||||||
@@ -58,7 +58,7 @@ module.exports = function({
|
|||||||
const { NotFoundError } = require('../../errors');
|
const { NotFoundError } = require('../../errors');
|
||||||
throw new NotFoundError('App template');
|
throw new NotFoundError('App template');
|
||||||
}
|
}
|
||||||
res.json({ success: true, template });
|
ok(res, { template });
|
||||||
}, 'apps-template-detail'));
|
}, 'apps-template-detail'));
|
||||||
|
|
||||||
// Check port availability
|
// Check port availability
|
||||||
@@ -80,7 +80,7 @@ module.exports = function({
|
|||||||
const usedPorts = await docker.getUsedPorts();
|
const usedPorts = await docker.getUsedPorts();
|
||||||
for (let port = basePort; port < basePort + maxAttempts; port++) {
|
for (let port = basePort; port < basePort + maxAttempts; port++) {
|
||||||
if (!usedPorts.has(port)) {
|
if (!usedPorts.has(port)) {
|
||||||
res.json({ success: true, suggestedPort: port, basePort });
|
ok(res, { suggestedPort: port, basePort });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,8 +170,7 @@ module.exports = function({
|
|||||||
log.warn('deploy', 'Service update warning', { error: error.message || String(error) });
|
log.warn('deploy', 'Service update warning', { error: error.message || String(error) });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
message: `Subdomain updated: ${oldSubdomain} -> ${newSubdomain}`,
|
message: `Subdomain updated: ${oldSubdomain} -> ${newSubdomain}`,
|
||||||
newUrl: `https://${ctx.buildDomain(newSubdomain)}`,
|
newUrl: `https://${ctx.buildDomain(newSubdomain)}`,
|
||||||
results
|
results
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { APP_PORTS, ARR_SERVICES } = require('../../constants');
|
|||||||
const { validateURL, validateToken } = require('../../input-validator');
|
const { validateURL, validateToken } = require('../../input-validator');
|
||||||
const { ValidationError, AuthenticationError, NotFoundError } = require('../../errors');
|
const { ValidationError, AuthenticationError, NotFoundError } = require('../../errors');
|
||||||
const { logError } = require('../../src/utils/logging');
|
const { logError } = require('../../src/utils/logging');
|
||||||
|
const { ok, successMessage } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arr configuration routes factory
|
* Arr configuration routes factory
|
||||||
@@ -258,11 +259,7 @@ module.exports = function(ctx) {
|
|||||||
const version = service === 'plex' ? data.MediaContainer?.version : data.version;
|
const version = service === 'plex' ? data.MediaContainer?.version : data.version;
|
||||||
const appName = service === 'plex' ? 'Plex' : data.appName;
|
const appName = service === 'plex' ? 'Plex' : data.appName;
|
||||||
log.info('arr', 'Service connection successful', { service, appName, version });
|
log.info('arr', 'Service connection successful', { service, appName, version });
|
||||||
return res.json({
|
return ok(res, { version, appName });
|
||||||
success: true,
|
|
||||||
version,
|
|
||||||
appName
|
|
||||||
});
|
|
||||||
} else if (response.status === 401) {
|
} else if (response.status === 401) {
|
||||||
throw new AuthenticationError('Invalid API key');
|
throw new AuthenticationError('Invalid API key');
|
||||||
} else if (response.status === 404) {
|
} else if (response.status === 404) {
|
||||||
@@ -553,7 +550,7 @@ module.exports = function(ctx) {
|
|||||||
const metadata = await credentialManager.getMetadata(`arr.${service}.apikey`);
|
const metadata = await credentialManager.getMetadata(`arr.${service}.apikey`);
|
||||||
const storedProfileId = metadata?.qualityProfileId || null;
|
const storedProfileId = metadata?.qualityProfileId || null;
|
||||||
|
|
||||||
res.json({ success: true, profiles: mapped, storedProfileId });
|
ok(res, { profiles: mapped, storedProfileId });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.cause?.code === 'ECONNREFUSED') {
|
if (e.cause?.code === 'ECONNREFUSED') {
|
||||||
return errorResponse(res, 502, 'Connection refused — is the service running?');
|
return errorResponse(res, 502, 'Connection refused — is the service running?');
|
||||||
@@ -588,7 +585,7 @@ module.exports = function(ctx) {
|
|||||||
existing.qualityProfileName = qualityProfileName || null;
|
existing.qualityProfileName = qualityProfileName || null;
|
||||||
await credentialManager.storeMetadata(credKey, existing);
|
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'));
|
}, 'arr-quality-profile-save'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { validateURL, validateToken } = require('../../input-validator');
|
const { validateURL, validateToken } = require('../../input-validator');
|
||||||
const { ValidationError } = require('../../errors');
|
const { ValidationError } = require('../../errors');
|
||||||
|
const { ok, successMessage } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arr credentials routes factory
|
* 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 });
|
log.info('arr', 'Stored API key', { service, verified: connectionTest?.success || false });
|
||||||
|
|
||||||
res.json({
|
ok(res, { message: `${service} API key stored`, connectionTest, url: resolvedUrl });
|
||||||
success: true,
|
|
||||||
message: `${service} API key stored`,
|
|
||||||
connectionTest,
|
|
||||||
url: resolvedUrl
|
|
||||||
});
|
|
||||||
}, 'arr-credentials-store'));
|
}, 'arr-credentials-store'));
|
||||||
|
|
||||||
// List stored arr credentials (keys only, not values)
|
// List stored arr credentials (keys only, not values)
|
||||||
@@ -131,7 +127,7 @@ module.exports = function({ credentialManager, servicesStateManager, asyncHandle
|
|||||||
// Get seedbox base URL
|
// Get seedbox base URL
|
||||||
const seedboxBaseUrl = await credentialManager.retrieve('arr.seedbox.baseurl');
|
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'));
|
}, 'arr-credentials-list'));
|
||||||
|
|
||||||
// Delete stored arr credentials
|
// Delete stored arr credentials
|
||||||
@@ -140,7 +136,7 @@ module.exports = function({ credentialManager, servicesStateManager, asyncHandle
|
|||||||
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
|
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
|
||||||
await credentialManager.delete(credKey);
|
await credentialManager.delete(credKey);
|
||||||
log.info('arr', 'Deleted credentials', { service });
|
log.info('arr', 'Deleted credentials', { service });
|
||||||
res.json({ success: true, message: `${service} credentials removed` });
|
successMessage(res, `${service} credentials removed`);
|
||||||
}, 'arr-credentials-delete'));
|
}, 'arr-credentials-delete'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { APP_PORTS, ARR_SERVICES } = require('../../constants');
|
const { APP_PORTS, ARR_SERVICES } = require('../../constants');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arr service detection routes factory
|
* 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);
|
detected.plex.token = await helpers.getPlexToken(detected.plex.containerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
services: detected,
|
services: detected,
|
||||||
summary: {
|
summary: {
|
||||||
plexReady: !!(detected.plex?.token),
|
plexReady: !!(detected.plex?.token),
|
||||||
@@ -287,7 +287,7 @@ module.exports = function({ docker, servicesStateManager, credentialManager, fet
|
|||||||
readyForAutoConnect: statuses.filter(s => s.status === 'connected').length >= 2
|
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'));
|
}, 'smart-detect'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { APP_PORTS } = require('../../constants');
|
const { APP_PORTS } = require('../../constants');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plex routes factory
|
* Plex routes factory
|
||||||
@@ -86,7 +87,7 @@ module.exports = function({ fetchT, asyncHandler, errorResponse, log: _log, help
|
|||||||
lastVerified: new Date().toISOString()
|
lastVerified: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, serverName, version, libraries });
|
ok(res, { serverName, version, libraries });
|
||||||
}, 'plex-libraries'));
|
}, 'plex-libraries'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { ValidationError, ForbiddenError, NotFoundError } = require('../../errors');
|
const { ValidationError, ForbiddenError, NotFoundError } = require('../../errors');
|
||||||
|
const { ok, successMessage } = require('../../src/utils/responses');
|
||||||
/**
|
/**
|
||||||
* Auth API keys routes factory
|
* Auth API keys routes factory
|
||||||
* @param {Object} deps - Explicit dependencies
|
* @param {Object} deps - Explicit dependencies
|
||||||
@@ -39,7 +40,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const keys = await authManager.listAPIKeys();
|
const keys = await authManager.listAPIKeys();
|
||||||
res.json({ success: true, keys });
|
ok(res, { keys });
|
||||||
}, 'auth-keys-list'));
|
}, 'auth-keys-list'));
|
||||||
|
|
||||||
// Generate new API key
|
// Generate new API key
|
||||||
@@ -66,8 +67,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
|
|||||||
scopes || ['read', 'write']
|
scopes || ['read', 'write']
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
key: keyData.key,
|
key: keyData.key,
|
||||||
id: keyData.id,
|
id: keyData.id,
|
||||||
name: keyData.name,
|
name: keyData.name,
|
||||||
@@ -93,7 +93,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
|
|||||||
const success = await authManager.revokeAPIKey(keyId);
|
const success = await authManager.revokeAPIKey(keyId);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.json({ success: true, message: 'API key revoked successfully' });
|
successMessage(res, 'API key revoked successfully');
|
||||||
} else {
|
} else {
|
||||||
throw new NotFoundError(`API key ${keyId}`);
|
throw new NotFoundError(`API key ${keyId}`);
|
||||||
}
|
}
|
||||||
@@ -126,8 +126,7 @@ module.exports = function({ authManager, asyncHandler, log }) {
|
|||||||
const expiresInMs = parseExpiration(expiresIn || '24h');
|
const expiresInMs = parseExpiration(expiresIn || '24h');
|
||||||
const expiresAt = new Date(Date.now() + expiresInMs).toISOString();
|
const expiresAt = new Date(Date.now() + expiresInMs).toISOString();
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
token,
|
token,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
usage: 'Include in Authorization header as: Bearer <token>'
|
usage: 'Include in Authorization header as: Bearer <token>'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { ValidationError, AuthenticationError } = require('../../errors');
|
const { ValidationError, AuthenticationError } = require('../../errors');
|
||||||
|
const { ok, successMessage } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth TOTP routes factory
|
* Auth TOTP routes factory
|
||||||
@@ -27,8 +28,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
|
|||||||
|
|
||||||
// Get current TOTP config (public route)
|
// Get current TOTP config (public route)
|
||||||
router.get('/totp/config', asyncHandler(async (req, res) => {
|
router.get('/totp/config', asyncHandler(async (req, res) => {
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
config: {
|
config: {
|
||||||
enabled: ctx.totpConfig.enabled,
|
enabled: ctx.totpConfig.enabled,
|
||||||
sessionDuration: ctx.totpConfig.sessionDuration,
|
sessionDuration: ctx.totpConfig.sessionDuration,
|
||||||
@@ -62,7 +62,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
|
|||||||
color: { dark: '#ffffff', light: '#00000000' }
|
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'));
|
}, 'totp-setup'));
|
||||||
|
|
||||||
// Verify first code to confirm setup, then activate TOTP
|
// 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.create(req, ctx.totpConfig.sessionDuration);
|
||||||
ctx.session.setCookie(res, 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'));
|
}, 'totp-verify-setup'));
|
||||||
|
|
||||||
// Login: verify TOTP code and set session cookie
|
// 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');
|
const newCsrfToken = renewCSRFToken(res, req.secure || req.protocol === 'https');
|
||||||
|
|
||||||
log.debug('auth', 'Session created', { sessions: ctx.session.ipSessions.size });
|
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'));
|
}, 'totp-verify'));
|
||||||
|
|
||||||
// Check session validity (used by Caddy forward_auth)
|
// 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.clear(req);
|
||||||
ctx.session.clearCookie(res);
|
ctx.session.clearCookie(res);
|
||||||
res.json({ success: true, message: 'TOTP disabled' });
|
successMessage(res, 'TOTP disabled');
|
||||||
}, 'totp-disable'));
|
}, 'totp-disable'));
|
||||||
|
|
||||||
// Update TOTP settings (session duration)
|
// Update TOTP settings (session duration)
|
||||||
@@ -204,8 +204,7 @@ module.exports = function({ authManager, credentialManager, totpConfig, saveTotp
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ctx.saveTotpConfig();
|
await ctx.saveTotpConfig();
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
config: { enabled: ctx.totpConfig.enabled, sessionDuration: ctx.totpConfig.sessionDuration, isSetUp: ctx.totpConfig.isSetUp }
|
config: { enabled: ctx.totpConfig.enabled, sessionDuration: ctx.totpConfig.sessionDuration, isSetUp: ctx.totpConfig.isSetUp }
|
||||||
});
|
});
|
||||||
}, 'totp-config'));
|
}, 'totp-config'));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const path = require('path');
|
|||||||
const { exists, isAccessible } = require('../fs-helpers');
|
const { exists, isAccessible } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const { ValidationError, ForbiddenError } = require('../errors');
|
const { ValidationError, ForbiddenError } = require('../errors');
|
||||||
|
const { ok } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse route factory
|
* 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-roots'));
|
||||||
|
|
||||||
// Browse directory contents
|
// Browse directory contents
|
||||||
@@ -64,7 +65,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke
|
|||||||
roots.push(r);
|
roots.push(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.json({ success: true, path: '', items: roots });
|
return ok(res, { path: '', items: roots });
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchingRoot = BROWSE_ROOTS.find(r =>
|
const matchingRoot = BROWSE_ROOTS.find(r =>
|
||||||
@@ -124,8 +125,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke
|
|||||||
|
|
||||||
const paginationParams = parsePaginationParams(req.query);
|
const paginationParams = parsePaginationParams(req.query);
|
||||||
const result = paginate(folders, paginationParams);
|
const result = paginate(folders, paginationParams);
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
path: requestedPath,
|
path: requestedPath,
|
||||||
parent: path.dirname(requestedPath).replace(/\\/g, '/') || null,
|
parent: path.dirname(requestedPath).replace(/\\/g, '/') || null,
|
||||||
items: result.data,
|
items: result.data,
|
||||||
@@ -190,8 +190,7 @@ module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
mounts: detectedMounts,
|
mounts: detectedMounts,
|
||||||
message: detectedMounts.length > 0
|
message: detectedMounts.length > 0
|
||||||
? `Found ${detectedMounts.length} media mount(s) from existing containers`
|
? `Found ${detectedMounts.length} media mount(s) from existing containers`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const path = require('path');
|
|||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
const { ValidationError } = require('../errors');
|
const { ValidationError } = require('../errors');
|
||||||
|
const { ok } = require('../src/utils/responses');
|
||||||
const platformPaths = require('../platform-paths');
|
const platformPaths = require('../platform-paths');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function(ctx) {
|
||||||
@@ -26,8 +27,7 @@ module.exports = function(ctx) {
|
|||||||
const expirationDate = new Date(certInfo.validUntil);
|
const expirationDate = new Date(certInfo.validUntil);
|
||||||
const daysUntilExpiration = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
|
const daysUntilExpiration = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
certificate: {
|
certificate: {
|
||||||
name: certInfo.name,
|
name: certInfo.name,
|
||||||
fingerprint: certInfo.fingerprint,
|
fingerprint: certInfo.fingerprint,
|
||||||
@@ -243,7 +243,7 @@ ${safeDomain.includes('.') ? `DNS.2 = *.${safeDomain}` : ''}`;
|
|||||||
const certsDir = platformPaths.generatedCertsDir;
|
const certsDir = platformPaths.generatedCertsDir;
|
||||||
|
|
||||||
if (!await exists(certsDir)) {
|
if (!await exists(certsDir)) {
|
||||||
return res.json({ success: true, certificates: [] });
|
return ok(res, { certificates: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirEntries = await fsp.readdir(certsDir);
|
const dirEntries = await fsp.readdir(certsDir);
|
||||||
@@ -278,7 +278,7 @@ ${safeDomain.includes('.') ? `DNS.2 = *.${safeDomain}` : ''}`;
|
|||||||
}
|
}
|
||||||
}))).filter(Boolean);
|
}))).filter(Boolean);
|
||||||
|
|
||||||
res.json({ success: true, certificates });
|
ok(res, { certificates });
|
||||||
}, 'ca-certs'));
|
}, 'ca-certs'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const { LIMITS } = require('../../constants');
|
|||||||
const { exists } = require('../../fs-helpers');
|
const { exists } = require('../../fs-helpers');
|
||||||
const { ValidationError } = require('../../errors');
|
const { ValidationError } = require('../../errors');
|
||||||
const platformPaths = require('../../platform-paths');
|
const platformPaths = require('../../platform-paths');
|
||||||
|
const { ok, successMessage } = require('../../src/utils/responses');
|
||||||
/**
|
/**
|
||||||
* Config assets routes factory
|
* Config assets routes factory
|
||||||
* @param {Object} deps - Explicit dependencies
|
* @param {Object} deps - Explicit dependencies
|
||||||
@@ -63,8 +64,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
const filePath = path.join(assetsPath, safeFilename);
|
const filePath = path.join(assetsPath, safeFilename);
|
||||||
await fsp.writeFile(filePath, buffer);
|
await fsp.writeFile(filePath, buffer);
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
path: `/assets/${safeFilename}`,
|
path: `/assets/${safeFilename}`,
|
||||||
message: `Logo saved to ${filePath}`
|
message: `Logo saved to ${filePath}`
|
||||||
});
|
});
|
||||||
@@ -76,8 +76,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
// Get current logo path, position, and title
|
// Get current logo path, position, and title
|
||||||
router.get('/logo', asyncHandler(async (req, res) => {
|
router.get('/logo', asyncHandler(async (req, res) => {
|
||||||
const config = await ctx.readConfig();
|
const config = await ctx.readConfig();
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
// Dark/light variants (new)
|
// Dark/light variants (new)
|
||||||
customLogoDark: config.customLogoDark || null,
|
customLogoDark: config.customLogoDark || null,
|
||||||
customLogoLight: config.customLogoLight || null,
|
customLogoLight: config.customLogoLight || null,
|
||||||
@@ -156,8 +155,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
config.updatedAt = new Date().toISOString();
|
config.updatedAt = new Date().toISOString();
|
||||||
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
pathDark: pathDark,
|
pathDark: pathDark,
|
||||||
pathLight: pathLight,
|
pathLight: pathLight,
|
||||||
// Legacy compat
|
// Legacy compat
|
||||||
@@ -195,10 +193,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
config.updatedAt = new Date().toISOString();
|
config.updatedAt = new Date().toISOString();
|
||||||
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
|
||||||
res.json({
|
successMessage(res, 'Branding reset to defaults');
|
||||||
success: true,
|
|
||||||
message: 'Branding reset to defaults'
|
|
||||||
});
|
|
||||||
}, 'logo-delete'));
|
}, 'logo-delete'));
|
||||||
|
|
||||||
// ===== FAVICON ENDPOINTS =====
|
// ===== FAVICON ENDPOINTS =====
|
||||||
@@ -207,8 +202,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
// Get current favicon
|
// Get current favicon
|
||||||
router.get('/favicon', asyncHandler(async (req, res) => {
|
router.get('/favicon', asyncHandler(async (req, res) => {
|
||||||
const config = await ctx.readConfig();
|
const config = await ctx.readConfig();
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
customFavicon: config.customFavicon || null,
|
customFavicon: config.customFavicon || null,
|
||||||
isDefault: !config.customFavicon
|
isDefault: !config.customFavicon
|
||||||
});
|
});
|
||||||
@@ -268,8 +262,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
// Update config
|
// Update config
|
||||||
await ctx.saveConfig({ customFavicon: '/assets/favicon.ico', updatedAt: new Date().toISOString() });
|
await ctx.saveConfig({ customFavicon: '/assets/favicon.ico', updatedAt: new Date().toISOString() });
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
path: '/assets/favicon.ico',
|
path: '/assets/favicon.ico',
|
||||||
message: 'Favicon created successfully'
|
message: 'Favicon created successfully'
|
||||||
});
|
});
|
||||||
@@ -293,10 +286,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
|||||||
config.updatedAt = new Date().toISOString();
|
config.updatedAt = new Date().toISOString();
|
||||||
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
await fsp.writeFile(ctx.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
||||||
|
|
||||||
res.json({
|
successMessage(res, 'Favicon reset to default');
|
||||||
success: true,
|
|
||||||
message: 'Favicon reset to default'
|
|
||||||
});
|
|
||||||
}, 'favicon-delete'));
|
}, 'favicon-delete'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const { CADDY } = require('../../constants');
|
|||||||
const { exists } = require('../../fs-helpers');
|
const { exists } = require('../../fs-helpers');
|
||||||
const { ValidationError, AuthenticationError } = require('../../errors');
|
const { ValidationError, AuthenticationError } = require('../../errors');
|
||||||
const platformPaths = require('../../platform-paths');
|
const platformPaths = require('../../platform-paths');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config backup routes factory
|
* Config backup routes factory
|
||||||
@@ -210,7 +211,7 @@ module.exports = function(deps) {
|
|||||||
preview.browserStateCount = Object.keys(backup.browserState).length;
|
preview.browserStateCount = Object.keys(backup.browserState).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, preview });
|
ok(res, { preview });
|
||||||
}, 'backup-preview'));
|
}, 'backup-preview'));
|
||||||
|
|
||||||
// Restore configuration from backup
|
// Restore configuration from backup
|
||||||
@@ -391,13 +392,17 @@ module.exports = function(deps) {
|
|||||||
|
|
||||||
const success = results.restored.length > 0 && results.errors.length === 0;
|
const success = results.restored.length > 0 && results.errors.length === 0;
|
||||||
|
|
||||||
res.json({
|
if (success) {
|
||||||
success,
|
ok(res, {
|
||||||
message: success
|
message: `Restored ${results.restored.length} file(s) successfully`,
|
||||||
? `Restored ${results.restored.length} file(s) successfully`
|
|
||||||
: `Restore completed with ${results.errors.length} error(s)`,
|
|
||||||
results
|
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 });
|
log.info('backup', 'Backup restore completed', { restored: results.restored.length, errors: results.errors.length });
|
||||||
}, 'backup-restore'));
|
}, 'backup-restore'));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const fsp = require('fs').promises;
|
|||||||
const { validateConfig } = require('../../config-schema');
|
const { validateConfig } = require('../../config-schema');
|
||||||
const { exists } = require('../../fs-helpers');
|
const { exists } = require('../../fs-helpers');
|
||||||
const { ValidationError } = require('../../errors');
|
const { ValidationError } = require('../../errors');
|
||||||
|
const { ok, successMessage } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config settings routes factory
|
* Config settings routes factory
|
||||||
@@ -71,14 +72,14 @@ module.exports = function({ configStateManager: _configStateManager, asyncHandle
|
|||||||
}
|
}
|
||||||
log.info('config', 'Config saved', { path: ctx.CONFIG_FILE });
|
log.info('config', 'Config saved', { path: ctx.CONFIG_FILE });
|
||||||
|
|
||||||
res.json({ success: true, message: 'Configuration saved', config, warnings });
|
ok(res, { message: 'Configuration saved', config, warnings });
|
||||||
}, 'config-save'));
|
}, 'config-save'));
|
||||||
|
|
||||||
router.delete('/config', asyncHandler(async (req, res) => {
|
router.delete('/config', asyncHandler(async (req, res) => {
|
||||||
if (await exists(ctx.CONFIG_FILE)) {
|
if (await exists(ctx.CONFIG_FILE)) {
|
||||||
await fsp.unlink(ctx.CONFIG_FILE);
|
await fsp.unlink(ctx.CONFIG_FILE);
|
||||||
}
|
}
|
||||||
res.json({ success: true, message: 'Configuration reset' });
|
successMessage(res, 'Configuration reset');
|
||||||
}, 'config-delete'));
|
}, 'config-delete'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -383,9 +383,8 @@ module.exports = function({
|
|||||||
|
|
||||||
const response = await fetchT(technitiumUrl, {
|
const response = await fetchT(technitiumUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Accept': 'text/plain' },
|
headers: { 'Accept': 'text/plain' }
|
||||||
timeout: 10000
|
}, 10000);
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
@@ -553,7 +552,7 @@ module.exports = function({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return ok(res, {
|
||||||
success: anySuccess,
|
success: anySuccess,
|
||||||
message: anySuccess ? 'Credentials saved for one or more servers' : 'All server credential tests failed',
|
message: anySuccess ? 'Credentials saved for one or more servers' : 'All server credential tests failed',
|
||||||
results
|
results
|
||||||
@@ -640,7 +639,7 @@ module.exports = function({
|
|||||||
const dnsPort = siteConfig.dnsServerPort || '5380';
|
const dnsPort = siteConfig.dnsServerPort || '5380';
|
||||||
try {
|
try {
|
||||||
const url = `http://${serverInfo.ip}:${dnsPort}/api/admin/restart?token=${encodeURIComponent(tokenResult.token)}`;
|
const url = `http://${serverInfo.ip}:${dnsPort}/api/admin/restart?token=${encodeURIComponent(tokenResult.token)}`;
|
||||||
const response = await fetchT(url, { method: 'POST', timeout: 5000 });
|
const response = await fetchT(url, { method: 'POST' }, 5000);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.status === 'ok') {
|
if (result.status === 'ok') {
|
||||||
success(res, { message: 'Restart initiated' });
|
success(res, { message: 'Restart initiated' });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { ok } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server-Sent Events route factory
|
* Server-Sent Events route factory
|
||||||
@@ -147,7 +148,7 @@ module.exports = function({ resourceMonitor, healthChecker, updateManager, logEr
|
|||||||
|
|
||||||
// Client count (useful for debugging)
|
// Client count (useful for debugging)
|
||||||
router.get('/clients', (req, res) => {
|
router.get('/clients', (req, res) => {
|
||||||
res.json({ success: true, count: clients.size });
|
ok(res, { count: clients.size });
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { exists } = require('../fs-helpers');
|
|||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const platformPaths = require('../platform-paths');
|
const platformPaths = require('../platform-paths');
|
||||||
const { resolveServiceUrl } = require('../url-resolver');
|
const { resolveServiceUrl } = require('../url-resolver');
|
||||||
const { success, error: errorResponse } = require('../src/utils/responses');
|
const { success, error: errorResponse, errorResponse: sendError, ok, notFound } = require('../src/utils/responses');
|
||||||
const { ValidationError } = require('../errors');
|
const { ValidationError } = require('../errors');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,11 +273,7 @@ module.exports = function({
|
|||||||
try {
|
try {
|
||||||
// Check if certificate exists
|
// Check if certificate exists
|
||||||
if (!await exists(rootCertPath)) {
|
if (!await exists(rootCertPath)) {
|
||||||
return res.json({
|
return sendError(res, 404, 'Root CA certificate not found', { caStatus: 'error', daysUntilExpiration: null });
|
||||||
status: 'error',
|
|
||||||
message: 'Root CA certificate not found',
|
|
||||||
daysUntilExpiration: null
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dates = execSync(`openssl x509 -in "${rootCertPath}" -noout -dates`).toString();
|
const dates = execSync(`openssl x509 -in "${rootCertPath}" -noout -dates`).toString();
|
||||||
@@ -286,36 +282,32 @@ module.exports = function({
|
|||||||
const daysUntilExpiration = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
|
const daysUntilExpiration = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// Alert thresholds
|
// Alert thresholds
|
||||||
let status = 'healthy';
|
let caStatus = 'healthy';
|
||||||
let message = `CA certificate valid for ${daysUntilExpiration} days`;
|
let message = `CA certificate valid for ${daysUntilExpiration} days`;
|
||||||
|
|
||||||
if (daysUntilExpiration < 0) {
|
if (daysUntilExpiration < 0) {
|
||||||
status = 'critical';
|
caStatus = 'critical';
|
||||||
message = `CA certificate EXPIRED ${Math.abs(daysUntilExpiration)} days ago!`;
|
message = `CA certificate EXPIRED ${Math.abs(daysUntilExpiration)} days ago!`;
|
||||||
} else if (daysUntilExpiration < 7) {
|
} else if (daysUntilExpiration < 7) {
|
||||||
status = 'critical';
|
caStatus = 'critical';
|
||||||
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
||||||
} else if (daysUntilExpiration < 30) {
|
} else if (daysUntilExpiration < 30) {
|
||||||
status = 'critical';
|
caStatus = 'critical';
|
||||||
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
||||||
} else if (daysUntilExpiration < 90) {
|
} else if (daysUntilExpiration < 90) {
|
||||||
status = 'warning';
|
caStatus = 'warning';
|
||||||
message = `CA certificate expires in ${daysUntilExpiration} days`;
|
message = `CA certificate expires in ${daysUntilExpiration} days`;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
status: status,
|
caStatus,
|
||||||
message: message,
|
message,
|
||||||
daysUntilExpiration: daysUntilExpiration,
|
daysUntilExpiration,
|
||||||
expiresAt: notAfter
|
expiresAt: notAfter
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await logError('GET /api/health/ca', error);
|
await logError('GET /api/health/ca', error);
|
||||||
res.json({
|
sendError(res, 500, error.message, { caStatus: 'error', daysUntilExpiration: null });
|
||||||
status: 'error',
|
|
||||||
message: error.message,
|
|
||||||
daysUntilExpiration: null
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, 'health-ca'));
|
}, 'health-ca'));
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const path = require('path');
|
|||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const { NotFoundError, ValidationError, ForbiddenError } = require('../errors');
|
const { NotFoundError, ValidationError, ForbiddenError } = require('../errors');
|
||||||
|
const { ok } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs route factory
|
* Logs route factory
|
||||||
@@ -31,7 +32,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance }
|
|||||||
|
|
||||||
const paginationParams = parsePaginationParams(req.query);
|
const paginationParams = parsePaginationParams(req.query);
|
||||||
const result = paginate(containerList, paginationParams);
|
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'));
|
}, 'logs-containers'));
|
||||||
|
|
||||||
// Get logs for a specific container
|
// Get logs for a specific container
|
||||||
@@ -81,8 +82,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance }
|
|||||||
offset += 8 + size;
|
offset += 8 + size;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
containerId, containerName,
|
containerId, containerName,
|
||||||
logs: lines,
|
logs: lines,
|
||||||
count: lines.length
|
count: lines.length
|
||||||
@@ -153,23 +153,23 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance }
|
|||||||
if (!logDigest) throw new Error('Log digest not available');
|
if (!logDigest) throw new Error('Log digest not available');
|
||||||
const digest = await logDigest.getLatestDigest();
|
const digest = await logDigest.getLatestDigest();
|
||||||
if (!digest) {
|
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'));
|
}, 'logs-digest-latest'));
|
||||||
|
|
||||||
// Get live digest data (today's accumulated stats)
|
// Get live digest data (today's accumulated stats)
|
||||||
router.get('/logs/digest/live', asyncHandler(async (req, res) => {
|
router.get('/logs/digest/live', asyncHandler(async (req, res) => {
|
||||||
if (!logDigest) throw new Error('Log digest not available');
|
if (!logDigest) throw new Error('Log digest not available');
|
||||||
const live = logDigest.getLiveData();
|
const live = logDigest.getLiveData();
|
||||||
res.json({ success: true, ...live });
|
ok(res, { ...live });
|
||||||
}, 'logs-digest-live'));
|
}, 'logs-digest-live'));
|
||||||
|
|
||||||
// List available digest dates
|
// List available digest dates
|
||||||
router.get('/logs/digest/history', asyncHandler(async (req, res) => {
|
router.get('/logs/digest/history', asyncHandler(async (req, res) => {
|
||||||
if (!logDigest) throw new Error('Log digest not available');
|
if (!logDigest) throw new Error('Log digest not available');
|
||||||
const dates = await logDigest.listDigests();
|
const dates = await logDigest.listDigests();
|
||||||
res.json({ success: true, dates });
|
ok(res, { dates });
|
||||||
}, 'logs-digest-history'));
|
}, 'logs-digest-history'));
|
||||||
|
|
||||||
// Generate digest on demand (for today or a specific date)
|
// 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');
|
if (!logDigest) throw new Error('Log digest not available');
|
||||||
const date = req.body.date || new Date().toISOString().slice(0, 10);
|
const date = req.body.date || new Date().toISOString().slice(0, 10);
|
||||||
const digest = await logDigest.generateDailyDigest(date);
|
const digest = await logDigest.generateDailyDigest(date);
|
||||||
res.json({ success: true, digest });
|
ok(res, { digest });
|
||||||
}, 'logs-digest-generate'));
|
}, 'logs-digest-generate'));
|
||||||
|
|
||||||
// Get digest for a specific date (JSON)
|
// Get digest for a specific date (JSON)
|
||||||
@@ -196,7 +196,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance }
|
|||||||
}
|
}
|
||||||
const digest = await logDigest.getDigestByDate(date);
|
const digest = await logDigest.getDigestByDate(date);
|
||||||
if (!digest) throw new NotFoundError(`Digest for ${date}`);
|
if (!digest) throw new NotFoundError(`Digest for ${date}`);
|
||||||
res.json({ success: true, digest });
|
ok(res, { digest });
|
||||||
}, 'logs-digest-date'));
|
}, 'logs-digest-date'));
|
||||||
|
|
||||||
// Get Docker disk usage snapshot
|
// 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');
|
if (!dockerMaintenance) throw new Error('Docker maintenance not available');
|
||||||
const diskUsage = await dockerMaintenance.getDiskUsage();
|
const diskUsage = await dockerMaintenance.getDiskUsage();
|
||||||
const status = dockerMaintenance.getStatus();
|
const status = dockerMaintenance.getStatus();
|
||||||
res.json({ success: true, diskUsage, maintenance: status });
|
ok(res, { diskUsage, maintenance: status });
|
||||||
}, 'logs-docker-disk'));
|
}, 'logs-docker-disk'));
|
||||||
|
|
||||||
// Trigger Docker maintenance manually
|
// Trigger Docker maintenance manually
|
||||||
router.post('/logs/docker-maintenance', asyncHandler(async (req, res) => {
|
router.post('/logs/docker-maintenance', asyncHandler(async (req, res) => {
|
||||||
if (!dockerMaintenance) throw new Error('Docker maintenance not available');
|
if (!dockerMaintenance) throw new Error('Docker maintenance not available');
|
||||||
const result = await dockerMaintenance.runMaintenance();
|
const result = await dockerMaintenance.runMaintenance();
|
||||||
res.json({ success: true, result });
|
ok(res, { result });
|
||||||
}, 'logs-docker-maintenance'));
|
}, 'logs-docker-maintenance'));
|
||||||
|
|
||||||
// Get logs from a file path (for native applications)
|
// Get logs from a file path (for native applications)
|
||||||
@@ -261,8 +261,7 @@ module.exports = function({ asyncHandler, docker, logDigest, dockerMaintenance }
|
|||||||
timestamp: extractTimestamp(line)
|
timestamp: extractTimestamp(line)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
logPath: normalizedPath,
|
logPath: normalizedPath,
|
||||||
logs,
|
logs,
|
||||||
count: logs.length,
|
count: logs.length,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { validateURL, validateToken } = require('../input-validator');
|
|||||||
const validatorLib = require('validator');
|
const validatorLib = require('validator');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const { ValidationError } = require('../errors');
|
const { ValidationError } = require('../errors');
|
||||||
|
const { ok, successMessage } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications route factory
|
* Notifications route factory
|
||||||
@@ -44,7 +45,7 @@ module.exports = function({ notification, asyncHandler }) {
|
|||||||
events: notificationConfig.events,
|
events: notificationConfig.events,
|
||||||
healthCheck: notificationConfig.healthCheck
|
healthCheck: notificationConfig.healthCheck
|
||||||
};
|
};
|
||||||
res.json({ success: true, config: safeConfig });
|
ok(res, { config: safeConfig });
|
||||||
}, 'notifications-config-get'));
|
}, 'notifications-config-get'));
|
||||||
|
|
||||||
// POST /config — Update notification configuration
|
// POST /config — Update notification configuration
|
||||||
@@ -150,7 +151,7 @@ module.exports = function({ notification, asyncHandler }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await notification.saveConfig();
|
await notification.saveConfig();
|
||||||
res.json({ success: true, message: 'Notification config updated' });
|
successMessage(res, 'Notification config updated');
|
||||||
}, 'notifications-config-update'));
|
}, 'notifications-config-update'));
|
||||||
|
|
||||||
// POST /test — Test notification delivery
|
// POST /test — Test notification delivery
|
||||||
@@ -176,11 +177,11 @@ module.exports = function({ notification, asyncHandler }) {
|
|||||||
default:
|
default:
|
||||||
throw new ValidationError('Unknown provider');
|
throw new ValidationError('Unknown provider');
|
||||||
}
|
}
|
||||||
res.json({ success: result.success, provider, error: result.error });
|
ok(res, { success: result.success, provider, error: result.error });
|
||||||
} else {
|
} else {
|
||||||
// Test all enabled providers
|
// Test all enabled providers
|
||||||
const result = await notification.send('test', 'Test Notification', 'This is a test notification from DashCaddy.', 'info');
|
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'));
|
}, 'notifications-test'));
|
||||||
|
|
||||||
@@ -190,11 +191,10 @@ module.exports = function({ notification, asyncHandler }) {
|
|||||||
const paginationParams = parsePaginationParams(req.query);
|
const paginationParams = parsePaginationParams(req.query);
|
||||||
if (paginationParams) {
|
if (paginationParams) {
|
||||||
const result = paginate(notificationHistory, 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 {
|
} else {
|
||||||
const limit = parseInt(req.query.limit) || 50;
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
history: notificationHistory.slice(0, limit),
|
history: notificationHistory.slice(0, limit),
|
||||||
total: notificationHistory.length
|
total: notificationHistory.length
|
||||||
});
|
});
|
||||||
@@ -204,15 +204,14 @@ module.exports = function({ notification, asyncHandler }) {
|
|||||||
// DELETE /history — Clear notification history
|
// DELETE /history — Clear notification history
|
||||||
router.delete('/history', asyncHandler(async (req, res) => {
|
router.delete('/history', asyncHandler(async (req, res) => {
|
||||||
notification.clearHistory();
|
notification.clearHistory();
|
||||||
res.json({ success: true, message: 'Notification history cleared' });
|
successMessage(res, 'Notification history cleared');
|
||||||
}, 'notifications-history-clear'));
|
}, 'notifications-history-clear'));
|
||||||
|
|
||||||
// POST /health-check — Manually trigger health check
|
// POST /health-check — Manually trigger health check
|
||||||
router.post('/health-check', asyncHandler(async (req, res) => {
|
router.post('/health-check', asyncHandler(async (req, res) => {
|
||||||
await notification.checkHealth();
|
await notification.checkHealth();
|
||||||
const notificationConfig = notification.getConfig();
|
const notificationConfig = notification.getConfig();
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
lastCheck: notificationConfig.healthCheck.lastCheck,
|
lastCheck: notificationConfig.healthCheck.lastCheck,
|
||||||
containersMonitored: Object.keys(notification.getHealthState()).length
|
containersMonitored: Object.keys(notification.getHealthState()).length
|
||||||
});
|
});
|
||||||
@@ -223,8 +222,7 @@ module.exports = function({ notification, asyncHandler }) {
|
|||||||
const notificationConfig = notification.getConfig();
|
const notificationConfig = notification.getConfig();
|
||||||
const providers = notificationConfig.providers || {};
|
const providers = notificationConfig.providers || {};
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
enabled: notificationConfig.enabled,
|
enabled: notificationConfig.enabled,
|
||||||
providers: {
|
providers: {
|
||||||
discord: providers.discord?.enabled && !!providers.discord?.webhookUrl,
|
discord: providers.discord?.enabled && !!providers.discord?.webhookUrl,
|
||||||
@@ -252,7 +250,7 @@ module.exports = function({ notification, asyncHandler }) {
|
|||||||
// Use 'test' as the event for manual sends
|
// Use 'test' as the event for manual sends
|
||||||
const result = await notification.send(event, data || {}, type || 'info');
|
const result = await notification.send(event, data || {}, type || 'info');
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
event,
|
event,
|
||||||
results: result.results
|
results: result.results
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const { ok, errorResponse, notFound, conflict } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenClaw management routes
|
* OpenClaw management routes
|
||||||
@@ -93,8 +94,8 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
proxyRes.on('data', function(d) { res.write(d); });
|
proxyRes.on('data', function(d) { res.write(d); });
|
||||||
proxyRes.on('end', function() { res.end(); });
|
proxyRes.on('end', function() { res.end(); });
|
||||||
});
|
});
|
||||||
proxyReq.on('error', function(e) { res.status(502).json({ success: false, error: e.message }); });
|
proxyReq.on('error', function(e) { errorResponse(res, 502, e.message); });
|
||||||
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); res.status(504).json({ success: false, error: 'gateway timeout' }); });
|
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); errorResponse(res, 504, 'gateway timeout'); });
|
||||||
proxyReq.write(body);
|
proxyReq.write(body);
|
||||||
proxyReq.end();
|
proxyReq.end();
|
||||||
} else {
|
} else {
|
||||||
@@ -104,8 +105,8 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
proxyRes.on('data', function(d) { res.write(d); });
|
proxyRes.on('data', function(d) { res.write(d); });
|
||||||
proxyRes.on('end', function() { res.end(); });
|
proxyRes.on('end', function() { res.end(); });
|
||||||
});
|
});
|
||||||
proxyReq.on('error', function(e) { res.status(502).json({ success: false, error: e.message }); });
|
proxyReq.on('error', function(e) { errorResponse(res, 502, e.message); });
|
||||||
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); res.status(504).json({ success: false, error: 'gateway timeout' }); });
|
proxyReq.setTimeout(15000, function() { proxyReq.destroy(); errorResponse(res, 504, 'gateway timeout'); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
const container = await findOpenClawContainer();
|
const container = await findOpenClawContainer();
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return res.json({ success: true, deployed: false });
|
return ok(res, { deployed: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await getGatewayToken(container.Id);
|
const token = await getGatewayToken(container.Id);
|
||||||
@@ -123,8 +124,7 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
const baseUrl = 'http://localhost:' + port;
|
const baseUrl = 'http://localhost:' + port;
|
||||||
const health = await gatewayHealth(baseUrl, token);
|
const health = await gatewayHealth(baseUrl, token);
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
deployed: true,
|
deployed: true,
|
||||||
container: {
|
container: {
|
||||||
id: container.Id.slice(0, 12),
|
id: container.Id.slice(0, 12),
|
||||||
@@ -149,7 +149,7 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
router.post('/deploy', asyncHandler(async function(req, res) {
|
router.post('/deploy', asyncHandler(async function(req, res) {
|
||||||
const existing = await findOpenClawContainer();
|
const existing = await findOpenClawContainer();
|
||||||
if (existing) {
|
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';
|
const image = 'ghcr.io/nousresearch/openclaw:latest';
|
||||||
@@ -170,7 +170,7 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
});
|
});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
log.error('OpenClaw pull failed: ' + e.message);
|
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
|
// Create + start container
|
||||||
@@ -196,8 +196,7 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
await container.start();
|
await container.start();
|
||||||
log.info('OpenClaw deployed: ' + container.id.slice(0, 12));
|
log.info('OpenClaw deployed: ' + container.id.slice(0, 12));
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
deployed: true,
|
deployed: true,
|
||||||
container: { id: container.id.slice(0, 12), name: name },
|
container: { id: container.id.slice(0, 12), name: name },
|
||||||
gateway: {
|
gateway: {
|
||||||
@@ -207,7 +206,7 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
});
|
});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
log.error('OpenClaw deploy failed: ' + e.message);
|
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) {
|
router.get('/proxy/*', asyncHandler(async function(req, res) {
|
||||||
const container = await findOpenClawContainer();
|
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 token = await getGatewayToken(container.Id);
|
||||||
const port = await getContainerPort(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) {
|
router.post('/proxy/*', asyncHandler(async function(req, res) {
|
||||||
const container = await findOpenClawContainer();
|
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 token = await getGatewayToken(container.Id);
|
||||||
const port = await getContainerPort(container.Id);
|
const port = await getContainerPort(container.Id);
|
||||||
@@ -243,17 +242,17 @@ module.exports = function openClawRoutes(ctx) {
|
|||||||
|
|
||||||
router.delete('/', asyncHandler(async function(req, res) {
|
router.delete('/', asyncHandler(async function(req, res) {
|
||||||
const container = await findOpenClawContainer();
|
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 {
|
try {
|
||||||
const c = docker.client.container(container.Id);
|
const c = docker.client.container(container.Id);
|
||||||
await c.stop().catch(function() {});
|
await c.stop().catch(function() {});
|
||||||
await c.remove({ force: true });
|
await c.remove({ force: true });
|
||||||
log.info('OpenClaw container ' + container.Id.slice(0, 12) + ' removed');
|
log.info('OpenClaw container ' + container.Id.slice(0, 12) + ' removed');
|
||||||
res.json({ success: true, message: 'OpenClaw removed' });
|
ok(res, { message: 'OpenClaw removed' });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
log.error('Failed to remove OpenClaw: ' + e.message);
|
log.error('Failed to remove OpenClaw: ' + e.message);
|
||||||
res.status(500).json({ success: false, error: e.message });
|
errorResponse(res, 500, e.message);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const { ValidationError } = require('../../errors');
|
const { ValidationError } = require('../../errors');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { DOCKER } = require('../../constants');
|
const { DOCKER } = require('../../constants');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recipes deployment routes factory
|
* Recipes deployment routes factory
|
||||||
@@ -146,7 +147,7 @@ module.exports = function({ docker, credentialManager: _credentialManager, servi
|
|||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json(response);
|
ok(res, response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message });
|
log.error('recipe', 'Recipe deployment failed', { recipeId, error: error.message });
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const deployRoutes = require('./deploy');
|
const deployRoutes = require('./deploy');
|
||||||
const manageRoutes = require('./manage');
|
const manageRoutes = require('./manage');
|
||||||
const { NotFoundError } = require('../../errors');
|
const { NotFoundError } = require('../../errors');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recipes routes aggregator
|
* Recipes routes aggregator
|
||||||
@@ -55,7 +56,7 @@ module.exports = function(ctx) {
|
|||||||
setupInstructions: recipe.setupInstructions
|
setupInstructions: recipe.setupInstructions
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({ success: true, templates, categories: RECIPE_CATEGORIES });
|
ok(res, { templates, categories: RECIPE_CATEGORIES });
|
||||||
}, 'recipe-templates'));
|
}, 'recipe-templates'));
|
||||||
|
|
||||||
// GET /api/recipes/templates/:recipeId — get single recipe template detail
|
// 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];
|
const recipe = RECIPE_TEMPLATES[req.params.recipeId];
|
||||||
if (!recipe) throw new NotFoundError(`Recipe template ${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'));
|
}, 'recipe-template-detail'));
|
||||||
|
|
||||||
// Mount deploy and manage sub-routes — pass full ctx for sub-routes that reference ctx.*
|
// Mount deploy and manage sub-routes — pass full ctx for sub-routes that reference ctx.*
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { DOCKER } = require('../../constants');
|
const { DOCKER } = require('../../constants');
|
||||||
const { NotFoundError } = require('../../errors');
|
const { NotFoundError } = require('../../errors');
|
||||||
|
const { ok } = require('../../src/utils/responses');
|
||||||
|
|
||||||
module.exports = function({ servicesStateManager, asyncHandler, log, docker, notification, buildDomain, caddy }) {
|
module.exports = function({ servicesStateManager, asyncHandler, log, docker, notification, buildDomain, caddy }) {
|
||||||
const router = express.Router();
|
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'));
|
}, 'recipe-deployed'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,7 +130,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info('recipe', 'Recipe started', { recipeId, results });
|
log.info('recipe', 'Recipe started', { recipeId, results });
|
||||||
res.json({ success: true, recipeId, results });
|
ok(res, { recipeId, results });
|
||||||
}, 'recipe-start'));
|
}, 'recipe-start'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,7 +162,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info('recipe', 'Recipe stopped', { recipeId, results });
|
log.info('recipe', 'Recipe stopped', { recipeId, results });
|
||||||
res.json({ success: true, recipeId, results });
|
ok(res, { recipeId, results });
|
||||||
}, 'recipe-stop'));
|
}, 'recipe-stop'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,7 +188,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info('recipe', 'Recipe restarted', { recipeId, results });
|
log.info('recipe', 'Recipe restarted', { recipeId, results });
|
||||||
res.json({ success: true, recipeId, results });
|
ok(res, { recipeId, results });
|
||||||
}, 'recipe-restart'));
|
}, 'recipe-restart'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,7 +260,7 @@ module.exports = function({ servicesStateManager, asyncHandler, log, docker, not
|
|||||||
);
|
);
|
||||||
|
|
||||||
log.info('recipe', 'Recipe removed', { recipeId, results });
|
log.info('recipe', 'Recipe removed', { recipeId, results });
|
||||||
res.json({ success: true, recipeId, results });
|
ok(res, { recipeId, results });
|
||||||
}, 'recipe-remove'));
|
}, 'recipe-remove'));
|
||||||
|
|
||||||
// === Helper functions ===
|
// === Helper functions ===
|
||||||
|
|||||||
@@ -356,9 +356,11 @@ module.exports = function({
|
|||||||
}, 'services-status'));
|
}, 'services-status'));
|
||||||
|
|
||||||
// List all services
|
// 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) => {
|
router.get('/services', asyncHandler(async (req, res) => {
|
||||||
if (!await exists(SERVICES_FILE)) {
|
if (!await exists(SERVICES_FILE)) {
|
||||||
return res.json([]);
|
return success(res, { services: [] });
|
||||||
}
|
}
|
||||||
const services = await servicesStateManager.read();
|
const services = await servicesStateManager.read();
|
||||||
const paginationParams = parsePaginationParams(req.query);
|
const paginationParams = parsePaginationParams(req.query);
|
||||||
@@ -366,7 +368,7 @@ module.exports = function({
|
|||||||
if (paginationParams) {
|
if (paginationParams) {
|
||||||
success(res, { services: result.data, pagination: result.pagination });
|
success(res, { services: result.data, pagination: result.pagination });
|
||||||
} else {
|
} else {
|
||||||
res.json(result.data);
|
success(res, { services: result.data });
|
||||||
}
|
}
|
||||||
}, 'services-list'));
|
}, 'services-list'));
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|||||||
const { CADDY, REGEX, LIMITS } = require('../constants');
|
const { CADDY, REGEX, LIMITS } = require('../constants');
|
||||||
const { ValidationError, ConflictError, NotFoundError } = require('../errors');
|
const { ValidationError, ConflictError, NotFoundError } = require('../errors');
|
||||||
const { validateURL } = require('../input-validator');
|
const { validateURL } = require('../input-validator');
|
||||||
|
const { ok, successMessage } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sites route factory
|
* Sites route factory
|
||||||
@@ -23,14 +24,14 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
// Get Caddyfile contents
|
// Get Caddyfile contents
|
||||||
router.get('/caddyfile', asyncHandler(async (req, res) => {
|
router.get('/caddyfile', asyncHandler(async (req, res) => {
|
||||||
const content = await caddy.read();
|
const content = await caddy.read();
|
||||||
res.json({ success: true, content });
|
ok(res, { content });
|
||||||
}, 'caddyfile-get'));
|
}, 'caddyfile-get'));
|
||||||
|
|
||||||
// Get current Caddy config (from admin API)
|
// Get current Caddy config (from admin API)
|
||||||
router.get('/caddy/config', asyncHandler(async (req, res) => {
|
router.get('/caddy/config', asyncHandler(async (req, res) => {
|
||||||
const response = await fetchT(`${caddy.adminUrl}/config/`);
|
const response = await fetchT(`${caddy.adminUrl}/config/`);
|
||||||
const config = await response.json();
|
const config = await response.json();
|
||||||
res.json({ success: true, config });
|
ok(res, { config });
|
||||||
}, 'caddy-config'));
|
}, 'caddy-config'));
|
||||||
|
|
||||||
// Reload Caddy configuration via admin API
|
// Reload Caddy configuration via admin API
|
||||||
@@ -49,7 +50,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
throw new Error('Caddy reload failed. Check server logs for details.');
|
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'));
|
}, 'caddy-reload'));
|
||||||
|
|
||||||
// Get Certificate Authorities from Caddyfile
|
// Get Certificate Authorities from Caddyfile
|
||||||
@@ -127,7 +128,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
name: ca.name,
|
name: ca.name,
|
||||||
displayName: ca.name !== (ca.id || ca.name) ? `${ca.name} (${ca.id || ca.name})` : ca.name
|
displayName: ca.name !== (ca.id || ca.name) ? `${ca.name} (${ca.id || ca.name})` : ca.name
|
||||||
}));
|
}));
|
||||||
res.json({ status: 'success', data: { cas: caList } });
|
ok(res, { cas: caList });
|
||||||
}, 'caddy-get-cas'));
|
}, 'caddy-get-cas'));
|
||||||
|
|
||||||
// Remove a site from Caddyfile
|
// Remove a site from Caddyfile
|
||||||
@@ -152,7 +153,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
throw new NotFoundError(`Site block for "" in Caddyfile`);
|
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'));
|
}, 'site-delete'));
|
||||||
|
|
||||||
// Add a new site to Caddyfile and reload
|
// Add a new site to Caddyfile and reload
|
||||||
@@ -180,7 +181,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
result.rolledBack ? { note: 'Caddyfile was rolled back to previous state' } : {});
|
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'));
|
}, 'site-add'));
|
||||||
|
|
||||||
// Add external service reverse proxy to Caddyfile
|
// Add external service reverse proxy to Caddyfile
|
||||||
@@ -260,12 +261,11 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = {
|
const responseData = {
|
||||||
success: true,
|
|
||||||
message: `External service proxy for ${domain} -> ${externalUrl} created${shouldReload ? ' and Caddy reloaded' : ''}`
|
message: `External service proxy for ${domain} -> ${externalUrl} created${shouldReload ? ' and Caddy reloaded' : ''}`
|
||||||
};
|
};
|
||||||
if (dnsWarning) response.warning = dnsWarning;
|
if (dnsWarning) responseData.warning = dnsWarning;
|
||||||
res.json(response);
|
ok(res, responseData);
|
||||||
}, 'site-external'));
|
}, 'site-external'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|||||||
const { TAILSCALE } = require('../constants');
|
const { TAILSCALE } = require('../constants');
|
||||||
const { exists } = require('../fs-helpers');
|
const { exists } = require('../fs-helpers');
|
||||||
const { ValidationError, NotFoundError } = require('../errors');
|
const { ValidationError, NotFoundError } = require('../errors');
|
||||||
|
const { ok, successMessage, unauthorized } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tailscale route factory
|
* Tailscale route factory
|
||||||
@@ -35,8 +36,7 @@ module.exports = function({
|
|||||||
const localIP = await tailscale.getLocalIP();
|
const localIP = await tailscale.getLocalIP();
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return res.json({
|
return ok(res, {
|
||||||
success: true,
|
|
||||||
installed: false,
|
installed: false,
|
||||||
connected: false,
|
connected: false,
|
||||||
message: 'Tailscale not available or not running'
|
message: 'Tailscale not available or not running'
|
||||||
@@ -58,8 +58,7 @@ module.exports = function({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
installed: true,
|
installed: true,
|
||||||
connected: status.BackendState === 'Running',
|
connected: status.BackendState === 'Running',
|
||||||
backendState: status.BackendState,
|
backendState: status.BackendState,
|
||||||
@@ -85,8 +84,7 @@ module.exports = function({
|
|||||||
|
|
||||||
await tailscale.save();
|
await tailscale.save();
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
message: 'Tailscale configuration updated',
|
message: 'Tailscale configuration updated',
|
||||||
config: tailscale.config
|
config: tailscale.config
|
||||||
});
|
});
|
||||||
@@ -101,8 +99,7 @@ module.exports = function({
|
|||||||
const ipsToCheck = [clientIP, forwardedFor, realIP].filter(Boolean);
|
const ipsToCheck = [clientIP, forwardedFor, realIP].filter(Boolean);
|
||||||
const isTailscale = ipsToCheck.some(ip => tailscale.isTailscaleIP(ip.toString().split(',')[0].trim()));
|
const isTailscale = ipsToCheck.some(ip => tailscale.isTailscaleIP(ip.toString().split(',')[0].trim()));
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
isTailscale,
|
isTailscale,
|
||||||
clientIP,
|
clientIP,
|
||||||
forwardedFor: forwardedFor || null,
|
forwardedFor: forwardedFor || null,
|
||||||
@@ -114,7 +111,7 @@ module.exports = function({
|
|||||||
router.get('/devices', asyncHandler(async (req, res) => {
|
router.get('/devices', asyncHandler(async (req, res) => {
|
||||||
const status = await tailscale.getStatus();
|
const status = await tailscale.getStatus();
|
||||||
if (!status || !status.Peer) {
|
if (!status || !status.Peer) {
|
||||||
return res.json({ success: true, devices: [] });
|
return ok(res, { devices: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = [];
|
const devices = [];
|
||||||
@@ -141,7 +138,7 @@ module.exports = function({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, devices });
|
ok(res, { devices });
|
||||||
}, 'tailscale-devices'));
|
}, 'tailscale-devices'));
|
||||||
|
|
||||||
// Toggle Tailscale-only mode for an existing service
|
// Toggle Tailscale-only mode for an existing service
|
||||||
@@ -190,8 +187,7 @@ module.exports = function({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
message: `Service ${domain} is now ${tailscaleOnly !== false ? 'protected by' : 'no longer restricted to'} Tailscale`,
|
message: `Service ${domain} is now ${tailscaleOnly !== false ? 'protected by' : 'no longer restricted to'} Tailscale`,
|
||||||
tailscaleOnly: tailscaleOnly !== false
|
tailscaleOnly: tailscaleOnly !== false
|
||||||
});
|
});
|
||||||
@@ -254,7 +250,7 @@ module.exports = function({
|
|||||||
log.warn('tailscale', 'Initial sync after OAuth config failed', { error: e.message });
|
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'));
|
}, 'tailscale-oauth-config'));
|
||||||
|
|
||||||
// Remove OAuth credentials and disable API sync
|
// Remove OAuth credentials and disable API sync
|
||||||
@@ -269,7 +265,7 @@ module.exports = function({
|
|||||||
|
|
||||||
tailscale.stopSync();
|
tailscale.stopSync();
|
||||||
|
|
||||||
res.json({ success: true, message: 'Tailscale OAuth credentials removed' });
|
successMessage(res, 'Tailscale OAuth credentials removed');
|
||||||
}, 'tailscale-oauth-delete'));
|
}, 'tailscale-oauth-delete'));
|
||||||
|
|
||||||
// Get enriched device list from Tailscale API
|
// Get enriched device list from Tailscale API
|
||||||
@@ -279,8 +275,7 @@ module.exports = function({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return cached devices from last sync
|
// Return cached devices from last sync
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
devices: tailscale.config.devices || [],
|
devices: tailscale.config.devices || [],
|
||||||
lastSync: tailscale.config.lastSync
|
lastSync: tailscale.config.lastSync
|
||||||
});
|
});
|
||||||
@@ -294,8 +289,7 @@ module.exports = function({
|
|||||||
|
|
||||||
const devices = await tailscale.syncAPI();
|
const devices = await tailscale.syncAPI();
|
||||||
|
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
devices: devices || [],
|
devices: devices || [],
|
||||||
lastSync: tailscale.config.lastSync
|
lastSync: tailscale.config.lastSync
|
||||||
});
|
});
|
||||||
@@ -325,7 +319,7 @@ module.exports = function({
|
|||||||
sshRuleCount: (acl.ssh || []).length
|
sshRuleCount: (acl.ssh || []).length
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json({ success: true, acl, summary });
|
ok(res, { acl, summary });
|
||||||
}, 'tailscale-acl'));
|
}, 'tailscale-acl'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const { ValidationError } = require('../errors');
|
const { ValidationError } = require('../errors');
|
||||||
|
const { ok, successMessage } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates route factory
|
* Updates route factory
|
||||||
@@ -20,7 +21,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
|
|||||||
router.post('/updates/check', asyncHandler(async (req, res) => {
|
router.post('/updates/check', asyncHandler(async (req, res) => {
|
||||||
await updateManager.checkForUpdates();
|
await updateManager.checkForUpdates();
|
||||||
const updates = updateManager.getAvailableUpdates();
|
const updates = updateManager.getAvailableUpdates();
|
||||||
res.json({ success: true, updates, count: updates.length });
|
ok(res, { updates, count: updates.length });
|
||||||
}, 'updates-check'));
|
}, 'updates-check'));
|
||||||
|
|
||||||
// Get available updates
|
// Get available updates
|
||||||
@@ -28,19 +29,19 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
|
|||||||
const updates = updateManager.getAvailableUpdates();
|
const updates = updateManager.getAvailableUpdates();
|
||||||
const paginationParams = parsePaginationParams(req.query);
|
const paginationParams = parsePaginationParams(req.query);
|
||||||
const result = paginate(updates, paginationParams);
|
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'));
|
}, 'updates-available'));
|
||||||
|
|
||||||
// Update a container
|
// Update a container
|
||||||
router.post('/updates/update/:containerId', asyncHandler(async (req, res) => {
|
router.post('/updates/update/:containerId', asyncHandler(async (req, res) => {
|
||||||
const result = await updateManager.updateContainer(req.params.containerId, req.body);
|
const result = await updateManager.updateContainer(req.params.containerId, req.body);
|
||||||
res.json({ success: true, result });
|
ok(res, { result });
|
||||||
}, 'updates-update'));
|
}, 'updates-update'));
|
||||||
|
|
||||||
// Rollback update
|
// Rollback update
|
||||||
router.post('/updates/rollback/:containerId', asyncHandler(async (req, res) => {
|
router.post('/updates/rollback/:containerId', asyncHandler(async (req, res) => {
|
||||||
await updateManager.rollbackUpdate(req.params.containerId);
|
await updateManager.rollbackUpdate(req.params.containerId);
|
||||||
res.json({ success: true, message: 'Rollback completed' });
|
successMessage(res, 'Rollback completed');
|
||||||
}, 'updates-rollback'));
|
}, 'updates-rollback'));
|
||||||
|
|
||||||
// Get update history
|
// 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 fetchLimit = paginationParams ? Number.MAX_SAFE_INTEGER : (parseInt(req.query.limit) || 50);
|
||||||
const history = updateManager.getHistory(fetchLimit);
|
const history = updateManager.getHistory(fetchLimit);
|
||||||
const result = paginate(history, paginationParams);
|
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'));
|
}, 'updates-history'));
|
||||||
|
|
||||||
// Configure auto-update
|
// Configure auto-update
|
||||||
router.post('/updates/auto-update/:containerId', asyncHandler(async (req, res) => {
|
router.post('/updates/auto-update/:containerId', asyncHandler(async (req, res) => {
|
||||||
updateManager.configureAutoUpdate(req.params.containerId, req.body);
|
updateManager.configureAutoUpdate(req.params.containerId, req.body);
|
||||||
res.json({ success: true, message: 'Auto-update configured' });
|
successMessage(res, 'Auto-update configured');
|
||||||
}, 'updates-auto-update'));
|
}, 'updates-auto-update'));
|
||||||
|
|
||||||
// Get auto-update configuration
|
// Get auto-update configuration
|
||||||
router.get('/updates/auto-update', asyncHandler(async (req, res) => {
|
router.get('/updates/auto-update', asyncHandler(async (req, res) => {
|
||||||
const config = updateManager.getAutoUpdateConfig();
|
const config = updateManager.getAutoUpdateConfig();
|
||||||
res.json({ success: true, config });
|
ok(res, { config });
|
||||||
}, 'updates-auto-update-config'));
|
}, 'updates-auto-update-config'));
|
||||||
|
|
||||||
// Schedule update
|
// Schedule update
|
||||||
@@ -72,7 +73,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
|
|||||||
throw new ValidationError('scheduledTime is required');
|
throw new ValidationError('scheduledTime is required');
|
||||||
}
|
}
|
||||||
updateManager.scheduleUpdate(req.params.containerId, scheduledTime);
|
updateManager.scheduleUpdate(req.params.containerId, scheduledTime);
|
||||||
res.json({ success: true, message: 'Update scheduled', scheduledTime });
|
ok(res, { message: 'Update scheduled', scheduledTime });
|
||||||
}, 'updates-schedule'));
|
}, 'updates-schedule'));
|
||||||
|
|
||||||
// ===== DASHCADDY SELF-UPDATE ENDPOINTS =====
|
// ===== DASHCADDY SELF-UPDATE ENDPOINTS =====
|
||||||
@@ -80,20 +81,20 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
|
|||||||
// Get current version
|
// Get current version
|
||||||
router.get('/system/version', asyncHandler(async (req, res) => {
|
router.get('/system/version', asyncHandler(async (req, res) => {
|
||||||
const local = selfUpdater.getLocalVersion();
|
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'));
|
}, 'system-version'));
|
||||||
|
|
||||||
// Check for DashCaddy update
|
// Check for DashCaddy update
|
||||||
router.get('/system/update-check', asyncHandler(async (req, res) => {
|
router.get('/system/update-check', asyncHandler(async (req, res) => {
|
||||||
const result = await selfUpdater.checkForUpdate();
|
const result = await selfUpdater.checkForUpdate();
|
||||||
res.json({ success: true, ...result });
|
ok(res, result);
|
||||||
}, 'system-update-check'));
|
}, 'system-update-check'));
|
||||||
|
|
||||||
// Apply available update
|
// Apply available update
|
||||||
router.post('/system/update-apply', asyncHandler(async (req, res) => {
|
router.post('/system/update-apply', asyncHandler(async (req, res) => {
|
||||||
const check = await selfUpdater.checkForUpdate();
|
const check = await selfUpdater.checkForUpdate();
|
||||||
if (!check.available) {
|
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
|
// Refuse same-version applies. The check.available flag can theoretically be
|
||||||
// true with equal versions (commit-mismatch path); applying anyway just
|
// 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 localV = check.local && check.local.version;
|
||||||
const remoteV = check.remote && check.remote.version;
|
const remoteV = check.remote && check.remote.version;
|
||||||
if (localV && remoteV && localV === remoteV) {
|
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
|
// Start async — container may restart
|
||||||
selfUpdater.applyUpdate(check.remote).catch(err => {
|
selfUpdater.applyUpdate(check.remote).catch(err => {
|
||||||
logError('self-update', err);
|
logError('self-update', err);
|
||||||
});
|
});
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
message: 'Update initiated',
|
message: 'Update initiated',
|
||||||
fromVersion: localV,
|
fromVersion: localV,
|
||||||
toVersion: remoteV,
|
toVersion: remoteV,
|
||||||
@@ -132,16 +132,15 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
|
|||||||
presentedBuf.length > 0 &&
|
presentedBuf.length > 0 &&
|
||||||
require('crypto').timingSafeEqual(presentedBuf, expectedBuf);
|
require('crypto').timingSafeEqual(presentedBuf, expectedBuf);
|
||||||
if (!ok) {
|
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');
|
const result = selfUpdater.notifyAndApply('http-notify');
|
||||||
res.json({ success: true, ...result });
|
ok(res, result);
|
||||||
}, 'system-update-notify'));
|
}, 'system-update-notify'));
|
||||||
|
|
||||||
// Get update status
|
// Get update status
|
||||||
router.get('/system/update-status', asyncHandler(async (req, res) => {
|
router.get('/system/update-status', asyncHandler(async (req, res) => {
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
status: selfUpdater.getStatus(),
|
status: selfUpdater.getStatus(),
|
||||||
lastCheck: selfUpdater.lastCheckTime,
|
lastCheck: selfUpdater.lastCheckTime,
|
||||||
lastResult: selfUpdater.lastCheckResult,
|
lastResult: selfUpdater.lastCheckResult,
|
||||||
@@ -151,13 +150,13 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
|
|||||||
// Get self-update history
|
// Get self-update history
|
||||||
router.get('/system/update-history', asyncHandler(async (req, res) => {
|
router.get('/system/update-history', asyncHandler(async (req, res) => {
|
||||||
const history = selfUpdater.getUpdateHistory();
|
const history = selfUpdater.getUpdateHistory();
|
||||||
res.json({ success: true, history });
|
ok(res, { history });
|
||||||
}, 'system-update-history'));
|
}, 'system-update-history'));
|
||||||
|
|
||||||
// List rollback versions
|
// List rollback versions
|
||||||
router.get('/system/rollback-versions', asyncHandler(async (req, res) => {
|
router.get('/system/rollback-versions', asyncHandler(async (req, res) => {
|
||||||
const versions = selfUpdater.getAvailableRollbacks();
|
const versions = selfUpdater.getAvailableRollbacks();
|
||||||
res.json({ success: true, versions });
|
ok(res, { versions });
|
||||||
}, 'system-rollback-versions'));
|
}, 'system-rollback-versions'));
|
||||||
|
|
||||||
// Rollback to a previous version
|
// Rollback to a previous version
|
||||||
@@ -167,7 +166,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
|
|||||||
selfUpdater.rollbackToVersion(version).catch(err => {
|
selfUpdater.rollbackToVersion(version).catch(err => {
|
||||||
logError('self-rollback', err);
|
logError('self-rollback', err);
|
||||||
});
|
});
|
||||||
res.json({ success: true, message: `Rollback to ${version} initiated` });
|
ok(res, { message: `Rollback to ${version} initiated` });
|
||||||
}, 'system-rollback'));
|
}, 'system-rollback'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { ok } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workflows routes factory
|
* Workflows routes factory
|
||||||
@@ -19,21 +20,21 @@ module.exports = function({ workflowEngine, licenseManager, asyncHandler }) {
|
|||||||
// List all bundled workflows
|
// List all bundled workflows
|
||||||
router.get('/workflows', asyncHandler(async (req, res) => {
|
router.get('/workflows', asyncHandler(async (req, res) => {
|
||||||
const workflows = workflowEngine.listWorkflows();
|
const workflows = workflowEngine.listWorkflows();
|
||||||
res.json({ success: true, workflows });
|
ok(res, { workflows });
|
||||||
}, 'workflows-list'));
|
}, 'workflows-list'));
|
||||||
|
|
||||||
// Enable a workflow
|
// Enable a workflow
|
||||||
router.post('/workflows/:workflowId/enable', asyncHandler(async (req, res) => {
|
router.post('/workflows/:workflowId/enable', asyncHandler(async (req, res) => {
|
||||||
const { workflowId } = req.params;
|
const { workflowId } = req.params;
|
||||||
const result = workflowEngine.setWorkflowEnabled(workflowId, true);
|
const result = workflowEngine.setWorkflowEnabled(workflowId, true);
|
||||||
res.json({ success: true, ...result });
|
ok(res, result);
|
||||||
}, 'workflows-enable'));
|
}, 'workflows-enable'));
|
||||||
|
|
||||||
// Disable a workflow
|
// Disable a workflow
|
||||||
router.post('/workflows/:workflowId/disable', asyncHandler(async (req, res) => {
|
router.post('/workflows/:workflowId/disable', asyncHandler(async (req, res) => {
|
||||||
const { workflowId } = req.params;
|
const { workflowId } = req.params;
|
||||||
const result = workflowEngine.setWorkflowEnabled(workflowId, false);
|
const result = workflowEngine.setWorkflowEnabled(workflowId, false);
|
||||||
res.json({ success: true, ...result });
|
ok(res, result);
|
||||||
}, 'workflows-disable'));
|
}, 'workflows-disable'));
|
||||||
|
|
||||||
// Manually trigger a workflow
|
// Manually trigger a workflow
|
||||||
@@ -43,7 +44,7 @@ module.exports = function({ workflowEngine, licenseManager, asyncHandler }) {
|
|||||||
triggerData.trigger = 'manual';
|
triggerData.trigger = 'manual';
|
||||||
|
|
||||||
const result = await workflowEngine.executeWorkflow(workflowId, triggerData);
|
const result = await workflowEngine.executeWorkflow(workflowId, triggerData);
|
||||||
res.json({ success: true, result });
|
ok(res, { result });
|
||||||
}, 'workflows-run'));
|
}, 'workflows-run'));
|
||||||
|
|
||||||
// Get execution history for a workflow
|
// Get execution history for a workflow
|
||||||
@@ -51,14 +52,14 @@ module.exports = function({ workflowEngine, licenseManager, asyncHandler }) {
|
|||||||
const { workflowId } = req.params;
|
const { workflowId } = req.params;
|
||||||
const limit = parseInt(req.query.limit) || 50;
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
const history = workflowEngine.getHistory(workflowId, limit);
|
const history = workflowEngine.getHistory(workflowId, limit);
|
||||||
res.json({ success: true, history });
|
ok(res, { history });
|
||||||
}, 'workflows-history'));
|
}, 'workflows-history'));
|
||||||
|
|
||||||
// Get all workflow execution history
|
// Get all workflow execution history
|
||||||
router.get('/workflows/history', asyncHandler(async (req, res) => {
|
router.get('/workflows/history', asyncHandler(async (req, res) => {
|
||||||
const limit = parseInt(req.query.limit) || 100;
|
const limit = parseInt(req.query.limit) || 100;
|
||||||
const history = workflowEngine.getHistory(null, limit);
|
const history = workflowEngine.getHistory(null, limit);
|
||||||
res.json({ success: true, history });
|
ok(res, { history });
|
||||||
}, 'workflows-all-history'));
|
}, 'workflows-all-history'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -404,8 +404,7 @@ async function createApp() {
|
|||||||
appName = pkg.name || appName;
|
appName = pkg.name || appName;
|
||||||
} catch { /* package.json unreadable — keep fallback */ }
|
} catch { /* package.json unreadable — keep fallback */ }
|
||||||
apiRouter.get('/version', (req, res) => {
|
apiRouter.get('/version', (req, res) => {
|
||||||
res.json({
|
ok(res, {
|
||||||
success: true,
|
|
||||||
name: appName,
|
name: appName,
|
||||||
version: appVersion,
|
version: appVersion,
|
||||||
node: process.version,
|
node: process.version,
|
||||||
@@ -608,15 +607,15 @@ async function createApp() {
|
|||||||
|
|
||||||
// Inline API routes
|
// Inline API routes
|
||||||
apiRouter.get('/health', (req, res) => {
|
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) => {
|
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) => {
|
apiRouter.get('/metrics', (req, res) => {
|
||||||
res.json({ success: true, metrics: metrics.getSummary() });
|
ok(res, { metrics: metrics.getSummary() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mount at /api/v1 (canonical, single version)
|
// Mount at /api/v1 (canonical, single version)
|
||||||
@@ -624,7 +623,7 @@ async function createApp() {
|
|||||||
|
|
||||||
// Root-level health check
|
// Root-level health check
|
||||||
app.get('/health', (req, res) => {
|
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?"
|
// Liveness probe — "is the process alive?"
|
||||||
@@ -632,7 +631,7 @@ async function createApp() {
|
|||||||
// Used by k8s/Docker to decide whether to RESTART the container.
|
// Used by k8s/Docker to decide whether to RESTART the container.
|
||||||
// DO NOT add dependency checks here — those belong in /health/ready.
|
// DO NOT add dependency checks here — those belong in /health/ready.
|
||||||
app.get('/health/live', (req, res) => {
|
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?"
|
// Readiness probe — "is the app ready to serve traffic?"
|
||||||
@@ -704,7 +703,7 @@ async function createApp() {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
checks
|
checks
|
||||||
};
|
};
|
||||||
res.status(allOk ? 200 : 503).json(body);
|
ok(res, body, allOk ? 200 : 503);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Lightweight probe endpoint
|
// Lightweight probe endpoint
|
||||||
@@ -830,7 +829,7 @@ async function createApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(result);
|
ok(res, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, 500, safeErrorMessage(error));
|
errorResponse(res, 500, safeErrorMessage(error));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,9 +93,8 @@ async function verifySiteAccessible(domain, fetchT, httpsAgent, log, maxAttempts
|
|||||||
try {
|
try {
|
||||||
const response = await fetchT(`https://${domain}/`, {
|
const response = await fetchT(`https://${domain}/`, {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
agent: httpsAgent,
|
agent: httpsAgent
|
||||||
timeout: 5000
|
}, 5000);
|
||||||
});
|
|
||||||
|
|
||||||
log.info('caddy', 'Site is accessible', { domain, status: response.status });
|
log.info('caddy', 'Site is accessible', { domain, status: response.status });
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ async function refreshDnsToken(username, password, server, fetchT, log) {
|
|||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
10000
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ function createProviderDnsContext(siteConfig, buildDomain, credentialManager, fe
|
|||||||
const params = new URLSearchParams({ user: username, pass: password, includeInfo: 'false' });
|
const params = new URLSearchParams({ user: username, pass: password, includeInfo: 'false' });
|
||||||
const response = await fetchT(
|
const response = await fetchT(
|
||||||
`http://${server}:5380/api/user/login?${params.toString()}`,
|
`http://${server}:5380/api/user/login?${params.toString()}`,
|
||||||
{ method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 }
|
{ method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' } },
|
||||||
|
10000
|
||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.status === 'ok' && result.token) {
|
if (result.status === 'ok' && result.token) {
|
||||||
|
|||||||
@@ -38,7 +38,15 @@ function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
|||||||
if (!opts.signal) {
|
if (!opts.signal) {
|
||||||
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
|
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
|
||||||
}
|
}
|
||||||
delete opts.timeout;
|
// The `timeout` key in fetch() opts is silently ignored by undici. Callers
|
||||||
|
// should use the third arg of fetchT() (timeoutMs) instead. If a caller
|
||||||
|
// passes `timeout: N` here, it's almost certainly a bug — we used to silently
|
||||||
|
// strip it, which masked the issue. Now we surface it in logs and strip it.
|
||||||
|
if ('timeout' in opts) {
|
||||||
|
console.warn(`[fetchT] opts.timeout=${opts.timeout} is ignored — pass timeoutMs as the 3rd arg of fetchT() instead. Called from: ${new Error().stack.split('\n').slice(2, 4).join(' <- ')}`);
|
||||||
|
const { timeout, ...rest } = opts;
|
||||||
|
opts = rest;
|
||||||
|
}
|
||||||
return fetch(url, opts);
|
return fetch(url, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,9 @@
|
|||||||
if (window.SkeletonLoader) window.SkeletonLoader.show(6);
|
if (window.SkeletonLoader) window.SkeletonLoader.show(6);
|
||||||
const response = await fetch('/api/v1/services', { cache: 'no-store' });
|
const response = await fetch('/api/v1/services', { cache: 'no-store' });
|
||||||
if (response.ok) {
|
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();
|
if (window.SkeletonLoader) window.SkeletonLoader.hide();
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to load services:', response.status);
|
console.error('Failed to load services:', response.status);
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.status === 'success') {
|
if (result.success) {
|
||||||
const select = document.getElementById('existing-ca-select');
|
const select = document.getElementById('existing-ca-select');
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
|
|
||||||
if (result.data.cas.length === 0) {
|
if (result.cas.length === 0) {
|
||||||
select.innerHTML = '<option value="">No CAs found in Caddyfile</option>';
|
select.innerHTML = '<option value="">No CAs found in Caddyfile</option>';
|
||||||
} else {
|
} else {
|
||||||
select.innerHTML = '<option value="">Select existing CA...</option>';
|
select.innerHTML = '<option value="">Select existing CA...</option>';
|
||||||
result.data.cas.forEach(ca => {
|
result.cas.forEach(ca => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
if (typeof ca === 'object') {
|
if (typeof ca === 'object') {
|
||||||
option.value = ca.id;
|
option.value = ca.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user