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:
Sami
2026-05-17 11:38:45 -07:00
co-authored by Claude Opus 4.7
parent cd8ccaba2b
commit d36705bd90
18 changed files with 663 additions and 252 deletions
+71
View File
@@ -0,0 +1,71 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
name: Test & Lint
runs-on: ubuntu-latest
defaults:
run:
working-directory: dashcaddy-api
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: dashcaddy-api/package-lock.json
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test (CI mode + coverage)
run: npm run test:ci
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v3
with:
name: coverage-${{ github.sha }}
path: dashcaddy-api/coverage/
retention-days: 14
security:
name: Security audit
runs-on: ubuntu-latest
defaults:
run:
working-directory: dashcaddy-api
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: dashcaddy-api/package-lock.json
- name: Install dependencies
run: npm ci
- name: npm audit (production deps, high+ severity)
run: npm audit --production --audit-level=high
continue-on-error: true
- name: Run security-focused test suite
run: npm run test:security
+220
View File
@@ -0,0 +1,220 @@
# Changelog
All notable changes to DashCaddy are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.5.0] - 2026-05-17
### Changed (BREAKING)
- API routes now mounted exclusively under `/api/v1/`. The legacy un-versioned
`/api/` mount has been removed. Frontend, OpenAPI spec, DashCA pages, and
all internal path matchers (CSRF exclusions, auth public routes, audit log,
rate-limit mounts) updated accordingly. **Existing integrations that hit
`/api/...` directly must update to `/api/v1/...`.** Held at minor bump
(1.5.0) rather than major (2.0.0) — DashCaddy is still pre-1.0-API-stable.
### Added
- `LICENSE` (proprietary EULA) at repo root.
- `CHANGELOG.md` (this file) — Keep a Changelog format.
- Gitea Actions workflow ([.gitea/workflows/ci.yml](.gitea/workflows/ci.yml))
that runs `npm test` (with coverage) and `npm run lint` on every push to
`main`/`master` and on PRs, plus a `security` job running `npm audit` and
the security-focused test subset.
### Fixed
- 9 pre-existing `no-empty` ESLint errors in `backup-manager.js` and
`routes/backups.js` (intentional ignore-failure catches now annotated).
### Removed
- Stale files at repo root: `*.bak`, `server-old.js`, and ad-hoc
deployment/migration/test reports (`DEPLOYMENT-SUCCESS.md`,
`FINAL-DEPLOYMENT-REPORT.md`, `DESLOPIFICATION-ROADMAP.md`,
`error-handling-*.md`, `WHAT-IS-DASHCADDY.md`, etc.). Already gitignored —
disk-only cleanup.
---
## [1.4.10] - 2026-05-17
### Fixed
- `release.sh` now stages build-rewritten files (`sw.js`, `index.html`) so
they're included in the published tarball.
## [1.4.9] - 2026-05-17
### Fixed
- Container-logs feature was misusing `wireModal`, which short-circuited the
rest of `features.js` and broke unrelated dashboard features.
## [1.4.8] - 2026-05-17
### Fixed
- CSP hash now computed on LF-normalized `index.html` so Windows and Linux
builds produce identical hashes.
## [1.4.7] - 2026-05-17
### Fixed
- Dashboard unbroken: corrected bundle order, closed dangling IIFE, removed
duplicate `const` declaration.
## [1.4.6] - 2026-05-17
### Fixed
- `sw.js` cache tag now derived from bundle content hash, so service worker
invalidates correctly when bundle content changes.
## [1.4.5] - 2026-05-17
### Fixed
- Frontend deploy routed through the host-side updater (matches the API
container's own update path).
## [1.4.4] - 2026-05-16
### Fixed
- `notify` endpoint exempted from CSRF (it's called by the host-side updater,
not the browser).
- `release.sh` JSON parsing made portable (no longer assumes GNU `jq`
semantics on every host).
## [1.4.3] - 2026-05-16
### Added
- Seamless release flow: push-notify endpoint, VERSION file copy into
release tarball, robust SSH mirror handling on port 22022.
## [1.4.2] - 2026-05-16
## [1.4.1] - 2026-05-16
### Changed
- Version bump only — packaging plumbing for the 1.4.x release line.
## [1.4.0] - 2026-05-06
### Added
- `scripts/release.sh` — one-command release cutting and publishing.
---
## [1.3.1] - 2026-05-06
### Fixed
- Installer: added `src/` directory to the deploy manifest; dropped
`MakeDirectory=yes` from the systemd updater path unit.
- Self-updater: copies `src/`, replaces `routes/` in place instead of
nesting it inside the existing tree.
## [1.3.0] - 2026-05-06
### Added
- Self-updater supports `DASHCADDY_API_SOURCE_DIR` env override for
non-standard deploy layouts.
### Fixed
- Self-updater now clears *all* pending history entries, not just one.
---
## [1.2.0] - 2026-05-14
### Added
- Container Log Viewer with streaming, search, and download.
- Service filter, batch operations across multiple services, and snapshot
capture.
- Auto CSP hash updates during build.
- Dashboard version button and self-update UI wiring.
- Release policy checks and dashboard version verification.
### Changed
- All routine `console.log` calls gated behind `window.DASHCADDY_DEBUG`
flag for quieter production output.
- All `console.error` calls routed through `ErrorHandler` for consistent
tracking.
### Fixed
- Updater no longer triggers a false-positive "update available" loop
when commit hash is unknown.
---
## [1.1.5] - 2026-03-23
### Added
- Pylon health relay for remote service health checks (with relay
fallback on `/probe/:id`).
- Host-side auto-updater for zero-touch API container rebuilds.
### Fixed
- Service edit preserves service ID on subdomain change; accepts
`localhost` as a valid IP.
- Taxi theme accent color now distinct from text.
- Prevents encryption key conflicts; adds license backup on rotation.
## [1.1.1] - 2026-03-23
### Fixed
- Service edit, CSRF token stability, and license restore.
---
## [1.0.x] - 2026-03-05 → 2026-03-22
Initial release line. Highlights from work between v1.0 and v1.1:
### Added
- Cross-platform path support (Windows + Linux deployments).
- Subdirectory routing mode for public-domain deployments.
- Auto-update system for DashCaddy instances.
- Batched status endpoint (frontend performance).
- Install-wide onboarding tour (no longer per-browser).
- Daily log digest and Docker hygiene/maintenance.
- Unified backup/restore v2.0 with full state capture.
- DNS uptime bars and fully-dynamic DNS server config.
### Changed
- Phase 1-3 refactor: extracted config/context/utils into `src/`, split
monolithic `server.js`, standardized all 25+ route files with explicit
dependency injection.
- Unified error handling system (throw-based, migrated 25 route files).
- ESLint + Prettier baseline with auto-fixes.
### Security
- 7 critical + 16 high/medium API security bugs fixed.
- 7 frontend security vulnerabilities fixed (4 critical, 3 high).
- Logger sanitization to prevent log injection.
### Tests
- Comprehensive test suite reaching 80%+ coverage threshold.
- `docker-security` test suite (41 tests).
- `auth-manager` and `credential-manager` test suites.
## [1.0.0] - 2026-03-05
Initial release of DashCaddy. Unified dashboard for Docker container
management, Caddy reverse proxy configuration, DNS automation, and SSL
certificate provisioning.
[Unreleased]: ../../compare/v1.5.0...HEAD
[1.5.0]: ../../compare/v1.4.10...v1.5.0
[1.4.10]: ../../compare/v1.4.9...v1.4.10
[1.4.9]: ../../compare/v1.4.8...v1.4.9
[1.4.8]: ../../compare/v1.4.7...v1.4.8
[1.4.7]: ../../compare/v1.4.6...v1.4.7
[1.4.6]: ../../compare/v1.4.5...v1.4.6
[1.4.5]: ../../compare/v1.4.4...v1.4.5
[1.4.4]: ../../compare/v1.4.3...v1.4.4
[1.4.3]: ../../compare/v1.4.2...v1.4.3
[1.4.2]: ../../compare/v1.4.1...v1.4.2
[1.4.1]: ../../compare/v1.4.0...v1.4.1
[1.4.0]: ../../compare/v1.3.1...v1.4.0
[1.3.1]: ../../compare/v1.3.0...v1.3.1
[1.3.0]: ../../compare/v1.2.0...v1.3.0
[1.2.0]: ../../compare/v1.1.5...v1.2.0
[1.1.5]: ../../compare/v1.1.1...v1.1.5
[1.1.1]: ../../compare/v1.0.0...v1.1.1
[1.0.0]: ../../releases/tag/v1.0.0
+125
View File
@@ -0,0 +1,125 @@
DashCaddy End-User License Agreement (EULA)
=============================================
Copyright (c) 2024-2026 Sami Ahmed. All rights reserved.
This software and its source code (the "Software") are proprietary and
confidential. By installing, copying, accessing, or otherwise using the
Software, you ("Licensee") agree to be bound by the terms of this License.
If you do not agree, do not install, copy, or use the Software.
1. GRANT OF LICENSE
-------------------
Subject to the terms of this License and the purchase of a valid license
key where required, Licensor grants Licensee a non-exclusive,
non-transferable, revocable license to install and use the Software on
hardware that Licensee owns or controls, solely for Licensee's internal
purposes.
A separate license key is required for each production deployment. Use
of the Software without a valid license key is permitted only for
personal, non-commercial evaluation on a single host, for up to 30 days.
2. RESTRICTIONS
---------------
Licensee shall NOT:
(a) sell, rent, lease, sublicense, distribute, publish, or otherwise
transfer the Software or any portion thereof to any third party;
(b) modify, adapt, translate, or create derivative works based on the
Software, except as expressly permitted in Section 3;
(c) reverse engineer, decompile, or disassemble the Software, except
to the extent that such activity is expressly permitted by
applicable law notwithstanding this limitation;
(d) remove, alter, or obscure any copyright, trademark, or other
proprietary notices contained in the Software;
(e) use the Software to operate a hosted or managed service that
makes the Software's functionality available to third parties,
without a separate commercial agreement with Licensor;
(f) use the Software in any manner that violates applicable law.
3. SOURCE AVAILABILITY
----------------------
The Software's source code is made available for the purposes of
transparency, security review, and self-hosted deployment. Source
availability does NOT constitute a grant of open-source rights.
Modifications made by Licensee for internal use only are permitted,
provided they are not redistributed.
4. OWNERSHIP
------------
The Software is licensed, not sold. Licensor retains all right, title,
and interest in and to the Software, including all intellectual property
rights therein. No rights are granted to Licensee other than those
expressly set forth in this License.
5. UPDATES
----------
Licensor may, at its sole discretion, provide updates, patches, or new
versions of the Software. Any such updates are subject to the terms of
this License unless accompanied by a separate license agreement.
6. TERMINATION
--------------
This License is effective until terminated. Licensor may terminate this
License immediately upon any breach by Licensee. Upon termination,
Licensee shall cease all use of the Software and destroy all copies in
its possession or control.
7. WARRANTY DISCLAIMER
----------------------
THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
TITLE, AND NON-INFRINGEMENT. LICENSEE BEARS THE ENTIRE RISK ARISING
OUT OF THE USE OR PERFORMANCE OF THE SOFTWARE.
8. LIMITATION OF LIABILITY
--------------------------
IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF
PROFITS, REVENUE, DATA, OR USE, ARISING OUT OF OR RELATED TO THIS
LICENSE OR THE SOFTWARE, EVEN IF LICENSOR HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES. LICENSOR'S TOTAL CUMULATIVE LIABILITY
SHALL NOT EXCEED THE AMOUNT PAID BY LICENSEE FOR THE SOFTWARE IN THE
TWELVE (12) MONTHS PRECEDING THE EVENT GIVING RISE TO LIABILITY, OR
ONE HUNDRED U.S. DOLLARS (USD $100), WHICHEVER IS GREATER.
9. THIRD-PARTY COMPONENTS
-------------------------
The Software incorporates third-party open-source components, each
governed by its own license. A list of such components and their
licenses is available in the project's `node_modules/` directory or
on request. This License does not modify the terms of any third-party
component license.
10. GOVERNING LAW
-----------------
This License shall be governed by and construed in accordance with the
laws of the jurisdiction in which Licensor resides, without regard to
its conflict of laws principles.
11. ENTIRE AGREEMENT
--------------------
This License constitutes the entire agreement between the parties with
respect to the Software and supersedes all prior or contemporaneous
understandings, whether written or oral.
For licensing inquiries, contact: ahmed.sami@gmail.com
@@ -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';
+56 -56
View File
@@ -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] || '';
}
+8 -8
View File
@@ -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;
}
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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
+7 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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` });
+5 -6
View File
@@ -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;
+5 -5
View File
@@ -59,7 +59,7 @@ async function makeRequest(url, options = {}) {
}
// Get current Caddy configuration
app.get('/api/caddy/config', async (req, res) => {
app.get('/api/v1/caddy/config', async (req, res) => {
try {
const response = await makeRequest(`${CADDY_ADMIN_API}/config/`);
@@ -85,7 +85,7 @@ app.get('/api/caddy/config', async (req, res) => {
});
// Get list of services (from apps.json + custom apps)
app.get('/api/services', async (req, res) => {
app.get('/api/v1/services', async (req, res) => {
try {
const servicesPath = path.join(__dirname, '../apps.json');
@@ -184,7 +184,7 @@ async function addCaddyRoute(domain, upstreamUrl, useTls = true) {
}
// Deploy app endpoint - handles DNS and Caddy configuration via APIs
app.post('/api/apps/deploy', async (req, res) => {
app.post('/api/v1/apps/deploy', async (req, res) => {
try {
const { appId, config } = req.body;
const { subdomain, ip, createDns, port, sslType, dnsType } = config;
@@ -264,7 +264,7 @@ app.post('/api/apps/deploy', async (req, res) => {
});
// Delete app endpoint - removes DNS and Caddy configuration
app.post('/api/apps/delete', async (req, res) => {
app.post('/api/v1/apps/delete', async (req, res) => {
try {
const { domain, ip } = req.body;
@@ -324,7 +324,7 @@ app.post('/api/apps/delete', async (req, res) => {
});
// Test endpoint
app.get('/api/caddy/test', (req, res) => {
app.get('/api/v1/caddy/test', (req, res) => {
res.json({
status: 'success',
message: 'Caddy API is running',
+2 -2
View File
@@ -38,7 +38,7 @@ async function runTests() {
// Test 2: API Test Endpoint
console.log('\n2. Testing API test endpoint...');
try {
const test = await makeRequest('/api/caddy/test');
const test = await makeRequest('/api/v1/caddy/test');
if (test.status === 200) {
console.log(' ✓ API test passed');
console.log(' Platform:', test.data.platform);
@@ -55,7 +55,7 @@ async function runTests() {
// Test 3: Services Endpoint
console.log('\n3. Testing services endpoint...');
try {
const services = await makeRequest('/api/services');
const services = await makeRequest('/api/v1/services');
if (services.status === 200) {
console.log(' ✓ Services endpoint passed');
console.log(' Found', services.data.services.length, 'services');
+1 -1
View File
@@ -793,7 +793,7 @@
if (apiDocsBtn) {
apiDocsBtn.addEventListener('click', function() {
window.open('/api/docs', '_blank');
window.open('/api/v1/docs', '_blank');
});
}
+1 -1
View File
@@ -105,7 +105,7 @@ self.addEventListener('fetch', (event) => {
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/probe/')) return;
if (url.pathname.startsWith('/api/v1/') || url.pathname.startsWith('/probe/')) return;
if (isNavigationRequest(request) || url.pathname === '/' || url.pathname.endsWith('/index.html')) {
event.respondWith(networkFirst(request, event.preloadResponse));