fix(auth): keep session on a wrong-current-password 401

The change-password endpoint returns 401 for a wrong *current* password,
the same status the global on401 hook uses to detect an expired session.
The hook fired first and cleared session.user, so the account form's
"still signed in?" guard always saw null and bounced the user to /login
instead of surfacing the error inline.

Add a per-call `suppressOn401` option to `request`; changePassword opts
in, so a 401 there stays inline and the session is left intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-01 20:11:07 +02:00
parent 5596e1920d
commit 0865659ab3
6 changed files with 53 additions and 10 deletions

View File

@@ -54,7 +54,24 @@ export function setOn401Hook(handler: (() => void) | null): void {
on401Hook = handler;
}
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
/** Per-call knobs that don't belong on the native `RequestInit`. */
export type RequestOptions = {
/**
* Skip the module-level 401 hook for this call. Used by endpoints
* where a 401 does *not* mean "session expired" — e.g. the
* change-password endpoint returns 401 for a wrong *current*
* password while the caller is still fully authenticated. Firing
* the hook there would clear the cached user and bounce them to
* /login instead of surfacing the error inline.
*/
suppressOn401?: boolean;
};
export async function request<T>(
path: string,
init?: RequestInit,
opts?: RequestOptions
): Promise<T> {
// Forward credentials (session cookie) explicitly so cross-origin
// deployments — those configured via CORS_ALLOWED_ORIGINS — keep
// working. For same-origin requests this is a no-op compared to the
@@ -87,7 +104,7 @@ export async function request<T>(path: string, init?: RequestInit): Promise<T> {
} catch {
// Body wasn't parseable; keep the http_error fallback.
}
if (res.status === 401 && on401Hook) {
if (res.status === 401 && on401Hook && !opts?.suppressOn401) {
// Fire before throwing so the session store updates even
// if the caller swallows the ApiError (e.g. the *OrEmpty
// wrappers used by guest-rendering pages).