fix(auth): login/registration forms redirect again under hash CSP

The htmx login/registration partials answered success with an inline
<script> that stashed the auth token and set window.location — refused
by hash-mode script-src (no 'unsafe-inline'), so the user saw "Login
successful! Redirecting..." and nothing else (reported from the home-
page login form; /login was equally broken). The same pattern lived in
collections.go (delete → redirect) and devices.go (token regen →
reload, whose script tag was also unclosed).

Replacement, per the AGENTS.md CSP rules:
- auth.go login/registration: the partial now carries data-auth-token +
  data-redirect (redirect is path-only — open-redirect hygiene — and
  escaped); web/src/auth_forms.ts (new, armed from main.ts on every
  page) listens for htmx:afterSwap, stashes the token, and navigates.
- collections.go delete: data-redirect, same listener.
- devices.go token regen: htmx's native HX-Refresh response header.
- The dead localStorage 'user'/'refreshToken' writes are dropped — no
  reader anywhere in web/src; only 'token' is load-bearing (19 modules).

Tripwire extension: internal/middleware/csp_go_sources_test.go scans
non-test Go sources for literal <script> tags and native on*= attributes
(hand-written files only — templ codegen is owned by the .templ scan),
verified to fire on a reintroduced probe. AGENTS.md CSP section notes
the Go-emitted-partial vector.

Verified live: home-page login form → /dashboard with token stashed and
dashboard loading clean; /register form → /dashboard; collections and
devices share the same mechanisms (data-redirect / HX-Refresh). Gates:
make test (9 pkgs incl. both tripwires), vitest 155, Playwright 20,
tsc/vp clean.
This commit is contained in:
John O'Keefe committed 2026-10-27 13:57:41 -04:00
1 parent b7595c3011
commit 7c9e561e68
8 files changed
+124 -20

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
// CSP-safe post-swap navigation for server-emitted status partials.
//
// The login/registration/collection-delete htmx flows used to answer with an
// inline <script> that stashed the auth token and redirected. Hash-mode CSP
// (no 'unsafe-inline') refuses those scripts, so the server now emits plain
// data attributes instead and this module performs the navigation:
//
// <div data-redirect="/dashboard" data-auth-token="…">Login successful!…</div>
//
// One delegated body listener covers every page (the main bundle loads
// everywhere htmx does), so no per-page wiring. Devices' token regeneration
// uses htmx's native HX-Refresh response header instead and needs nothing
// here.
//
// Note: only the token is stashed. The old script also wrote localStorage
// 'user'/'refreshToken', but nothing in web/src ever read them.
export function armAuthFormRedirects() {
document.body.addEventListener("htmx:afterSwap", (event) => {
const detail = (event as CustomEvent).detail as { target?: HTMLElement } | undefined;
const el = detail?.target?.querySelector("[data-redirect]");
if (!el) return;
const token = el.getAttribute("data-auth-token");
if (token) localStorage.setItem("token", token);
window.location.href = el.getAttribute("data-redirect") ?? "/";
});
}