feat(web): install global fetch wrapper with credentials + 401 handling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Sonnet 4.6 committed 2026-05-27 13:54:24 +08:00
1 parent 0dc8746914
commit 7509814abc
2 files changed
+51

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
import router from '../router/index.js';
import { useSession } from '../composables/useSession.js';
let installed = false;
/**
* Wraps fetch so every call:
* - sends cookies (`credentials: 'same-origin'`)
* - on 401 from /api/*: clear local session, redirect to /login
*/
export function apiFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
const merged: RequestInit = {
credentials: 'same-origin',
...init,
headers: { ...(init.headers ?? {}) },
};
return fetch(input, merged).then(async (res) => {
if (res.status === 401 && isApiPath(input)) {
const session = useSession();
await session.refresh();
const current = router.currentRoute.value;
if (current.name !== 'login' && current.name !== 'first-run') {
await router.replace({ name: 'login', query: { next: current.fullPath } });
}
}
return res;
});
}
function isApiPath(input: RequestInfo | URL): boolean {
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
return url.startsWith('/api/');
}
/**
* Replaces window.fetch with apiFetch so existing call sites do not need to be touched.
* Call once at app startup.
*/
export function installApiClient(): void {
if (installed) return;
installed = true;
const original = window.fetch.bind(window);
window.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
return apiFetch(input, init ?? {});
}) as typeof window.fetch;
// Keep original accessible if anything needs to bypass
(window as unknown as { __originalFetch?: typeof fetch }).__originalFetch = original;
}