From a2e656695882f84c51979d3f45c7805718d1c15d Mon Sep 17 00:00:00 2001 From: Sami Date: Sun, 28 Jun 2026 03:38:31 -0700 Subject: [PATCH] refactor(desloppify): SSO login-page route, CLAUDE.md rewrite, gitignore cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sso-gate.js: add GET /api/v1/auth/login-page?service= route; auto-login HTML for chat/plex/jellyfin/emby now served from code instead of inline Caddyfile respond blobs. Fix merge() try-block syntax error (was missing closing } before catch, breaking Jellyfin/Emby localStorage merge). - middleware.js: add /api/v1/auth/login-page to PUBLIC_ROUTES. - CLAUDE.md: complete rewrite — was describing the old Windows-local C:/caddy/ layout; now accurately describes DNS2 production (paths, container, caddy-apply workflow, SSO architecture, common mistakes). - .gitignore: cover runtime JSON/log/cert files that were sitting untracked in dev root (audit-log, backup-history, credentials, health-history, etc.), plus generated-certs/, pki/, assets/. - Remove tracked dev-root noise: comprehensive-test.js, license-keygen.js, test-security-fixes.js (scripts that don't belong at repo root). - Remove stale routes/openclaw.js (leftover from old monolithic structure). Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 279 ++++++++----------- dashcaddy-api/.gitignore | 21 ++ dashcaddy-api/license-keygen.js | 313 ---------------------- dashcaddy-api/routes/auth/sso-gate.js | 68 +++++ dashcaddy-api/src/utilities/middleware.js | 1 + 5 files changed, 196 insertions(+), 486 deletions(-) delete mode 100644 dashcaddy-api/license-keygen.js diff --git a/CLAUDE.md b/CLAUDE.md index 86bbd11..e2d748c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,217 +10,149 @@ - When deploying new containers, always use `E:/dockerdata//` for bind mount paths - For CIFS volumes in docker-compose, use `//Sami-pc/e_share/dockerdata/...` as the device path -## CRITICAL: Production vs Development Paths +## CRITICAL: Production is on DNS2 (not this machine) + +DashCaddy runs on **DNS2** (`100.121.150.22` via Tailscale / `194.233.88.206` public). +SSH in with: `ssh root@100.121.150.22` + +### Production Layout on DNS2 -### Production Files (LIVE - what actually runs) ``` -C:/caddy/ -├── Caddyfile # Active Caddy configuration -├── services.json # Services shown on dashboard -├── dns-credentials.json # DNS API credentials -├── config.json # DashCaddy configuration -└── sites/ - └── status/ # Dashboard frontend files - └── assets/ # Logos, fonts, icons +/opt/dashcaddy/ # git repo (auto-updated) +├── dashcaddy-api/ +│ ├── *.js # API server source +│ └── data/ +│ ├── services.json # LIVE services list +│ ├── config.json # LIVE DashCaddy config +│ ├── dns-credentials.json # DNS API credentials +│ └── credentials.json # Encrypted app credentials +├── status/ # Dashboard frontend (built) +│ ├── index.html +│ ├── dist/ # Bundled JS (core/features/onboarding/init) +│ ├── js/ # Source JS (also served statically) +│ ├── css/ +│ └── assets/ +├── ca/ # DashCA static site +├── updates/ # Auto-updater staging + history +└── start.sh # Container launch script (run by @reboot cron) ``` -### Development Files (for editing/testing) +### Docker Container + +- **Name**: `dashcaddy-api` +- **Image**: `dashcaddy-dashcaddy-api:latest` +- **Port**: `127.0.0.1:3001` (Caddy proxies to it) +- **Started by**: `/opt/dashcaddy/start.sh` via root `@reboot` cron + +Key container mounts: +| Container path | Host path | +|---|---| +| `/app/data/` | `/opt/dashcaddy/dashcaddy-api/data/` | +| `/app/assets` | `/opt/dashcaddy/status/assets` | +| `/caddyfile` | `/etc/caddy/Caddyfile` | +| `/app/backups` | `/opt/dashcaddy/backups` | + +### Caddy + +- **Config**: `/etc/caddy/Caddyfile` (git-guarded — edit then run `caddy-apply`) +- **Admin API**: `http://localhost:2019` (NOT 2021) +- **TLS storage**: `/var/lib/caddy/` +- **Static files**: Caddy serves `/opt/dashcaddy/status/` for `status.sami` + +### Development Files (for editing) + ``` e:/CaddyCerts/sites/ -├── caddy-api/ -│ ├── server.js # API server source code -│ ├── app-templates.js # Docker app templates (52+ apps) -│ ├── services.json # DEV ONLY - not used in production! +├── dashcaddy-api/ # API server source (NOT caddy-api/) +│ ├── server.js +│ ├── src/app.js # Express app factory +│ ├── routes/ # Route handlers +│ ├── middleware.js │ └── ... -└── status/ - └── index.html # Dashboard UI source +└── status/ # Dashboard frontend source + ├── index.html # HTML template (~853 lines) + ├── js/ # Source JS modules + ├── css/ + ├── dist/ # Built output (run node build.js) + └── build.js # Build script (uses esbuild) ``` -## Docker Container Mount Points - -The `caddy-api` container mounts production files: - -| Container Path | Host Path (Production) | -|----------------|------------------------| -| `/app/services.json` | `C:/caddy/services.json` | -| `/app/dns-credentials.json` | `C:/caddy/dns-credentials.json` | -| `/caddyfile` | `C:/caddy/Caddyfile` | -| `/app/assets` | `C:/caddy/sites/status/assets` | - ## When Making Changes ### To add/remove services from dashboard: -Edit `C:/caddy/services.json` (NOT e:/CaddyCerts/sites/caddy-api/services.json) +Edit `/opt/dashcaddy/dashcaddy-api/data/services.json` on DNS2 directly, +OR use the dashboard UI at `https://status.sami`. ### To modify Caddy reverse proxy rules: -Edit `C:/caddy/Caddyfile`, then reload via: ```bash -curl -X POST http://localhost:2019/load -H "Content-Type: text/caddyfile" --data-binary @"C:/caddy/Caddyfile" +ssh root@100.121.150.22 +# Edit /etc/caddy/Caddyfile +caddy-apply "reason for change" # validates + reloads + git commits ``` ### To modify API server code: -Edit `e:/CaddyCerts/sites/caddy-api/server.js`, then: -1. Copy to production: `C:/caddy/sites/caddy-api/` -2. Restart container: `docker restart caddy-api` +1. Edit `e:/CaddyCerts/sites/dashcaddy-api/` locally +2. `scp` changed files to `root@100.121.150.22:/opt/dashcaddy/dashcaddy-api/` +3. Rebuild container: `ssh root@100.121.150.22 "bash /opt/dashcaddy/start.sh"` -### To modify app templates: -Edit `e:/CaddyCerts/sites/caddy-api/app-templates.js` -(Templates are loaded at runtime, changes require container restart) +### To modify dashboard frontend: +1. Edit source in `e:/CaddyCerts/sites/status/js/` or `status/index.html` +2. Build: `cd e:/CaddyCerts/sites/status && node build.js` +3. Deploy: `scp -r dist/ index.html sw.js root@100.121.150.22:/opt/dashcaddy/status/` -### To modify dashboard UI: -Edit `e:/CaddyCerts/sites/status/index.html` -Copy to `C:/caddy/sites/status/` for production - -### To modify DashCA (CA certificate distribution): +### To modify DashCA: Edit files in `e:/CaddyCerts/sites/ca/`, then: -1. Regenerate certificate formats: `cd e:/CaddyCerts/sites/ca/scripts && bash generate-all.sh` -2. Copy to production: `cp -r e:/CaddyCerts/sites/ca/* C:/caddy/sites/ca/` -3. Reload Caddy if Caddyfile changes were made +1. Regenerate: `cd e:/CaddyCerts/sites/ca/scripts && bash generate-all.sh` +2. Deploy: `scp -r e:/CaddyCerts/sites/ca/* root@100.121.150.22:/opt/dashcaddy/ca/` ## DashCA - Certificate Authority Distribution -**Purpose**: Provides a one-click installation page for the root CA certificate, allowing users to easily trust *.sami domains on any device. - -**Access**: https://ca.sami (or https://ca.yourdomain for other installations) - -### File Locations - -**Development (for editing):** -``` -e:/CaddyCerts/sites/ca/ -├── index.html # Landing page -├── root.crt, root.der # Certificate formats -├── root.mobileconfig # Apple profile -├── intermediate.crt # Intermediate CA -├── cert-info.json # Certificate metadata -├── scripts/ -│ ├── install.ps1 # Windows installer -│ ├── install.sh # Linux/macOS installer -│ ├── generate-cert-info.js # Extract cert metadata -│ ├── generate-mobileconfig.js # Generate Apple profile -│ └── generate-all.sh # Regenerate all formats -└── assets/ # Icons, logos -``` - -**Production (served by Caddy):** -``` -C:/caddy/sites/ca/ -├── index.html -├── root.crt, root.der -├── root.mobileconfig -├── install.ps1, install.sh -└── assets/ -``` - -### Certificate Source - -Caddy's built-in PKI generates certificates at: -- **Root CA**: `C:/caddy/certs/pki/authorities/local/root.crt` -- **Intermediate CA**: `C:/caddy/certs/pki/authorities/local/intermediate.crt` +**Purpose**: One-click CA cert install page so *.sami domains are trusted on all devices. +**Access**: `https://ca.sami` **Certificate Info:** - **CN**: Sami Home Network Root CA - **Algorithm**: ECDSA P-256 with SHA-256 -- **Valid Until**: Dec 22, 2034 (~10 years) +- **Valid Until**: Dec 22, 2034 - **Fingerprint**: `08:98:A5:63:F5:A1:A2:58:5F:02:D7:A8:A2:54:87:E6:BC:33:96:21:29:0E` -### Deployment - -DashCA is a **static site** (not Docker-based), deployed via the app selector: -1. Navigate to App Selector in dashboard -2. Find "DashCA" in Security category -3. Click Deploy -4. System automatically: - - Creates `C:/caddy/sites/ca/` directory - - Copies files from development directory - - Generates certificate formats (DER, mobileconfig) - - Adds ca.sami block to Caddyfile - - Reloads Caddy configuration - - Registers service in `services.json` - -### Updating Certificates - -When Caddy's CA certificate is renewed (every ~10 years): - -```bash -# 1. Regenerate all certificate formats -cd e:/CaddyCerts/sites/ca/scripts -bash generate-all.sh - -# 2. Update fingerprint in installation scripts -# Edit install.ps1 - update $ExpectedFingerprint -# Edit install.sh - update EXPECTED_FP - -# 3. Copy to production -cp -r e:/CaddyCerts/sites/ca/* C:/caddy/sites/ca/ - -# 4. Notify users via dashboard or email -``` +**Certificate Source** (on DNS2): +- Root CA: `/etc/ssl/sami-ca/root.crt` +- Intermediate CA: auto-generated by Caddy at `/var/lib/caddy/pki/authorities/local/` ### API Endpoints - -- **GET /api/ca/info** - Returns certificate metadata (name, fingerprint, expiration, etc.) -- **GET /api/health/ca** - Returns CA expiration health status - - `healthy`: >90 days remaining - - `warning`: 30-90 days remaining - - `critical`: <30 days remaining - -### Caddyfile Configuration - -DashCA's Caddyfile block (auto-generated on deployment): -- **Root**: `C:/caddy/sites/ca` -- **TLS**: Internal (uses Caddy's local CA) -- **MIME Types**: Proper headers for .crt, .der, .mobileconfig, .ps1, .sh files -- **SPA Fallback**: Rewrites non-file requests to /index.html -- **Cache Control**: Certificates cached for 24h, HTML not cached - -### Supported Platforms - -- **Windows**: PowerShell installer (installs to LocalMachine\Root store) -- **macOS**: .mobileconfig profile or command-line installer -- **Linux**: Shell installer (Debian, RedHat, Arch) -- **iOS**: .mobileconfig profile (requires manual trust in Settings) -- **Android**: Direct .crt download (installs as user certificate) - -### Landing Page Features - -- Automatic OS detection -- QR code for mobile access -- Certificate info display (loaded from `/api/ca/info`) -- Platform-specific installation instructions -- Copy-to-clipboard for fingerprint and commands -- Download links for all certificate formats - -### Troubleshooting - -**Issue**: Certificate fingerprint mismatch during installation -**Cause**: CA certificate was renewed -**Solution**: Regenerate certificates and update fingerprints in install scripts - -**Issue**: *.sami sites still show warnings after CA install -**Cause**: Browser may have cached the untrusted state -**Solution**: Clear browser cache, restart browser, or visit site in incognito mode - -**Issue**: iOS doesn't trust certificate after profile install -**Cause**: iOS requires manual trust enablement -**Solution**: Settings → General → About → Certificate Trust Settings → Enable trust +- `GET /api/ca/info` — certificate metadata +- `GET /api/health/ca` — CA expiration health (`healthy` / `warning` / `critical`) ## Key Services -| Service | Port | Description | -|---------|------|-------------| -| Caddy (HTTPS) | 443 | Reverse proxy | -| Caddy Admin | 2019 | Caddy API (note: NOT 2021) | -| DashCaddy API | 3001 | Dashboard backend | -| DNS2 (Primary) | 100.74.102.61:5380 | Technitium DNS | -| DNS1 (Secondary) | 192.168.254.204:5380 | Technitium DNS | +| Service | Where | Port | Notes | +|---------|-------|------|-------| +| Caddy (HTTPS) | DNS2 | 443 | Reverse proxy | +| Caddy Admin | DNS2 | 2019 | Caddy API | +| DashCaddy API | DNS2 | 3001 | Dashboard backend (container) | +| Technitium DNS (primary) | DNS2 | 5380 | `100.121.150.22` | +| Technitium DNS (secondary) | DNS1 (this PC) | 5380 | `100.71.97.12` | + +## SSO Architecture + +`import dashcaddy_auth ` in the Caddyfile expands to a `forward_auth` gate that: +1. Checks the DashCaddy TOTP session (cookie domain `.sami` — shared across all `*.sami`) +2. Injects credentials (API key, Basic Auth, app cookies) into upstream request headers + +For client-side auto-login (chat, Plex, Jellyfin, Emby): +- Caddy redirects `path /` to `/dashcaddy-login` +- `/dashcaddy-login` proxies to `GET /api/v1/auth/login-page?service=` on the API +- That page's JS fetches `/dashcaddy-api/api/auth/app-token/` and stores the token in `localStorage` ## Common Mistakes to Avoid -1. **Wrong services.json**: The API container reads from `C:/caddy/services.json`, not the development copy -2. **Caddy admin port**: It's 2019, not 2021 (check with `netstat` if unsure) -3. **DNS server**: DNS2 (100.74.102.61) is PRIMARY, DNS1 is secondary -4. **Caddyfile not reloaded**: After editing, must POST to /load endpoint or restart Caddy +1. **Wrong API source dir**: It's `dashcaddy-api/`, NOT `caddy-api/` (old name, no longer exists) +2. **Wrong services file**: Edit the one in `/opt/dashcaddy/dashcaddy-api/data/` on DNS2, not the dev copy +3. **Caddyfile edits without caddy-apply**: Always use `caddy-apply` — it validates, reloads, and git-commits +4. **Caddy admin port**: It's 2019, not 2021 +5. **Frontend changes without build**: Edit JS source, then `node build.js`, then deploy `dist/` +6. **DNS2 Tailscale IP**: `100.121.150.22` (NOT the old `100.104.4.5` or `100.74.102.61`) --- @@ -316,3 +248,4 @@ vi /opt/dashcaddy/services.json # live-reloaded by the watcher - **Purpose**: Unified management for Docker + Caddy + DNS - **Local TLD (Windows)**: `.sami` - **Local TLD (Linux, DNS2)**: `.home` (default; configurable via `siteConfig.tld`) +- **Repo**: `/opt/dashcaddy/` on DNS2 (git, auto-updated by self-updater) diff --git a/dashcaddy-api/.gitignore b/dashcaddy-api/.gitignore index a8ad317..8264a7c 100644 --- a/dashcaddy-api/.gitignore +++ b/dashcaddy-api/.gitignore @@ -14,3 +14,24 @@ error.log # Test artifacts coverage/ audit-routes.js +comprehensive-test.js +test-security-fixes.js +license-keygen.js + +# Runtime-generated data files (written by the running server, not source) +alert-config.json +audit-log.json +audit-log.json.lock +backup-config.json +backup-history.json +container-stats.json +credentials.json +health-config.json +health-history.json +update-config.json +update-history.json + +# Runtime certificate/key directories +generated-certs/ +pki/ +assets/ diff --git a/dashcaddy-api/license-keygen.js b/dashcaddy-api/license-keygen.js deleted file mode 100644 index 24761c9..0000000 --- a/dashcaddy-api/license-keygen.js +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env node -/** - * DashCaddy License Code Generator - * - * Admin-only CLI tool for generating license codes. - * NOT shipped with the product — runs only on the developer's machine. - * - * Usage: - * node license-keygen.js --duration 365 --count 10 - * node license-keygen.js --duration 30 --count 1 --output codes.txt - * node license-keygen.js --verify DC-XXXXX-XXXXX-XXXXX-XXXXX - * node license-keygen.js --init-secret - */ - -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); - -// Master secret file — lives only on admin machine, NEVER shipped -const SECRET_FILE = path.join(__dirname, '.license-secret'); - -// License code format: DC-AAAAA-BBBBB-CCCCC-DDDDD -// Encodes: version(4bit) + duration_days(12bit) + code_id(32bit) + created_ts(32bit) + hmac(48bit) -// Total: 128 bits = 16 bytes, base32-encoded into 4 groups of 5 chars - -const VALID_DURATIONS = [30, 90, 180, 365]; -const LIFETIME_DURATION = 0; // Admin-only, not publicly available -const VERSION = 1; - -// Base32 alphabet (Crockford variant — no I/L/O/U to avoid confusion) -const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; - -function base32Encode(buffer) { - let bits = ''; - for (const byte of buffer) { - bits += byte.toString(2).padStart(8, '0'); - } - // Pad to multiple of 5 - while (bits.length % 5 !== 0) bits += '0'; - let result = ''; - for (let i = 0; i < bits.length; i += 5) { - const index = parseInt(bits.substring(i, i + 5), 2); - result += BASE32[index]; - } - return result; -} - -function base32Decode(str) { - let bits = ''; - for (const char of str.toUpperCase()) { - const index = BASE32.indexOf(char); - if (index === -1) throw new Error(`Invalid base32 character: ${char}`); - bits += index.toString(2).padStart(5, '0'); - } - const bytes = []; - for (let i = 0; i + 8 <= bits.length; i += 8) { - bytes.push(parseInt(bits.substring(i, i + 8), 2)); - } - return Buffer.from(bytes); -} - -function getSecret() { - if (!fs.existsSync(SECRET_FILE)) { - console.error('No master secret found. Run with --init-secret first.'); - process.exit(1); - } - return fs.readFileSync(SECRET_FILE, 'utf8').trim(); -} - -function initSecret() { - if (fs.existsSync(SECRET_FILE)) { - console.error('Master secret already exists at', SECRET_FILE); - console.error('Delete it first if you want to regenerate (WARNING: invalidates all existing codes).'); - process.exit(1); - } - const secret = crypto.randomBytes(32).toString('hex'); - fs.writeFileSync(SECRET_FILE, secret, { mode: 0o600 }); - console.log('Master secret generated and saved to', SECRET_FILE); - console.log('KEEP THIS FILE SAFE. It is needed to generate and validate all license codes.'); - console.log('DO NOT ship this file with the product.'); -} - -function generateCode(secret, durationDays, codeId) { - // Pack payload: version(4b) + duration_days(12b) + code_id(32b) + created_ts(32b) = 80 bits = 10 bytes - const payload = Buffer.alloc(10); - - // Byte 0-1: version (4 bits) + duration (12 bits) = 16 bits - const versionAndDuration = ((VERSION & 0x0F) << 12) | (durationDays & 0x0FFF); - payload.writeUInt16BE(versionAndDuration, 0); - - // Byte 2-5: code_id (32 bits) - payload.writeUInt32BE(codeId, 2); - - // Byte 6-9: created timestamp (32 bits, seconds since epoch) - const createdTs = Math.floor(Date.now() / 1000); - payload.writeUInt32BE(createdTs, 6); - - // HMAC the payload to get signature - const hmac = crypto.createHmac('sha256', secret).update(payload).digest(); - // Take first 5 bytes of HMAC (40 bits) — fits exactly in 25 base32 chars with 10-byte payload - const signature = hmac.subarray(0, 5); - - // Combine: payload (10 bytes) + signature (5 bytes) = 15 bytes = 120 bits - // 25 base32 chars = 125 bits, comfortably fits 120 bits - const combined = Buffer.concat([payload, signature]); - - let encoded = base32Encode(combined); - while (encoded.length < 25) encoded += '0'; - encoded = encoded.substring(0, 25); - const groups = []; - for (let i = 0; i < 25; i += 5) { - groups.push(encoded.substring(i, i + 5)); - } - - return `DC-${groups.join('-')}`; -} - -function parseCode(code) { - // Strip prefix and dashes - const cleaned = code.replace(/^DC-/, '').replace(/-/g, ''); - if (cleaned.length !== 25) { - throw new Error(`Invalid code length: expected 25 base32 chars, got ${cleaned.length}`); - } - - // Decode base32 — 25 chars = 125 bits = 15 full bytes - const decoded = base32Decode(cleaned); - if (decoded.length < 15) { - const padded = Buffer.alloc(15); - decoded.copy(padded); - return parsePayload(padded); - } - return parsePayload(decoded.subarray(0, 15)); -} - -function parsePayload(buffer) { - const payload = buffer.subarray(0, 10); - const signature = buffer.subarray(10, 15); - - const versionAndDuration = payload.readUInt16BE(0); - const version = (versionAndDuration >> 12) & 0x0F; - const durationDays = versionAndDuration & 0x0FFF; - const codeId = payload.readUInt32BE(2); - const createdTs = payload.readUInt32BE(6); - - return { version, durationDays, codeId, createdTs, payload, signature }; -} - -function verifyCode(secret, code) { - try { - const { version, durationDays, codeId, createdTs, payload, signature } = parseCode(code); - - // Verify HMAC (5-byte signature) - const expectedHmac = crypto.createHmac('sha256', secret).update(payload).digest(); - const expectedSig = expectedHmac.subarray(0, 5); - - if (!crypto.timingSafeEqual(signature, expectedSig)) { - return { valid: false, reason: 'Invalid signature — code is forged or corrupted' }; - } - - if (version !== VERSION) { - return { valid: false, reason: `Unsupported version: ${version}` }; - } - - // Accept lifetime (0) and standard durations - if (durationDays !== LIFETIME_DURATION && !VALID_DURATIONS.includes(durationDays)) { - return { valid: false, reason: `Invalid duration: ${durationDays} days` }; - } - - const createdDate = new Date(createdTs * 1000); - const isLifetime = durationDays === LIFETIME_DURATION; - const expiresDate = isLifetime ? null : new Date(createdTs * 1000 + durationDays * 86400000); - - return { - valid: true, - version, - durationDays, - codeId, - createdAt: createdDate.toISOString(), - expiresAt: isLifetime ? null : expiresDate.toISOString(), - expired: isLifetime ? false : Date.now() > expiresDate.getTime() - }; - } catch (error) { - return { valid: false, reason: error.message }; - } -} - -// CLI -function main() { - const args = process.argv.slice(2); - - if (args.includes('--help') || args.length === 0) { - console.log(` -DashCaddy License Code Generator - -Usage: - node license-keygen.js --init-secret Initialize master secret (first time only) - node license-keygen.js --duration [options] Generate license codes - node license-keygen.js --verify Verify a license code - node license-keygen.js --decode Decode and display code details - -Options: - --duration Code validity: 30, 90, 180, or 365 days (required for generation) - --count Number of codes to generate (default: 1) - --start-id Starting code ID (default: auto from counter file) - --output Write codes to file instead of stdout - --json Output as JSON - -Valid durations: ${VALID_DURATIONS.join(', ')} days - `); - process.exit(0); - } - - if (args.includes('--init-secret')) { - initSecret(); - return; - } - - if (args.includes('--verify') || args.includes('--decode')) { - const codeIndex = args.indexOf('--verify') !== -1 ? args.indexOf('--verify') : args.indexOf('--decode'); - const code = args[codeIndex + 1]; - if (!code) { - console.error('Please provide a code to verify.'); - process.exit(1); - } - const secret = getSecret(); - const result = verifyCode(secret, code); - if (args.includes('--json')) { - console.log(JSON.stringify(result, null, 2)); - } else if (result.valid) { - const isLifetime = result.durationDays === 0; - console.log('Code is VALID'); - console.log(` Version: ${result.version}`); - console.log(` Duration: ${isLifetime ? 'LIFETIME' : result.durationDays + ' days'}`); - console.log(` Code ID: ${result.codeId}`); - console.log(` Created: ${result.createdAt}`); - console.log(` Expires: ${isLifetime ? 'NEVER' : result.expiresAt}`); - console.log(` Status: ${isLifetime ? 'LIFETIME' : (result.expired ? 'EXPIRED' : 'ACTIVE')}`); - } else { - console.log('Code is INVALID'); - console.log(` Reason: ${result.reason}`); - } - return; - } - - // Generate codes - const isLifetime = args.includes('--lifetime'); - const durationIndex = args.indexOf('--duration'); - if (!isLifetime && durationIndex === -1) { - console.error('--duration is required. Use --help for usage.'); - process.exit(1); - } - const duration = isLifetime ? LIFETIME_DURATION : parseInt(args[durationIndex + 1]); - if (!isLifetime && !VALID_DURATIONS.includes(duration)) { - console.error(`Invalid duration: ${duration}. Valid: ${VALID_DURATIONS.join(', ')}`); - process.exit(1); - } - - const countIndex = args.indexOf('--count'); - const count = countIndex !== -1 ? parseInt(args[countIndex + 1]) : 1; - - // Load or create counter file for auto-incrementing code IDs - const counterFile = path.join(__dirname, '.license-counter'); - let startId; - const startIdIndex = args.indexOf('--start-id'); - if (startIdIndex !== -1) { - startId = parseInt(args[startIdIndex + 1]); - } else if (fs.existsSync(counterFile)) { - startId = parseInt(fs.readFileSync(counterFile, 'utf8').trim()) + 1; - } else { - startId = 1; - } - - const secret = getSecret(); - const codes = []; - - for (let i = 0; i < count; i++) { - const codeId = startId + i; - const code = generateCode(secret, duration, codeId); - codes.push({ code, codeId, durationDays: duration }); - } - - // Save counter - fs.writeFileSync(counterFile, String(startId + count - 1)); - - // Output - const outputIndex = args.indexOf('--output'); - if (args.includes('--json')) { - const output = JSON.stringify(codes, null, 2); - if (outputIndex !== -1) { - fs.writeFileSync(args[outputIndex + 1], output); - console.log(`${count} code(s) written to ${args[outputIndex + 1]}`); - } else { - console.log(output); - } - } else { - const lines = codes.map(c => `${c.code} (${c.durationDays === 0 ? 'LIFETIME' : c.durationDays + ' days'}, ID: ${c.codeId})`); - if (outputIndex !== -1) { - fs.writeFileSync(args[outputIndex + 1], codes.map(c => c.code).join('\n') + '\n'); - console.log(`${count} code(s) written to ${args[outputIndex + 1]}`); - } else { - lines.forEach(l => console.log(l)); - } - } - - console.log(`\nGenerated ${count} code(s) for ${duration === 0 ? 'LIFETIME' : duration + ' days'}. Next ID: ${startId + count}`); -} - -// Also export for use by license-manager.js -module.exports = { verifyCode, parseCode, VALID_DURATIONS, VERSION }; - -if (require.main === module) { - main(); -} diff --git a/dashcaddy-api/routes/auth/sso-gate.js b/dashcaddy-api/routes/auth/sso-gate.js index 69209f0..7706328 100644 --- a/dashcaddy-api/routes/auth/sso-gate.js +++ b/dashcaddy-api/routes/auth/sso-gate.js @@ -196,5 +196,73 @@ module.exports = function(deps) { } }, 'auth-app-token')); + // Serve service-specific auto-login page (auth enforced by Caddy forward_auth upstream) + router.get('/auth/login-page', (req, res) => { + const service = (req.query.service || '').replace(/[^a-z]/g, ''); + const html = buildLoginPage(service); + if (!html) return res.status(404).send('Unknown service'); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.setHeader('Cache-Control', 'no-store'); + res.send(html); + }); + return router; }; + +function buildLoginPage(service) { + const SHELL = (body) => ` +__TITLE__ + +

__TITLE__

+`; + + const pages = { + chat: { + title: 'Signing in...', bg: '#0a0a0a', accent: '#60a5fa', + body: `if(ls.getItem('token')){go('/?direct=1');return} +d.textContent='Fetching token from DashCaddy...'; +ft('chat').then(function(r){d.textContent+=' Status: '+r.status;return r.text()}).then(function(t){ + d.textContent+='\\n'+t.substring(0,300); + try{var j=JSON.parse(t);if(j.token){ls.setItem('token',j.token);go('/?direct=1')} + else{fail('Auto-login unavailable. Sign in manually','No token field in response')}} + catch(e){fail('Auto-login error. Sign in manually','Parse error: '+e.message)} +}).catch(function(e){fail('Could not reach DashCaddy. Sign in manually','Fetch error: '+e.message)})` + }, + plex: { + title: 'Signing in to Plex...', bg: '#1f1f1f', accent: '#e5a00d', + body: `if(ls.getItem('myPlexAccessToken')){go('/web/?direct=1');return} +ft('plex').then(function(r){return r.json()}).then(function(j){ + if(j.token){ls.setItem('myPlexAccessToken',j.token);d.textContent='Token stored, redirecting...';go('/web/?direct=1')} + else{fail('Auto-login unavailable. Open Plex manually',JSON.stringify(j))} +}).catch(function(e){fail('Could not reach DashCaddy. Open Plex manually','Error: '+e.message)})` + }, + jellyfin: { + title: 'Signing in to Jellyfin...', bg: '#101014', accent: '#00a4dc', + body: `ft('jellyfin').then(function(r){return r.json()}).then(function(j){ + if(j.token){merge('jellyfin_credentials',j,'Jellyfin');merge('_jellyfin_credentials',j,'Jellyfin');d.textContent='Token stored, redirecting...';go('/web/')} + else{fail('Auto-login unavailable. Open Jellyfin manually',JSON.stringify(j))} +}).catch(function(e){fail('Could not reach DashCaddy. Open Jellyfin manually','Error: '+e.message)})` + }, + emby: { + title: 'Signing in to Emby...', bg: '#101014', accent: '#52b54b', + body: `ft('emby').then(function(r){return r.json()}).then(function(j){ + if(j.token){merge('emby_credentials',j,'Emby');merge('_emby_credentials',j,'Emby');d.textContent='Token stored, redirecting...';go('/web/')} + else{fail('Auto-login unavailable. Open Emby manually',JSON.stringify(j))} +}).catch(function(e){fail('Could not reach DashCaddy. Open Emby manually','Error: '+e.message)})` + }, + }; + + const cfg = pages[service]; + if (!cfg) return null; + return SHELL(cfg.body) + .replace(/__TITLE__/g, cfg.title) + .replace('__BG__', cfg.bg) + .replace('__ACCENT__', cfg.accent); +} diff --git a/dashcaddy-api/src/utilities/middleware.js b/dashcaddy-api/src/utilities/middleware.js index 1e81b88..2a8c660 100644 --- a/dashcaddy-api/src/utilities/middleware.js +++ b/dashcaddy-api/src/utilities/middleware.js @@ -326,6 +326,7 @@ module.exports = function configureMiddleware(app, { { 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/auth/login-page', exact: true, method: 'GET' }, { 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' },