Files
punktfunk/web/server/middleware/auth.ts
T
enricobuehlerandClaude Fable 5 bb1f93d90e feat(web,docs): the Update-now flow — password re-entry at the BFF, restart-tolerant progress, same-origin hardening
apply.post.ts intercepts the one proxied route that restarts the host: the console
password is re-verified per apply (login throttle shared, stripped before forwarding) so
a 7-day cookie alone can't do it. New Sec-Fetch-Site same-origin check on every mutating
request (login CSRF included). The card's apply flow: confirm dialog → live-session
force escalation → download/verify/restart progress rendered from the last snapshot
while polls fail (the host and, on Windows, this very server restart mid-flow) →
durable success/failure from last_result. Docs: the one-click section + kill switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:55:27 +02:00

62 lines
2.3 KiB
TypeScript

// The single server-side gate. Runs for EVERY request to the deployed Bun/Nitro server
// (pages, the /api proxy, everything) before routing. Unauthenticated requests are
// redirected to /login (page navigations) or rejected 401 (/api). Fails CLOSED if
// PUNKTFUNK_UI_PASSWORD is unset, so a misconfigured LAN-exposed server admits no one.
import {
defineEventHandler,
getRequestHeader,
getRequestURL,
sendRedirect,
setResponseStatus,
useSession,
} from "h3";
import {
isPublicPath,
type SessionData,
sessionConfig,
uiPassword,
} from "../util/auth";
export default defineEventHandler(async (event) => {
const { pathname } = getRequestURL(event);
// Same-origin check for every MUTATING request (defense in depth beyond SameSite=Lax,
// added with the update-apply route where CSRF ≈ code execution — design
// host-update-from-web-console.md §4.3). `Sec-Fetch-Site` is browser-set and unforgeable
// from a page; absent (curl, very old browsers) ⇒ allowed — the console's threat here is
// a BROWSER being ridden cross-site, and every riding browser sends the header.
// `same-site` is rejected too: with an IP-address origin, another port on the same box
// counts as same-site, and nothing on another port has business mutating the console.
// Applies to public paths as well (login CSRF), before any session logic.
const method = event.method?.toUpperCase?.() ?? "GET";
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
const site = getRequestHeader(event, "sec-fetch-site")?.toLowerCase();
if (site && site !== "same-origin" && site !== "none") {
setResponseStatus(event, 403);
return { error: "cross-site request refused" };
}
}
if (isPublicPath(pathname)) return;
// Misconfigured: refuse everything rather than serve open on the LAN.
if (!uiPassword()) {
setResponseStatus(event, 503);
return { error: "auth not configured: set PUNKTFUNK_UI_PASSWORD" };
}
const session = await useSession<SessionData>(event, sessionConfig());
if (session.data.authenticated) return; // authenticated — let it through
if (pathname.startsWith("/api")) {
setResponseStatus(event, 401);
return { error: "unauthorized" };
}
// Page navigation → bounce to the login screen, remembering where they were headed.
return sendRedirect(
event,
`/login?next=${encodeURIComponent(pathname)}`,
302,
);
});