diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
new file mode 100644
index 0000000..25c62f8
--- /dev/null
+++ b/.gitea/workflows/ci.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2478a03
--- /dev/null
+++ b/CHANGELOG.md
@@ -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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1992fdb
--- /dev/null
+++ b/LICENSE
@@ -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
diff --git a/dashcaddy-api/__tests__/csrf-protection.test.js b/dashcaddy-api/__tests__/csrf-protection.test.js
index f36b002..4943d84 100644
--- a/dashcaddy-api/__tests__/csrf-protection.test.js
+++ b/dashcaddy-api/__tests__/csrf-protection.test.js
@@ -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';
diff --git a/dashcaddy-api/audit-logger.js b/dashcaddy-api/audit-logger.js
index 3fdf5b8..da05fc4 100644
--- a/dashcaddy-api/audit-logger.js
+++ b/dashcaddy-api/audit-logger.js
@@ -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] || '';
}
diff --git a/dashcaddy-api/backup-manager.js b/dashcaddy-api/backup-manager.js
index 1dd8082..e1eb40c 100644
--- a/dashcaddy-api/backup-manager.js
+++ b/dashcaddy-api/backup-manager.js
@@ -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;
}
diff --git a/dashcaddy-api/ca/CERTIFICATE-API.md b/dashcaddy-api/ca/CERTIFICATE-API.md
index fb5452a..a1d6e39 100644
--- a/dashcaddy-api/ca/CERTIFICATE-API.md
+++ b/dashcaddy-api/ca/CERTIFICATE-API.md
@@ -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.
diff --git a/dashcaddy-api/ca/README.md b/dashcaddy-api/ca/README.md
index aa09881..c31005b 100644
--- a/dashcaddy-api/ca/README.md
+++ b/dashcaddy-api/ca/README.md
@@ -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
diff --git a/dashcaddy-api/ca/index.html b/dashcaddy-api/ca/index.html
index 3d83d78..c3c8228 100644
--- a/dashcaddy-api/ca/index.html
+++ b/dashcaddy-api/ca/index.html
@@ -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 @@
Fingerprint:
${cert.fingerprint}
-
Download PFX
-
PEM
-
CRT
diff --git a/dashcaddy-api/csrf-protection.js b/dashcaddy-api/csrf-protection.js
index 089a826..1f4c465 100644
--- a/dashcaddy-api/csrf-protection.js
+++ b/dashcaddy-api/csrf-protection.js
@@ -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();
diff --git a/dashcaddy-api/middleware.js b/dashcaddy-api/middleware.js
index eb9c8ba..646def4 100644
--- a/dashcaddy-api/middleware.js
+++ b/dashcaddy-api/middleware.js
@@ -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());
diff --git a/dashcaddy-api/openapi.yaml b/dashcaddy-api/openapi.yaml
index c7c2019..e2360ea 100644
--- a/dashcaddy-api/openapi.yaml
+++ b/dashcaddy-api/openapi.yaml
@@ -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
diff --git a/dashcaddy-api/routes/backups.js b/dashcaddy-api/routes/backups.js
index 739c013..3f7208c 100644
--- a/dashcaddy-api/routes/backups.js
+++ b/dashcaddy-api/routes/backups.js
@@ -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` });
diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js
index 55daf46..cb7f5e8 100644
--- a/dashcaddy-api/src/app.js
+++ b/dashcaddy-api/src/app.js
@@ -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(`
@@ -598,12 +597,12 @@ async function createApp() {
-
+
`);
});
- 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;
diff --git a/status/api/caddy-api.js b/status/api/caddy-api.js
index c09b6e4..23e44cc 100644
--- a/status/api/caddy-api.js
+++ b/status/api/caddy-api.js
@@ -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',
diff --git a/status/api/test-api.js b/status/api/test-api.js
index ad5affb..a436db2 100644
--- a/status/api/test-api.js
+++ b/status/api/test-api.js
@@ -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');
diff --git a/status/index.html b/status/index.html
index b4f3910..a1618f9 100644
--- a/status/index.html
+++ b/status/index.html
@@ -793,7 +793,7 @@
if (apiDocsBtn) {
apiDocsBtn.addEventListener('click', function() {
- window.open('/api/docs', '_blank');
+ window.open('/api/v1/docs', '_blank');
});
}
diff --git a/status/sw.js b/status/sw.js
index ffaa006..304bebc 100644
--- a/status/sw.js
+++ b/status/sw.js
@@ -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));