Found while double-checking /login after the redirect fix: a wrong password left the form completely silent. Two stacked causes, both pre-dating the CSP work: 1. The two HX-Request branches for invalid credentials returned JSON, unlike every sibling error branch — there was no HTML partial to show at all. They now return a shared invalidCredentialsHTML div (the JSON shape is kept for non-htmx API callers). 2. htmx does not swap 4xx/5xx responses by default, so even the HTML error partials that DID exist (429 lockout, 500s, registration failures like "Email already exists") never landed in the target. auth_forms.ts now swaps them itself on htmx:beforeSwap when the error response is text/html — JSON errors stay ignored. Verified live: /login success → /dashboard; wrong password shows "Invalid email/username or password." and stays on /login; duplicate email on /register shows "Email already exists". Gates: make test 9 pkgs, vitest 155, Playwright 20.
46 lines
2.0 KiB
TypeScript
46 lines
2.0 KiB
TypeScript
// 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() {
|
|
// htmx refuses to swap 4xx/5xx responses by default, which left the login
|
|
// form silent on wrong passwords: the server's error partials (401 invalid
|
|
// credentials, 429 lockout, 500s) never landed in the target div. Swap
|
|
// them ourselves whenever the error response is an HTML partial — JSON
|
|
// errors (API clients) are still ignored by htmx.
|
|
document.body.addEventListener("htmx:beforeSwap", (event) => {
|
|
const detail = (event as CustomEvent).detail as {
|
|
xhr?: XMLHttpRequest;
|
|
shouldSwap?: boolean;
|
|
isError?: boolean;
|
|
};
|
|
const xhr = detail?.xhr;
|
|
if (!xhr || xhr.status < 400) return;
|
|
if (!(xhr.getResponseHeader("content-type") ?? "").includes("text/html")) return;
|
|
detail.shouldSwap = true;
|
|
detail.isError = false;
|
|
});
|
|
|
|
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") ?? "/";
|
|
});
|
|
}
|