// Wrapper around $fetch for authenticated WRITES from the client. The OIDC // access token in the server-side session can lapse while a tab stays open; // the next write then hits a proxy that finds no token and 401s (a page // refresh "fixed" it only because navigation re-ran the session refresh). // // On a 401 we hit nuxt-oidc-auth's refresh endpoint DIRECTLY (POST // /api/_auth/refresh) rather than useOidcAuth().refresh() — the composable // falls back to a full login() *redirect* when the refresh token is missing or // expired, which would navigate away mid-save and throw out the user's input. // Here, a failed refresh just rejects: we surface a clear error and leave the // user on the page with their form intact, so they can re-submit after signing // in again. Sign-in stays an explicit, user-driven action. // // Call this in setup(); the returned `request` can be invoked later. interface ApiOpts { method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' body?: unknown query?: Record headers?: Record } function isUnauthorized(err: unknown): boolean { const e = err as { statusCode?: number; response?: { status?: number } } return e?.statusCode === 401 || e?.response?.status === 401 } export function useApiFetch() { // Silent token refresh. Resolves true if the session now has a fresh access // token, false if it couldn't be refreshed (no/expired refresh token). async function refreshSession(): Promise { try { await $fetch('/api/_auth/refresh', { method: 'POST', headers: { Accept: 'text/json' } }) return true } catch { return false } } async function request(url: string, opts: ApiOpts = {}): Promise { const fetchOpts = opts as Parameters[1] try { return (await $fetch(url, fetchOpts)) as T } catch (err) { if (!isUnauthorized(err)) throw err // Token lapsed mid-session — try a silent refresh, then retry once. if (await refreshSession()) { return (await $fetch(url, fetchOpts)) as T } // Refresh failed: the session is genuinely expired. Don't redirect (that // would discard the user's input) — fail loudly so the caller keeps the // form open and can show "sign in again to save". throw createError({ statusCode: 401, statusMessage: 'Session expired', message: 'Your session expired. Please sign in again, then save your changes.', data: { message: 'Your session expired. Please sign in again, then save your changes.' }, }) } } return { request } }