diff --git a/web/src/api/http.ts b/web/src/api/http.ts new file mode 100644 index 0000000..676be26 --- /dev/null +++ b/web/src/api/http.ts @@ -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 { + 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; +} diff --git a/web/src/main.ts b/web/src/main.ts index e20f04b..89568a9 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -2,9 +2,12 @@ import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; import router from './router/index.js'; +import { installApiClient } from './api/http.js'; import './styles/global.scss'; import './styles/mobile.scss'; +installApiClient(); + const app = createApp(App); app.use(createPinia()); app.use(router);