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));