feat: 1.5.0 prep — API v1 cutover, LICENSE, CHANGELOG, CI
- Remove legacy /api/ mount; all routes now under /api/v1/ only - Update path matchers (CSRF excludes, public routes, audit log, rate limits) - Move standalone routes (/api/network/ips, /api/docs, /api/docs/spec) to v1 - Update openapi.yaml (110 paths), CA pages, and 4 lingering frontend files - Add LICENSE (proprietary EULA), CHANGELOG.md (Keep a Changelog format) - Add .gitea/workflows/ci.yml (test+lint and security audit jobs) - Fix 9 pre-existing no-empty lint errors so CI starts green - Drop ad-hoc scratch reports and *.bak files from repo root All 739 jest tests pass. Lint is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,7 +169,7 @@ describe('CSRF Protection', () => {
|
||||
const origEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const excludedPaths = ['/api/totp/verify', '/api/totp/setup', '/health', '/api/health'];
|
||||
const excludedPaths = ['/api/v1/totp/verify', '/api/v1/totp/setup', '/health', '/api/v1/health'];
|
||||
for (const excludedPath of excludedPaths) {
|
||||
const { req, res, next } = createMockReqRes({ method: 'POST', path: excludedPath });
|
||||
csrfValidationMiddleware(req, res, next);
|
||||
@@ -184,7 +184,7 @@ describe('CSRF Protection', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const { req, res, next } = createMockReqRes({
|
||||
method: 'POST', path: '/api/auth/gate/plex'
|
||||
method: 'POST', path: '/api/v1/auth/gate/plex'
|
||||
});
|
||||
csrfValidationMiddleware(req, res, next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
@@ -296,7 +296,7 @@ describe('CSRF Protection', () => {
|
||||
process.env.NODE_ENV = origEnv;
|
||||
});
|
||||
|
||||
it('normalizes /api/v1/ prefix for exclusion matching', () => {
|
||||
it('excludes /api/v1/ paths directly', () => {
|
||||
const origEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
|
||||
@@ -7,65 +7,65 @@ const MAX_ENTRIES = parseInt(process.env.AUDIT_MAX_ENTRIES || '1000', 10);
|
||||
|
||||
// Route path → readable action mapping
|
||||
const ACTION_MAP = {
|
||||
'POST /api/services/update': 'service.reorder',
|
||||
'POST /api/services': 'service.create',
|
||||
'PUT /api/services': 'service.update',
|
||||
'DELETE /api/services/': 'service.delete',
|
||||
'POST /api/site': 'caddy.add-site',
|
||||
'POST /api/site/external': 'caddy.add-external',
|
||||
'DELETE /api/site/': 'caddy.remove-site',
|
||||
'POST /api/caddy/reload': 'caddy.reload',
|
||||
'POST /api/dns/record': 'dns.add-record',
|
||||
'DELETE /api/dns/record': 'dns.delete-record',
|
||||
'POST /api/dns/credentials': 'dns.save-credentials',
|
||||
'DELETE /api/dns/credentials': 'dns.delete-credentials',
|
||||
'POST /api/dns/refresh-token': 'dns.refresh-token',
|
||||
'POST /api/dns/update': 'dns.update-server',
|
||||
'POST /api/containers/': 'container.action',
|
||||
'DELETE /api/containers/': 'container.delete',
|
||||
'POST /api/apps/deploy': 'container.deploy',
|
||||
'DELETE /api/apps/': 'container.undeploy',
|
||||
'POST /api/backups/execute': 'backup.execute',
|
||||
'POST /api/backups/restore/': 'backup.restore',
|
||||
'POST /api/backups/config': 'backup.config',
|
||||
'POST /api/config': 'config.update',
|
||||
'DELETE /api/config': 'config.reset',
|
||||
'POST /api/notifications/config': 'config.notifications',
|
||||
'POST /api/totp/setup': 'auth.totp-setup',
|
||||
'POST /api/totp/verify-setup': 'auth.totp-activate',
|
||||
'POST /api/totp/disable': 'auth.totp-disable',
|
||||
'POST /api/totp/config': 'auth.totp-config',
|
||||
'POST /api/credentials/rotate-key': 'config.rotate-key',
|
||||
'POST /api/updates/update/': 'container.update',
|
||||
'POST /api/updates/rollback/': 'container.rollback',
|
||||
'POST /api/updates/auto-update/': 'container.auto-update',
|
||||
'POST /api/updates/check': 'container.check-updates',
|
||||
'POST /api/health-checks/': 'config.health-check',
|
||||
'DELETE /api/health-checks/': 'config.health-check-delete',
|
||||
'POST /api/monitoring/alerts/': 'config.monitoring-alert',
|
||||
'DELETE /api/monitoring/alerts/': 'config.monitoring-alert-delete',
|
||||
'POST /api/arr/smart-connect': 'service.arr-connect',
|
||||
'POST /api/arr/credentials': 'config.arr-credentials',
|
||||
'DELETE /api/arr/credentials/': 'config.arr-credentials-delete',
|
||||
'POST /api/logo': 'config.logo-upload',
|
||||
'DELETE /api/logo': 'config.logo-delete',
|
||||
'POST /api/favicon': 'config.favicon-upload',
|
||||
'DELETE /api/favicon': 'config.favicon-delete',
|
||||
'POST /api/tailscale/config': 'config.tailscale',
|
||||
'POST /api/tailscale/protect-service': 'config.tailscale-protect',
|
||||
'POST /api/v1/services/update': 'service.reorder',
|
||||
'POST /api/v1/services': 'service.create',
|
||||
'PUT /api/v1/services': 'service.update',
|
||||
'DELETE /api/v1/services/': 'service.delete',
|
||||
'POST /api/v1/site': 'caddy.add-site',
|
||||
'POST /api/v1/site/external': 'caddy.add-external',
|
||||
'DELETE /api/v1/site/': 'caddy.remove-site',
|
||||
'POST /api/v1/caddy/reload': 'caddy.reload',
|
||||
'POST /api/v1/dns/record': 'dns.add-record',
|
||||
'DELETE /api/v1/dns/record': 'dns.delete-record',
|
||||
'POST /api/v1/dns/credentials': 'dns.save-credentials',
|
||||
'DELETE /api/v1/dns/credentials': 'dns.delete-credentials',
|
||||
'POST /api/v1/dns/refresh-token': 'dns.refresh-token',
|
||||
'POST /api/v1/dns/update': 'dns.update-server',
|
||||
'POST /api/v1/containers/': 'container.action',
|
||||
'DELETE /api/v1/containers/': 'container.delete',
|
||||
'POST /api/v1/apps/deploy': 'container.deploy',
|
||||
'DELETE /api/v1/apps/': 'container.undeploy',
|
||||
'POST /api/v1/backups/execute': 'backup.execute',
|
||||
'POST /api/v1/backups/restore/': 'backup.restore',
|
||||
'POST /api/v1/backups/config': 'backup.config',
|
||||
'POST /api/v1/config': 'config.update',
|
||||
'DELETE /api/v1/config': 'config.reset',
|
||||
'POST /api/v1/notifications/config': 'config.notifications',
|
||||
'POST /api/v1/totp/setup': 'auth.totp-setup',
|
||||
'POST /api/v1/totp/verify-setup': 'auth.totp-activate',
|
||||
'POST /api/v1/totp/disable': 'auth.totp-disable',
|
||||
'POST /api/v1/totp/config': 'auth.totp-config',
|
||||
'POST /api/v1/credentials/rotate-key': 'config.rotate-key',
|
||||
'POST /api/v1/updates/update/': 'container.update',
|
||||
'POST /api/v1/updates/rollback/': 'container.rollback',
|
||||
'POST /api/v1/updates/auto-update/': 'container.auto-update',
|
||||
'POST /api/v1/updates/check': 'container.check-updates',
|
||||
'POST /api/v1/health-checks/': 'config.health-check',
|
||||
'DELETE /api/v1/health-checks/': 'config.health-check-delete',
|
||||
'POST /api/v1/monitoring/alerts/': 'config.monitoring-alert',
|
||||
'DELETE /api/v1/monitoring/alerts/': 'config.monitoring-alert-delete',
|
||||
'POST /api/v1/arr/smart-connect': 'service.arr-connect',
|
||||
'POST /api/v1/arr/credentials': 'config.arr-credentials',
|
||||
'DELETE /api/v1/arr/credentials/': 'config.arr-credentials-delete',
|
||||
'POST /api/v1/logo': 'config.logo-upload',
|
||||
'DELETE /api/v1/logo': 'config.logo-delete',
|
||||
'POST /api/v1/favicon': 'config.favicon-upload',
|
||||
'DELETE /api/v1/favicon': 'config.favicon-delete',
|
||||
'POST /api/v1/tailscale/config': 'config.tailscale',
|
||||
'POST /api/v1/tailscale/protect-service': 'config.tailscale-protect',
|
||||
};
|
||||
|
||||
// Paths to skip logging (noisy or internal)
|
||||
const SKIP_PATHS = [
|
||||
'/api/totp/verify',
|
||||
'/api/totp/check-session',
|
||||
'/api/auth/gate/',
|
||||
'/api/auth/app-token/',
|
||||
'/api/audit-logs',
|
||||
'/api/health',
|
||||
'/api/v1/totp/verify',
|
||||
'/api/v1/totp/check-session',
|
||||
'/api/v1/auth/gate/',
|
||||
'/api/v1/auth/app-token/',
|
||||
'/api/v1/audit-logs',
|
||||
'/api/v1/health',
|
||||
'/health',
|
||||
'/api/notifications/test',
|
||||
'/api/notifications/health-check',
|
||||
'/api/v1/notifications/test',
|
||||
'/api/v1/notifications/health-check',
|
||||
];
|
||||
|
||||
class AuditLogger {
|
||||
@@ -82,14 +82,14 @@ class AuditLogger {
|
||||
if (key.startsWith(pattern)) return action;
|
||||
}
|
||||
// Fallback: derive from path
|
||||
const parts = urlPath.replace('/api/', '').split('/');
|
||||
const parts = urlPath.replace('/api/v1/', '').split('/');
|
||||
const category = parts[0] || 'unknown';
|
||||
return `${category}.${method.toLowerCase()}`;
|
||||
}
|
||||
|
||||
extractResource(urlPath) {
|
||||
// Pull a meaningful resource identifier from the URL path
|
||||
const parts = urlPath.replace('/api/', '').split('/');
|
||||
const parts = urlPath.replace('/api/v1/', '').split('/');
|
||||
if (parts.length >= 2) return parts.slice(1).join('/');
|
||||
return parts[0] || '';
|
||||
}
|
||||
|
||||
@@ -740,7 +740,7 @@ class BackupManager extends EventEmitter {
|
||||
size: data.length
|
||||
};
|
||||
} finally {
|
||||
try { await client.end(); } catch (_) {}
|
||||
try { await client.end(); } catch (_) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,7 +759,7 @@ class BackupManager extends EventEmitter {
|
||||
const buffer = await client.get(location.path);
|
||||
return Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
||||
} finally {
|
||||
try { await client.end(); } catch (_) {}
|
||||
try { await client.end(); } catch (_) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,7 +786,7 @@ class BackupManager extends EventEmitter {
|
||||
// Delete the probe
|
||||
try {
|
||||
await this._deleteFromDestination(location);
|
||||
} catch (_) {}
|
||||
} catch (_) { /* ignore */ }
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
return {
|
||||
@@ -817,14 +817,14 @@ class BackupManager extends EventEmitter {
|
||||
const { Dropbox } = require('dropbox');
|
||||
const creds = await this._getCloudCredentials('dropbox');
|
||||
const dbx = new Dropbox({ accessToken: creds.token });
|
||||
try { await dbx.filesDeleteV2({ path: location.path }); } catch (_) {}
|
||||
try { await dbx.filesDeleteV2({ path: location.path }); } catch (_) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
if (location.type === 'webdav') {
|
||||
const { createClient } = require('webdav');
|
||||
const creds = await this._getCloudCredentials('webdav');
|
||||
const client = createClient(creds.url, { username: creds.username, password: creds.password });
|
||||
try { await client.deleteFile(location.path); } catch (_) {}
|
||||
try { await client.deleteFile(location.path); } catch (_) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
if (location.type === 'sftp') {
|
||||
@@ -839,9 +839,9 @@ class BackupManager extends EventEmitter {
|
||||
password: creds.password || undefined,
|
||||
privateKey: creds.privateKey || undefined
|
||||
});
|
||||
try { await client.delete(location.path); } catch (_) {}
|
||||
try { await client.delete(location.path); } catch (_) { /* ignore */ }
|
||||
} finally {
|
||||
try { await client.end(); } catch (_) {}
|
||||
try { await client.end(); } catch (_) { /* ignore */ }
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -895,7 +895,7 @@ class BackupManager extends EventEmitter {
|
||||
recovered = true;
|
||||
console.log(`[BackupManager] Loaded backup from fallback location ${backup.locations[i].type}`);
|
||||
break;
|
||||
} catch (_) {}
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
if (!recovered) throw loadErr;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ DashCA now provides automatic SSL certificate generation for services on your ne
|
||||
|
||||
### 1. Generate/Download Certificate
|
||||
|
||||
**GET** `/api/ca/cert/:domain`
|
||||
**GET** `/api/v1/ca/cert/:domain`
|
||||
|
||||
Generate and download an SSL certificate for the specified domain.
|
||||
|
||||
@@ -52,7 +52,7 @@ Invoke-WebRequest -Uri "https://ca.sami/api/ca/cert/dns1.sami?format=pfx" -OutFi
|
||||
|
||||
### 2. List Generated Certificates
|
||||
|
||||
**GET** `/api/ca/certs`
|
||||
**GET** `/api/v1/ca/certs`
|
||||
|
||||
List all generated certificates with their status and expiration information.
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ The landing page (`index.html`) includes:
|
||||
- **DashCaddy Theme:** Dark mode with Sami Grotesk font
|
||||
|
||||
**API Integration:**
|
||||
- Loads certificate info from `/api/ca/info` endpoint
|
||||
- Loads certificate info from `/api/v1/ca/info` endpoint
|
||||
- Falls back to static info if API unavailable
|
||||
|
||||
## Development Workflow
|
||||
@@ -234,7 +234,7 @@ Returns CA expiration health status:
|
||||
|
||||
**Symptom:** Certificate info stuck on loading state
|
||||
**Cause:** API endpoint not accessible
|
||||
**Solution:** Check that dashcaddy-api server is running and `/api/ca/info` responds
|
||||
**Solution:** Check that dashcaddy-api server is running and `/api/v1/ca/info` responds
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
|
||||
@@ -952,7 +952,7 @@
|
||||
async function loadCertInfo() {
|
||||
try {
|
||||
// Add cache-busting parameter
|
||||
const response = await fetch('/api/ca/info?t=' + Date.now());
|
||||
const response = await fetch('/api/v1/ca/info?t=' + Date.now());
|
||||
if (!response.ok) {
|
||||
console.error('API response not OK:', response.status, response.statusText);
|
||||
throw new Error('Failed to fetch certificate info');
|
||||
@@ -1151,7 +1151,7 @@
|
||||
// Load service certificates
|
||||
async function loadServiceCertificates() {
|
||||
try {
|
||||
const response = await fetch('/api/ca/certs');
|
||||
const response = await fetch('/api/v1/ca/certs');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch certificates');
|
||||
}
|
||||
@@ -1205,17 +1205,17 @@
|
||||
<strong>Fingerprint:</strong><br>${cert.fingerprint}
|
||||
</div>
|
||||
<div class="cert-actions">
|
||||
<a href="/api/ca/cert/${cert.domain}?format=pfx&password=dashcaddy"
|
||||
<a href="/api/v1/ca/cert/${cert.domain}?format=pfx&password=dashcaddy"
|
||||
class="cert-btn cert-btn-primary"
|
||||
download="${cert.domain}.pfx">
|
||||
Download PFX
|
||||
</a>
|
||||
<a href="/api/ca/cert/${cert.domain}?format=pem"
|
||||
<a href="/api/v1/ca/cert/${cert.domain}?format=pem"
|
||||
class="cert-btn"
|
||||
download="${cert.domain}.pem">
|
||||
PEM
|
||||
</a>
|
||||
<a href="/api/ca/cert/${cert.domain}?format=crt"
|
||||
<a href="/api/v1/ca/cert/${cert.domain}?format=crt"
|
||||
class="cert-btn"
|
||||
download="${cert.domain}.crt">
|
||||
CRT
|
||||
|
||||
@@ -118,20 +118,18 @@ function csrfValidationMiddleware(req, res, next) {
|
||||
|
||||
// Excluded paths that don't require CSRF validation
|
||||
const excludedPaths = [
|
||||
'/api/totp/verify',
|
||||
'/api/totp/verify-setup',
|
||||
'/api/totp/setup',
|
||||
'/api/v1/totp/verify',
|
||||
'/api/v1/totp/verify-setup',
|
||||
'/api/v1/totp/setup',
|
||||
'/health',
|
||||
'/api/health',
|
||||
'/api/v1/health',
|
||||
// Machine-to-machine: publishing host POSTs here with its own shared-secret
|
||||
// header (X-DashCaddy-Notify-Secret) — browsers never reach this endpoint.
|
||||
'/api/system/update-notify'
|
||||
'/api/v1/system/update-notify'
|
||||
];
|
||||
|
||||
// Normalize /api/v1/... to /api/... so exclusions work with both prefixes
|
||||
const normalizedPath = req.path.replace(/^\/api\/v1\//, '/api/');
|
||||
const isExcluded = excludedPaths.some(path => normalizedPath === path) ||
|
||||
normalizedPath.startsWith('/api/auth/gate/');
|
||||
const isExcluded = excludedPaths.some(path => req.path === path) ||
|
||||
req.path.startsWith('/api/v1/auth/gate/');
|
||||
|
||||
if (isExcluded) {
|
||||
return next();
|
||||
|
||||
+39
-41
@@ -92,7 +92,7 @@ module.exports = function configureMiddleware(app, {
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
metrics.recordRequest(req.method, req.path, res.statusCode, duration);
|
||||
if (req.path !== '/health' && req.path !== '/api/health') {
|
||||
if (req.path !== '/health' && req.path !== '/api/v1/health') {
|
||||
const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'debug';
|
||||
log[level]('http', `${req.method} ${req.path} ${res.statusCode}`, {
|
||||
ms: duration, ip: req.ip, id: req.id
|
||||
@@ -108,11 +108,11 @@ module.exports = function configureMiddleware(app, {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.path === '/health' || req.path === '/api/health' || req.path.startsWith('/probe/')) {
|
||||
if (req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.path.startsWith('/api/tailscale/')) {
|
||||
if (req.path.startsWith('/api/v1/tailscale/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -273,41 +273,39 @@ module.exports = function configureMiddleware(app, {
|
||||
|
||||
// ── Public routes (bypass TOTP and JWT auth) ──
|
||||
const PUBLIC_ROUTES = [
|
||||
{ path: '/health', exact: true },
|
||||
{ path: '/api/health', exact: true },
|
||||
{ path: '/probe/', prefix: true },
|
||||
{ path: '/api/tailscale/', prefix: true },
|
||||
{ path: '/api/totp/config', exact: true, method: 'GET' },
|
||||
{ path: '/api/totp/verify', exact: true },
|
||||
{ path: '/api/totp/setup', exact: true, method: 'POST' },
|
||||
{ path: '/api/totp/verify-setup', exact: true, method: 'POST' },
|
||||
{ path: '/api/totp/check-session', exact: true },
|
||||
{ path: '/api/auth/gate/', prefix: true },
|
||||
{ path: '/api/auth/app-token/', prefix: true },
|
||||
{ path: '/api/services', exact: true, method: 'GET' },
|
||||
{ path: '/api/ca/info', exact: true, method: 'GET' },
|
||||
{ path: '/api/ca/root.crt', exact: true, method: 'GET' },
|
||||
{ path: '/api/ca/install-script', exact: true, method: 'GET' },
|
||||
{ path: '/api/health/ca', exact: true, method: 'GET' },
|
||||
{ path: '/api/ca/cert/', prefix: true, method: 'GET' },
|
||||
{ path: '/api/ca/certs', exact: true, method: 'GET' },
|
||||
{ path: '/api/csrf-token', exact: true, method: 'GET' },
|
||||
{ path: '/api/logo', exact: true, method: 'GET' },
|
||||
{ path: '/api/favicon', exact: true, method: 'GET' },
|
||||
{ path: '/api/themes', exact: true, method: 'GET' },
|
||||
{ path: '/api/license/status', exact: true, method: 'GET' },
|
||||
{ path: '/api/license/feature/', prefix: true, method: 'GET' },
|
||||
{ path: '/api/config', exact: true, method: 'GET' },
|
||||
{ path: '/api/services/status', exact: true, method: 'GET' },
|
||||
{ path: '/api/system/update-notify', exact: true, method: 'POST' },
|
||||
{ path: '/health', exact: true },
|
||||
{ path: '/api/v1/health', exact: true },
|
||||
{ path: '/probe/', prefix: true },
|
||||
{ path: '/api/v1/tailscale/', prefix: true },
|
||||
{ path: '/api/v1/totp/config', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/totp/verify', exact: true },
|
||||
{ path: '/api/v1/totp/setup', exact: true, method: 'POST' },
|
||||
{ path: '/api/v1/totp/verify-setup', exact: true, method: 'POST' },
|
||||
{ path: '/api/v1/totp/check-session', exact: true },
|
||||
{ path: '/api/v1/auth/gate/', prefix: true },
|
||||
{ path: '/api/v1/auth/app-token/', prefix: true },
|
||||
{ path: '/api/v1/services', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/ca/info', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/ca/root.crt', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/ca/install-script', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/health/ca', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/ca/cert/', prefix: true, method: 'GET' },
|
||||
{ path: '/api/v1/ca/certs', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/csrf-token', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/logo', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/favicon', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/themes', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/license/status', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/license/feature/', prefix: true, method: 'GET' },
|
||||
{ path: '/api/v1/config', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/services/status', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/system/update-notify', exact: true, method: 'POST' },
|
||||
];
|
||||
|
||||
function isPublicRoute(req) {
|
||||
// Normalize /api/v1/... to /api/... so public routes work with both
|
||||
const p = req.path.replace(/^\/api\/v1\//, '/api/');
|
||||
return PUBLIC_ROUTES.some(r => {
|
||||
if (r.method && req.method !== r.method) return false;
|
||||
return r.prefix ? p.startsWith(r.path) : p === r.path;
|
||||
return r.prefix ? req.path.startsWith(r.path) : req.path === r.path;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,7 +387,7 @@ module.exports = function configureMiddleware(app, {
|
||||
...RATE_LIMITS.GENERAL,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skip: (req) => isTest || req.path === '/health' || req.path === '/api/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/auth/gate/') || req.path === '/api/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/license/status' || req.path.startsWith('/api/license/feature/') || req.path === '/api/services' || req.path === '/api/config',
|
||||
skip: (req) => isTest || req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/v1/auth/gate/') || req.path === '/api/v1/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/v1/license/status' || req.path.startsWith('/api/v1/license/feature/') || req.path === '/api/v1/services' || req.path === '/api/v1/config',
|
||||
message: { success: false, error: 'Too many requests, please try again later' }
|
||||
});
|
||||
|
||||
@@ -402,11 +400,11 @@ module.exports = function configureMiddleware(app, {
|
||||
});
|
||||
|
||||
app.use(generalLimiter);
|
||||
app.use('/api/dns/credentials', strictLimiter);
|
||||
app.use('/api/apps/deploy', strictLimiter);
|
||||
app.use('/api/backup/restore', strictLimiter);
|
||||
app.use('/api/site', strictLimiter);
|
||||
app.use('/api/credentials/rotate-key', strictLimiter);
|
||||
app.use('/api/v1/dns/credentials', strictLimiter);
|
||||
app.use('/api/v1/apps/deploy', strictLimiter);
|
||||
app.use('/api/v1/backup/restore', strictLimiter);
|
||||
app.use('/api/v1/site', strictLimiter);
|
||||
app.use('/api/v1/credentials/rotate-key', strictLimiter);
|
||||
|
||||
const totpLimiter = rateLimit({
|
||||
...RATE_LIMITS.TOTP,
|
||||
@@ -414,8 +412,8 @@ module.exports = function configureMiddleware(app, {
|
||||
legacyHeaders: false,
|
||||
message: { success: false, error: 'Too many TOTP attempts, please try again later' }
|
||||
});
|
||||
app.use('/api/totp/verify', totpLimiter);
|
||||
app.use('/api/totp/verify-setup', totpLimiter);
|
||||
app.use('/api/v1/totp/verify', totpLimiter);
|
||||
app.use('/api/v1/totp/verify-setup', totpLimiter);
|
||||
|
||||
// ── Audit logging middleware (logs non-GET API requests) ──
|
||||
app.use(auditLogger.middleware());
|
||||
|
||||
+110
-110
@@ -83,7 +83,7 @@ paths:
|
||||
type: string
|
||||
example: ok
|
||||
|
||||
/api/health:
|
||||
/api/v1/health:
|
||||
get:
|
||||
tags: [Health & Status]
|
||||
summary: API health check
|
||||
@@ -118,7 +118,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/network/ips:
|
||||
/api/v1/network/ips:
|
||||
get:
|
||||
tags: [Health & Status]
|
||||
summary: Get network interface IPs
|
||||
@@ -145,7 +145,7 @@ paths:
|
||||
type: string
|
||||
|
||||
# TOTP Authentication
|
||||
/api/totp/config:
|
||||
/api/v1/totp/config:
|
||||
get:
|
||||
tags: [TOTP Authentication]
|
||||
summary: Get TOTP configuration
|
||||
@@ -184,7 +184,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/totp/setup:
|
||||
/api/v1/totp/setup:
|
||||
post:
|
||||
tags: [TOTP Authentication]
|
||||
summary: Generate TOTP secret
|
||||
@@ -206,7 +206,7 @@ paths:
|
||||
otpAuthUrl:
|
||||
type: string
|
||||
|
||||
/api/totp/verify-setup:
|
||||
/api/v1/totp/verify-setup:
|
||||
post:
|
||||
tags: [TOTP Authentication]
|
||||
summary: Verify and activate TOTP
|
||||
@@ -229,7 +229,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/totp/verify:
|
||||
/api/v1/totp/verify:
|
||||
post:
|
||||
tags: [TOTP Authentication]
|
||||
summary: Verify TOTP code and create session
|
||||
@@ -259,7 +259,7 @@ paths:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
/api/totp/check-session:
|
||||
/api/v1/totp/check-session:
|
||||
get:
|
||||
tags: [TOTP Authentication]
|
||||
summary: Check if session is valid
|
||||
@@ -276,7 +276,7 @@ paths:
|
||||
valid:
|
||||
type: boolean
|
||||
|
||||
/api/totp/disable:
|
||||
/api/v1/totp/disable:
|
||||
post:
|
||||
tags: [TOTP Authentication]
|
||||
summary: Disable TOTP
|
||||
@@ -299,7 +299,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# SSO Auth Gate
|
||||
/api/auth/gate/{serviceId}:
|
||||
/api/v1/auth/gate/{serviceId}:
|
||||
get:
|
||||
tags: [SSO Auth Gate]
|
||||
summary: Forward auth endpoint for Caddy
|
||||
@@ -315,7 +315,7 @@ paths:
|
||||
'401':
|
||||
description: Auth failed
|
||||
|
||||
/api/auth/app-token/{serviceId}:
|
||||
/api/v1/auth/app-token/{serviceId}:
|
||||
get:
|
||||
tags: [SSO Auth Gate]
|
||||
summary: Get app-specific session token
|
||||
@@ -339,7 +339,7 @@ paths:
|
||||
type: string
|
||||
|
||||
# Service Credentials
|
||||
/api/service-creds/{serviceId}:
|
||||
/api/v1/service-creds/{serviceId}:
|
||||
post:
|
||||
tags: [Service Credentials]
|
||||
summary: Store service credentials
|
||||
@@ -408,7 +408,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/seedhost-creds:
|
||||
/api/v1/seedhost-creds:
|
||||
post:
|
||||
tags: [Service Credentials]
|
||||
summary: Store seedhost credentials
|
||||
@@ -459,7 +459,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Tailscale
|
||||
/api/tailscale/status:
|
||||
/api/v1/tailscale/status:
|
||||
get:
|
||||
tags: [Tailscale]
|
||||
summary: Get Tailscale status
|
||||
@@ -482,7 +482,7 @@ paths:
|
||||
hostname:
|
||||
type: string
|
||||
|
||||
/api/tailscale/config:
|
||||
/api/v1/tailscale/config:
|
||||
post:
|
||||
tags: [Tailscale]
|
||||
summary: Update Tailscale config
|
||||
@@ -504,7 +504,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/tailscale/check-connection:
|
||||
/api/v1/tailscale/check-connection:
|
||||
get:
|
||||
tags: [Tailscale]
|
||||
summary: Check if request is from Tailscale
|
||||
@@ -521,7 +521,7 @@ paths:
|
||||
isTailscale:
|
||||
type: boolean
|
||||
|
||||
/api/tailscale/devices:
|
||||
/api/v1/tailscale/devices:
|
||||
get:
|
||||
tags: [Tailscale]
|
||||
summary: List Tailscale devices
|
||||
@@ -540,7 +540,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/tailscale/protect-service:
|
||||
/api/v1/tailscale/protect-service:
|
||||
post:
|
||||
tags: [Tailscale]
|
||||
summary: Add Tailscale ACLs
|
||||
@@ -563,7 +563,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Caddy Management
|
||||
/api/caddyfile:
|
||||
/api/v1/caddyfile:
|
||||
get:
|
||||
tags: [Caddy Management]
|
||||
summary: Read Caddyfile
|
||||
@@ -580,7 +580,7 @@ paths:
|
||||
content:
|
||||
type: string
|
||||
|
||||
/api/caddy/config:
|
||||
/api/v1/caddy/config:
|
||||
get:
|
||||
tags: [Caddy Management]
|
||||
summary: Get Caddy admin config
|
||||
@@ -597,7 +597,7 @@ paths:
|
||||
config:
|
||||
type: object
|
||||
|
||||
/api/caddy/reload:
|
||||
/api/v1/caddy/reload:
|
||||
post:
|
||||
tags: [Caddy Management]
|
||||
summary: Reload Caddy
|
||||
@@ -609,7 +609,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/caddy/get-cas:
|
||||
/api/v1/caddy/get-cas:
|
||||
get:
|
||||
tags: [Caddy Management]
|
||||
summary: Get certificate authorities
|
||||
@@ -629,7 +629,7 @@ paths:
|
||||
type: object
|
||||
|
||||
# Site Management
|
||||
/api/site:
|
||||
/api/v1/site:
|
||||
post:
|
||||
tags: [Site Management]
|
||||
summary: Add site to Caddyfile
|
||||
@@ -655,7 +655,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/site/external:
|
||||
/api/v1/site/external:
|
||||
post:
|
||||
tags: [Site Management]
|
||||
summary: Add external service proxy
|
||||
@@ -683,7 +683,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/site/{domain}:
|
||||
/api/v1/site/{domain}:
|
||||
delete:
|
||||
tags: [Site Management]
|
||||
summary: Remove site from Caddyfile
|
||||
@@ -702,7 +702,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# DNS Management
|
||||
/api/dns/record:
|
||||
/api/v1/dns/record:
|
||||
post:
|
||||
tags: [DNS Management]
|
||||
summary: Create DNS record
|
||||
@@ -757,7 +757,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/dns/resolve:
|
||||
/api/v1/dns/resolve:
|
||||
get:
|
||||
tags: [DNS Management]
|
||||
summary: Resolve DNS
|
||||
@@ -790,7 +790,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/dns/logs:
|
||||
/api/v1/dns/logs:
|
||||
get:
|
||||
tags: [DNS Management]
|
||||
summary: Get DNS query logs
|
||||
@@ -826,7 +826,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/dns/token-status:
|
||||
/api/v1/dns/token-status:
|
||||
get:
|
||||
tags: [DNS Management]
|
||||
summary: Check DNS token status
|
||||
@@ -843,7 +843,7 @@ paths:
|
||||
valid:
|
||||
type: boolean
|
||||
|
||||
/api/dns/credentials:
|
||||
/api/v1/dns/credentials:
|
||||
post:
|
||||
tags: [DNS Management]
|
||||
summary: Store DNS credentials
|
||||
@@ -894,7 +894,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/dns/refresh-token:
|
||||
/api/v1/dns/refresh-token:
|
||||
post:
|
||||
tags: [DNS Management]
|
||||
summary: Refresh DNS API token
|
||||
@@ -906,7 +906,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/dns/check-update:
|
||||
/api/v1/dns/check-update:
|
||||
get:
|
||||
tags: [DNS Management]
|
||||
summary: Check for DNS server updates
|
||||
@@ -923,7 +923,7 @@ paths:
|
||||
updateAvailable:
|
||||
type: boolean
|
||||
|
||||
/api/dns/update:
|
||||
/api/v1/dns/update:
|
||||
post:
|
||||
tags: [DNS Management]
|
||||
summary: Update DNS server
|
||||
@@ -936,7 +936,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Services Dashboard
|
||||
/api/services:
|
||||
/api/v1/services:
|
||||
get:
|
||||
tags: [Services Dashboard]
|
||||
summary: List all services
|
||||
@@ -992,7 +992,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/services/{id}:
|
||||
/api/v1/services/{id}:
|
||||
delete:
|
||||
tags: [Services Dashboard]
|
||||
summary: Delete service
|
||||
@@ -1010,7 +1010,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/services/update:
|
||||
/api/v1/services/update:
|
||||
post:
|
||||
tags: [Services Dashboard]
|
||||
summary: Reorder services
|
||||
@@ -1034,7 +1034,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Assets & Branding
|
||||
/api/assets/upload:
|
||||
/api/v1/assets/upload:
|
||||
post:
|
||||
tags: [Assets & Branding]
|
||||
summary: Upload asset file
|
||||
@@ -1061,7 +1061,7 @@ paths:
|
||||
filename:
|
||||
type: string
|
||||
|
||||
/api/logo:
|
||||
/api/v1/logo:
|
||||
get:
|
||||
tags: [Assets & Branding]
|
||||
summary: Get custom logo
|
||||
@@ -1104,7 +1104,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/favicon:
|
||||
/api/v1/favicon:
|
||||
get:
|
||||
tags: [Assets & Branding]
|
||||
summary: Get custom favicon
|
||||
@@ -1148,7 +1148,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Configuration
|
||||
/api/config:
|
||||
/api/v1/config:
|
||||
get:
|
||||
tags: [Configuration]
|
||||
summary: Get DashCaddy config
|
||||
@@ -1192,7 +1192,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Backup & Restore
|
||||
/api/backup/export:
|
||||
/api/v1/backup/export:
|
||||
get:
|
||||
tags: [Backup & Restore]
|
||||
summary: Export full backup
|
||||
@@ -1209,7 +1209,7 @@ paths:
|
||||
backup:
|
||||
type: object
|
||||
|
||||
/api/backup/preview:
|
||||
/api/v1/backup/preview:
|
||||
post:
|
||||
tags: [Backup & Restore]
|
||||
summary: Preview backup contents
|
||||
@@ -1235,7 +1235,7 @@ paths:
|
||||
preview:
|
||||
type: object
|
||||
|
||||
/api/backup/restore:
|
||||
/api/v1/backup/restore:
|
||||
post:
|
||||
tags: [Backup & Restore]
|
||||
summary: Restore from backup
|
||||
@@ -1257,7 +1257,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Credential Management
|
||||
/api/credentials/list:
|
||||
/api/v1/credentials/list:
|
||||
get:
|
||||
tags: [Credential Management]
|
||||
summary: List all stored credentials
|
||||
@@ -1276,7 +1276,7 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
|
||||
/api/credentials/rotate-key:
|
||||
/api/v1/credentials/rotate-key:
|
||||
post:
|
||||
tags: [Credential Management]
|
||||
summary: Rotate encryption key
|
||||
@@ -1289,7 +1289,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Arr Stack Integration
|
||||
/api/arr/detect:
|
||||
/api/v1/arr/detect:
|
||||
get:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Detect installed Arr apps
|
||||
@@ -1308,7 +1308,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/arr/configure-overseerr:
|
||||
/api/v1/arr/configure-overseerr:
|
||||
post:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Configure Overseerr
|
||||
@@ -1330,7 +1330,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/arr/test-connection:
|
||||
/api/v1/arr/test-connection:
|
||||
post:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Test Arr service connection
|
||||
@@ -1359,7 +1359,7 @@ paths:
|
||||
connected:
|
||||
type: boolean
|
||||
|
||||
/api/arr/auto-setup:
|
||||
/api/v1/arr/auto-setup:
|
||||
post:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Automatic Arr stack setup
|
||||
@@ -1371,7 +1371,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/arr/credentials:
|
||||
/api/v1/arr/credentials:
|
||||
post:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Store Arr credentials
|
||||
@@ -1409,7 +1409,7 @@ paths:
|
||||
credentials:
|
||||
type: object
|
||||
|
||||
/api/arr/credentials/{service}:
|
||||
/api/v1/arr/credentials/{service}:
|
||||
delete:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Delete Arr credentials
|
||||
@@ -1427,7 +1427,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/arr/smart-detect:
|
||||
/api/v1/arr/smart-detect:
|
||||
get:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Smart detection of Arr services
|
||||
@@ -1444,7 +1444,7 @@ paths:
|
||||
detected:
|
||||
type: object
|
||||
|
||||
/api/arr/smart-connect:
|
||||
/api/v1/arr/smart-connect:
|
||||
post:
|
||||
tags: [Arr Stack Integration]
|
||||
summary: Smart connect Arr stack
|
||||
@@ -1471,7 +1471,7 @@ paths:
|
||||
type: object
|
||||
|
||||
# Plex
|
||||
/api/plex/libraries:
|
||||
/api/v1/plex/libraries:
|
||||
get:
|
||||
tags: [Plex]
|
||||
summary: Get Plex libraries
|
||||
@@ -1491,7 +1491,7 @@ paths:
|
||||
type: object
|
||||
|
||||
# Docker App Deployment
|
||||
/api/apps/templates:
|
||||
/api/v1/apps/templates:
|
||||
get:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Get all app templates
|
||||
@@ -1510,7 +1510,7 @@ paths:
|
||||
items:
|
||||
$ref: '#/components/schemas/AppTemplate'
|
||||
|
||||
/api/apps/templates/{appId}:
|
||||
/api/v1/apps/templates/{appId}:
|
||||
get:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Get specific template
|
||||
@@ -1533,7 +1533,7 @@ paths:
|
||||
template:
|
||||
$ref: '#/components/schemas/AppTemplate'
|
||||
|
||||
/api/apps/check-port/{port}:
|
||||
/api/v1/apps/check-port/{port}:
|
||||
get:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Check port availability
|
||||
@@ -1556,7 +1556,7 @@ paths:
|
||||
available:
|
||||
type: boolean
|
||||
|
||||
/api/apps/suggest-port/{basePort}:
|
||||
/api/v1/apps/suggest-port/{basePort}:
|
||||
get:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Suggest next available port
|
||||
@@ -1579,7 +1579,7 @@ paths:
|
||||
port:
|
||||
type: integer
|
||||
|
||||
/api/apps/check-existing:
|
||||
/api/v1/apps/check-existing:
|
||||
post:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Check if app deployed
|
||||
@@ -1605,7 +1605,7 @@ paths:
|
||||
exists:
|
||||
type: boolean
|
||||
|
||||
/api/apps/deploy:
|
||||
/api/v1/apps/deploy:
|
||||
post:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Deploy Docker app
|
||||
@@ -1646,7 +1646,7 @@ paths:
|
||||
url:
|
||||
type: string
|
||||
|
||||
/api/apps/{appId}:
|
||||
/api/v1/apps/{appId}:
|
||||
delete:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Delete deployed app
|
||||
@@ -1664,7 +1664,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/apps/update-subdomain:
|
||||
/api/v1/apps/update-subdomain:
|
||||
post:
|
||||
tags: [Docker App Deployment]
|
||||
summary: Update app subdomain
|
||||
@@ -1689,7 +1689,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Container Management
|
||||
/api/containers/{id}/start:
|
||||
/api/v1/containers/{id}/start:
|
||||
post:
|
||||
tags: [Container Management]
|
||||
summary: Start container
|
||||
@@ -1707,7 +1707,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/containers/{id}/stop:
|
||||
/api/v1/containers/{id}/stop:
|
||||
post:
|
||||
tags: [Container Management]
|
||||
summary: Stop container
|
||||
@@ -1725,7 +1725,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/containers/{id}/restart:
|
||||
/api/v1/containers/{id}/restart:
|
||||
post:
|
||||
tags: [Container Management]
|
||||
summary: Restart container
|
||||
@@ -1743,7 +1743,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/containers/{id}/update:
|
||||
/api/v1/containers/{id}/update:
|
||||
post:
|
||||
tags: [Container Management]
|
||||
summary: Update container image
|
||||
@@ -1761,7 +1761,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/containers/{id}/check-update:
|
||||
/api/v1/containers/{id}/check-update:
|
||||
get:
|
||||
tags: [Container Management]
|
||||
summary: Check for container updates
|
||||
@@ -1784,7 +1784,7 @@ paths:
|
||||
updateAvailable:
|
||||
type: boolean
|
||||
|
||||
/api/containers/{id}/logs:
|
||||
/api/v1/containers/{id}/logs:
|
||||
get:
|
||||
tags: [Container Management]
|
||||
summary: Get container logs
|
||||
@@ -1815,7 +1815,7 @@ paths:
|
||||
logs:
|
||||
type: string
|
||||
|
||||
/api/containers/{id}:
|
||||
/api/v1/containers/{id}:
|
||||
delete:
|
||||
tags: [Container Management]
|
||||
summary: Delete container
|
||||
@@ -1833,7 +1833,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/containers/discover:
|
||||
/api/v1/containers/discover:
|
||||
get:
|
||||
tags: [Container Management]
|
||||
summary: Discover unmanaged containers
|
||||
@@ -1853,7 +1853,7 @@ paths:
|
||||
type: object
|
||||
|
||||
# Notifications
|
||||
/api/notifications/config:
|
||||
/api/v1/notifications/config:
|
||||
get:
|
||||
tags: [Notifications]
|
||||
summary: Get notification config
|
||||
@@ -1891,7 +1891,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/notifications/test:
|
||||
/api/v1/notifications/test:
|
||||
post:
|
||||
tags: [Notifications]
|
||||
summary: Send test notification
|
||||
@@ -1903,7 +1903,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/notifications/history:
|
||||
/api/v1/notifications/history:
|
||||
get:
|
||||
tags: [Notifications]
|
||||
summary: Get notification history
|
||||
@@ -1932,7 +1932,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/notifications/health-check:
|
||||
/api/v1/notifications/health-check:
|
||||
post:
|
||||
tags: [Notifications]
|
||||
summary: Trigger health check notification
|
||||
@@ -1945,7 +1945,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Container Stats & Logs
|
||||
/api/stats/containers:
|
||||
/api/v1/stats/containers:
|
||||
get:
|
||||
tags: [Container Stats & Logs]
|
||||
summary: Get all container stats
|
||||
@@ -1964,7 +1964,7 @@ paths:
|
||||
items:
|
||||
$ref: '#/components/schemas/ContainerStats'
|
||||
|
||||
/api/stats/container/{id}:
|
||||
/api/v1/stats/container/{id}:
|
||||
get:
|
||||
tags: [Container Stats & Logs]
|
||||
summary: Get specific container stats
|
||||
@@ -1987,7 +1987,7 @@ paths:
|
||||
stats:
|
||||
$ref: '#/components/schemas/ContainerStats'
|
||||
|
||||
/api/logs/containers:
|
||||
/api/v1/logs/containers:
|
||||
get:
|
||||
tags: [Container Stats & Logs]
|
||||
summary: List containers with logs
|
||||
@@ -2006,7 +2006,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/logs/container/{id}:
|
||||
/api/v1/logs/container/{id}:
|
||||
get:
|
||||
tags: [Container Stats & Logs]
|
||||
summary: Get container log entries
|
||||
@@ -2031,7 +2031,7 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
|
||||
/api/logs/stream/{id}:
|
||||
/api/v1/logs/stream/{id}:
|
||||
get:
|
||||
tags: [Container Stats & Logs]
|
||||
summary: Stream container logs (SSE)
|
||||
@@ -2049,7 +2049,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/api/logs/file:
|
||||
/api/v1/logs/file:
|
||||
get:
|
||||
tags: [Container Stats & Logs]
|
||||
summary: Read native log file
|
||||
@@ -2077,7 +2077,7 @@ paths:
|
||||
type: string
|
||||
|
||||
# Service Health
|
||||
/api/health/services:
|
||||
/api/v1/health/services:
|
||||
get:
|
||||
tags: [Service Health]
|
||||
summary: Full health check for all services
|
||||
@@ -2096,7 +2096,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/health/cached:
|
||||
/api/v1/health/cached:
|
||||
get:
|
||||
tags: [Service Health]
|
||||
summary: Cached health results
|
||||
@@ -2113,7 +2113,7 @@ paths:
|
||||
cached:
|
||||
type: object
|
||||
|
||||
/api/health/service/{id}:
|
||||
/api/v1/health/service/{id}:
|
||||
get:
|
||||
tags: [Service Health]
|
||||
summary: Health for specific service
|
||||
@@ -2137,7 +2137,7 @@ paths:
|
||||
type: boolean
|
||||
|
||||
# Resource Monitoring
|
||||
/api/monitoring/stats:
|
||||
/api/v1/monitoring/stats:
|
||||
get:
|
||||
tags: [Resource Monitoring]
|
||||
summary: All container resource stats
|
||||
@@ -2156,7 +2156,7 @@ paths:
|
||||
items:
|
||||
$ref: '#/components/schemas/ContainerStats'
|
||||
|
||||
/api/monitoring/stats/{containerId}:
|
||||
/api/v1/monitoring/stats/{containerId}:
|
||||
get:
|
||||
tags: [Resource Monitoring]
|
||||
summary: Specific container stats
|
||||
@@ -2179,7 +2179,7 @@ paths:
|
||||
stats:
|
||||
$ref: '#/components/schemas/ContainerStats'
|
||||
|
||||
/api/monitoring/history/{containerId}:
|
||||
/api/v1/monitoring/history/{containerId}:
|
||||
get:
|
||||
tags: [Resource Monitoring]
|
||||
summary: Historical stats
|
||||
@@ -2208,7 +2208,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/monitoring/aggregated/{containerId}:
|
||||
/api/v1/monitoring/aggregated/{containerId}:
|
||||
get:
|
||||
tags: [Resource Monitoring]
|
||||
summary: Aggregated stats
|
||||
@@ -2235,7 +2235,7 @@ paths:
|
||||
aggregated:
|
||||
type: object
|
||||
|
||||
/api/monitoring/alerts/{containerId}:
|
||||
/api/v1/monitoring/alerts/{containerId}:
|
||||
post:
|
||||
tags: [Resource Monitoring]
|
||||
summary: Configure alerts
|
||||
@@ -2304,7 +2304,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Automated Backups
|
||||
/api/backups/config:
|
||||
/api/v1/backups/config:
|
||||
get:
|
||||
tags: [Automated Backups]
|
||||
summary: Get backup config
|
||||
@@ -2344,7 +2344,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/backups/execute:
|
||||
/api/v1/backups/execute:
|
||||
post:
|
||||
tags: [Automated Backups]
|
||||
summary: Run manual backup
|
||||
@@ -2361,7 +2361,7 @@ paths:
|
||||
backupId:
|
||||
type: string
|
||||
|
||||
/api/backups/history:
|
||||
/api/v1/backups/history:
|
||||
get:
|
||||
tags: [Automated Backups]
|
||||
summary: Get backup history
|
||||
@@ -2380,7 +2380,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/backups/restore/{backupId}:
|
||||
/api/v1/backups/restore/{backupId}:
|
||||
post:
|
||||
tags: [Automated Backups]
|
||||
summary: Restore from backup
|
||||
@@ -2399,7 +2399,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Health Checks
|
||||
/api/health-check/status:
|
||||
/api/v1/health-check/status:
|
||||
get:
|
||||
tags: [Health Checks]
|
||||
summary: All service health status
|
||||
@@ -2418,7 +2418,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/health-check/stats/{serviceId}:
|
||||
/api/v1/health-check/stats/{serviceId}:
|
||||
get:
|
||||
tags: [Health Checks]
|
||||
summary: Detailed service stats
|
||||
@@ -2445,7 +2445,7 @@ paths:
|
||||
stats:
|
||||
type: object
|
||||
|
||||
/api/health-check/configure/{serviceId}:
|
||||
/api/v1/health-check/configure/{serviceId}:
|
||||
post:
|
||||
tags: [Health Checks]
|
||||
summary: Configure health check
|
||||
@@ -2492,7 +2492,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/health-check/incidents:
|
||||
/api/v1/health-check/incidents:
|
||||
get:
|
||||
tags: [Health Checks]
|
||||
summary: Open incidents
|
||||
@@ -2511,7 +2511,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/health-check/incidents/history:
|
||||
/api/v1/health-check/incidents/history:
|
||||
get:
|
||||
tags: [Health Checks]
|
||||
summary: Incident history
|
||||
@@ -2536,7 +2536,7 @@ paths:
|
||||
type: object
|
||||
|
||||
# Update Management
|
||||
/api/updates/check:
|
||||
/api/v1/updates/check:
|
||||
post:
|
||||
tags: [Update Management]
|
||||
summary: Check for updates
|
||||
@@ -2555,7 +2555,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/updates/available:
|
||||
/api/v1/updates/available:
|
||||
get:
|
||||
tags: [Update Management]
|
||||
summary: Get available updates
|
||||
@@ -2574,7 +2574,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/updates/update/{containerId}:
|
||||
/api/v1/updates/update/{containerId}:
|
||||
post:
|
||||
tags: [Update Management]
|
||||
summary: Update container
|
||||
@@ -2592,7 +2592,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/updates/rollback/{containerId}:
|
||||
/api/v1/updates/rollback/{containerId}:
|
||||
post:
|
||||
tags: [Update Management]
|
||||
summary: Rollback container
|
||||
@@ -2610,7 +2610,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/updates/history:
|
||||
/api/v1/updates/history:
|
||||
get:
|
||||
tags: [Update Management]
|
||||
summary: Get update history
|
||||
@@ -2634,7 +2634,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
|
||||
/api/updates/auto-update/{containerId}:
|
||||
/api/v1/updates/auto-update/{containerId}:
|
||||
post:
|
||||
tags: [Update Management]
|
||||
summary: Configure auto-update
|
||||
@@ -2661,7 +2661,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
/api/updates/schedule/{containerId}:
|
||||
/api/v1/updates/schedule/{containerId}:
|
||||
post:
|
||||
tags: [Update Management]
|
||||
summary: Schedule update
|
||||
@@ -2690,7 +2690,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Error Logs
|
||||
/api/error-logs:
|
||||
/api/v1/error-logs:
|
||||
get:
|
||||
tags: [Error Logs]
|
||||
summary: View error logs
|
||||
@@ -2720,7 +2720,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# Filesystem Browser
|
||||
/api/browse/roots:
|
||||
/api/v1/browse/roots:
|
||||
get:
|
||||
tags: [Filesystem Browser]
|
||||
summary: Get browseable roots
|
||||
@@ -2739,7 +2739,7 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
|
||||
/api/browse/dir:
|
||||
/api/v1/browse/dir:
|
||||
get:
|
||||
tags: [Filesystem Browser]
|
||||
summary: Browse directory
|
||||
@@ -2771,7 +2771,7 @@ paths:
|
||||
size:
|
||||
type: integer
|
||||
|
||||
/api/media/detected-mounts:
|
||||
/api/v1/media/detected-mounts:
|
||||
get:
|
||||
tags: [Filesystem Browser]
|
||||
summary: Detect media mounts
|
||||
@@ -2791,7 +2791,7 @@ paths:
|
||||
type: object
|
||||
|
||||
# Audit Log
|
||||
/api/audit-log:
|
||||
/api/v1/audit-log:
|
||||
get:
|
||||
tags: [Audit Log]
|
||||
summary: Query audit log
|
||||
@@ -2844,7 +2844,7 @@ paths:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
|
||||
# API Documentation
|
||||
/api/docs:
|
||||
/api/v1/docs:
|
||||
get:
|
||||
tags: [API Documentation]
|
||||
summary: API docs UI
|
||||
@@ -2856,7 +2856,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/api/docs/spec:
|
||||
/api/v1/docs/spec:
|
||||
get:
|
||||
tags: [API Documentation]
|
||||
summary: OpenAPI spec
|
||||
|
||||
@@ -146,7 +146,7 @@ module.exports = function({ backupManager, asyncHandler }) {
|
||||
};
|
||||
|
||||
for (const k of keys[provider]) {
|
||||
try { await credentialManager.delete(k); } catch (_) {}
|
||||
try { await credentialManager.delete(k); } catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
success(res, { message: `${provider} credentials deleted` });
|
||||
|
||||
@@ -446,9 +446,8 @@ async function createApp() {
|
||||
res.json({ success: true, metrics: metrics.getSummary() });
|
||||
});
|
||||
|
||||
// Mount at /api/v1 (canonical) and /api (legacy)
|
||||
// Mount at /api/v1 (canonical, single version)
|
||||
app.use('/api/v1', apiRouter);
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
// Root-level health check
|
||||
app.get('/health', (req, res) => {
|
||||
@@ -548,7 +547,7 @@ async function createApp() {
|
||||
}, 'probe'));
|
||||
|
||||
// Network IPs endpoint
|
||||
app.get('/api/network/ips', (req, res) => {
|
||||
app.get('/api/v1/network/ips', (req, res) => {
|
||||
try {
|
||||
const os = require('os');
|
||||
const envLan = process.env.HOST_LAN_IP;
|
||||
@@ -585,7 +584,7 @@ async function createApp() {
|
||||
});
|
||||
|
||||
// API Documentation
|
||||
app.get('/api/docs', (req, res) => {
|
||||
app.get('/api/v1/docs', (req, res) => {
|
||||
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data: https://unpkg.com;");
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -598,12 +597,12 @@ async function createApp() {
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script>SwaggerUIBundle({url:'/api/docs/spec',dom_id:'#swagger-ui',deepLinking:true})</script>
|
||||
<script>SwaggerUIBundle({url:'/api/v1/docs/spec',dom_id:'#swagger-ui',deepLinking:true})</script>
|
||||
</body>
|
||||
</html>`);
|
||||
});
|
||||
|
||||
app.get('/api/docs/spec', boundAsyncHandler(async (req, res) => {
|
||||
app.get('/api/v1/docs/spec', boundAsyncHandler(async (req, res) => {
|
||||
const path = require('path');
|
||||
const { exists } = require('../fs-helpers');
|
||||
const fsp = require('fs').promises;
|
||||
|
||||
Reference in New Issue
Block a user