Compare commits

..
3 Commits
Author SHA1 Message Date
Krystie cf8909740f DC-044: fix legacy /api/auth/totp/check-session shim path (drop /auth)
CI / Security audit (push) Has been cancelled
CI / Test & Lint (push) Has been cancelled
The shim added in the previous commit rewrote /api/auth/totp/check-session
to /api/v1/auth/totp/check-session, but the canonical route is mounted at
/totp/check-session (no /auth prefix). The 404 returned to the auto-login
JS path was Route GET /v1/auth/totp/check-session — Express's /api/v1
mount stripped the /api/v1 prefix, leaving /auth/totp/check-session, which
doesn't match /totp/check-session.

Drop both /api and /auth (9 chars) so the legacy path maps to the
canonical /api/v1/totp/check-session.

Verified after deploy:
  GET /api/auth/totp/check-session  -> {"authenticated":true}
  GET /api/v1/totp/check-session    -> {"authenticated":true}
2026-07-08 21:33:07 -07:00
Krystie a7b0714643 DC-044: fix plex.sami auto-login JS 404 (add check-session to legacy shim)
The Plex/Jellyfin/Emby/chat auto-login page JS (sso-gate.js
buildLoginPage) calls /api/auth/totp/check-session — the pre-1.5.0
legacy prefix. The back-compat shim in app.js only handled
/api/auth/gate/ and /api/auth/app-token/, so check-session 404'd and
the page hung at "Signing in to Plex..." forever (user reported
2026-07-09, confirmed: request returns "Route GET /v1/auth/totp/
check-session not found").

Add /api/auth/totp/check-session to the legacy path rewrite so the JS
gets the canonical /api/v1/totp/check-session endpoint.

Verified: plex.sami/dashcaddy-login now returns the auto-login page
and the JS check-session fetch resolves to {"authenticated":true} for
active TOTP sessions.
2026-07-08 21:33:06 -07:00
Krystie d539ee3b08 DC-044: fix /health/ready caddy probe false negative
The caddy.ok check in /health/ready probed /config/ (51KB) and timed out
at 3s with "This operation was aborted" while Caddy admin was actually
healthy. Two underlying issues:

1. Native undici fetch() rejects connections to :2019 (Caddy admin). Use
   fetchT() which falls back to raw http.request for the admin port.
2. /config/ is heavy and head-of-line blocks when /load is in flight.
   Switch to /config/apps/http/servers/srv0/listen (9 bytes) and bump
   timeout to 10s.

Verified on DNS2 2026-07-09: direct Caddy admin curl 200 in 3ms,
/health/ready was aborting at 3s. After fix: /health/ready caddy.ok
true in <100ms.

Caddyfile change (/etc/caddy/Caddyfile) added /dashcaddy-login to the
@needsAuth not path exclude so direct hits to the auto-login landing
page render the page instead of getting gate-redirected to a blank
302 — applied and reloaded via POST /load earlier this session.
2026-07-08 21:33:06 -07:00
3 changed files with 26 additions and 18 deletions
@@ -105,10 +105,7 @@ function buildApp({ configOk = true, servicesOk = true, dockerOk = true, caddyOk
try {
const caddyUrl = config.CADDY_ADMIN_URL || 'http://localhost:2019';
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`${caddyUrl}/config/`, { signal: controller.signal });
clearTimeout(timeout);
const response = await fetch(`${caddyUrl}/config/apps/http/servers/srv0/listen`, { signal: AbortSignal.timeout(10000) });
checks.caddy = { ok: response.ok, status: response.status };
if (!response.ok) allOk = false;
} catch (e) {
@@ -109,10 +109,7 @@ function buildApp({ configOk = true, servicesOk = true, dockerOk = true } = {})
}
try {
const caddyUrl = config.CADDY_ADMIN_URL || 'http://localhost:2019';
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`${caddyUrl}/config/`, { signal: controller.signal });
clearTimeout(timeout);
const response = await fetch(`${caddyUrl}/config/apps/http/servers/srv0/listen`, { signal: AbortSignal.timeout(10000) });
checks.caddy = { ok: response.ok, status: response.status };
if (!response.ok) allOk = false;
} catch (e) {
+24 -10
View File
@@ -192,13 +192,27 @@ async function createApp() {
// auto-login pages) historically call these endpoints under the pre-1.5.0
// prefix `/api/auth/...`. The canonical mount is `/api/v1`. Hand-maintained
// Caddyfiles have repeatedly drifted back to the old prefix and 404'd the SSO
// gate (breaking Plex/Jellyfin/Emby/chat). Transparently rewrite ONLY these two
// auth paths to the v1 mount so the gate is tolerant of that drift. Must run
// before configureMiddleware() so CSRF/auth see the canonical path. This is
// deliberately narrow — NOT a general `/api` -> `/api/v1` alias.
// gate (breaking Plex/Jellyfin/Emby/chat). Transparently rewrite ONLY these
// three auth paths to the v1 mount so the gate is tolerant of that drift.
// Must run before configureMiddleware() so CSRF/auth see the canonical path.
// This is deliberately narrow — NOT a general `/api` -> `/api/v1` alias.
//
// Path mapping (legacy -> canonical):
// /api/auth/gate/<id> -> /api/v1/auth/gate/<id> (mounted at /auth/gate/:serviceId)
// /api/auth/app-token/<id> -> /api/v1/auth/app-token/<id> (mounted at /auth/app-token/:serviceId)
// /api/auth/totp/check-session -> /api/v1/totp/check-session (mounted at /totp/check-session — no /auth prefix)
//
// The totp case drops `/auth` because the canonical route is /totp/check-session
// (no /auth prefix) but the legacy JS still uses /api/auth/totp/check-session.
// Without this rewrite the JS gets a 404 and the page hangs at
// "Signing in to Plex..." forever (user-reported 2026-07-09).
app.use((req, res, next) => {
if (req.url.startsWith('/api/auth/gate/') || req.url.startsWith('/api/auth/app-token/')) {
req.url = '/api/v1' + req.url.slice(4); // '/api'.length === 4
} else if (req.url.startsWith('/api/auth/totp/check-session')) {
// Legacy: /api/auth/totp/check-session -> /api/v1/totp/check-session
// Drop both '/api' and '/auth' prefixes (9 chars total).
req.url = '/api/v1' + req.url.slice(9); // '/api/auth'.length === 9
}
next();
});
@@ -727,14 +741,14 @@ async function createApp() {
}
// Check 4: Caddy admin API reachable
// Use fetchT() (NOT native fetch) because undici fetch rejects Caddy admin
// on :2019, and probe the LIGHTEST endpoint (srv0/listen = 9 bytes) to avoid
// head-of-line blocking when /load or another config mutation is in flight.
// A previous `/config/` probe hit the 3s AbortController timeout with
// "This operation was aborted" while Caddy was actually healthy.
try {
const caddyUrl = config.CADDY_ADMIN_URL || 'http://localhost:2019';
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`${caddyUrl}/config/`, {
signal: controller.signal
});
clearTimeout(timeout);
const response = await fetchT(`${caddyUrl}/config/apps/http/servers/srv0/listen`, {}, 10000);
checks.caddy = { ok: response.ok, status: response.status };
if (!response.ok) allOk = false;
} catch (e) {