102 lines
2.8 KiB
TypeScript
102 lines
2.8 KiB
TypeScript
import { auth } from '$lib/auth/store.svelte';
|
|
import { goto } from '$app/navigation';
|
|
|
|
const BASE = '/api';
|
|
|
|
let isRefreshing = false;
|
|
let refreshPromise: Promise<string | null> | null = null;
|
|
|
|
async function doRefresh(): Promise<string | null> {
|
|
const refreshToken = auth.getRefreshToken();
|
|
if (!refreshToken) return null;
|
|
|
|
try {
|
|
const res = await fetch(`${BASE}/token/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: refreshToken })
|
|
});
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
auth.setTokens(data.access_token, data.refresh_token);
|
|
return data.access_token;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getValidToken(): Promise<string | null> {
|
|
if (auth.accessToken) return auth.accessToken;
|
|
|
|
// Deduplicate concurrent refresh calls
|
|
if (isRefreshing) return refreshPromise;
|
|
isRefreshing = true;
|
|
refreshPromise = doRefresh().finally(() => {
|
|
isRefreshing = false;
|
|
refreshPromise = null;
|
|
});
|
|
return refreshPromise;
|
|
}
|
|
|
|
export async function apiFetch(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
retry = true
|
|
): Promise<Response> {
|
|
const token = await getValidToken();
|
|
|
|
const headers = new Headers(options.headers);
|
|
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
|
|
const res = await fetch(`${BASE}${path}`, { ...options, headers });
|
|
|
|
if (res.status === 401 && retry) {
|
|
// Force a refresh and retry once
|
|
auth.setAccessToken(''); // clear so getValidToken triggers refresh
|
|
const newToken = await doRefresh();
|
|
if (!newToken) {
|
|
auth.clear();
|
|
goto('/login');
|
|
return res;
|
|
}
|
|
auth.setAccessToken(newToken);
|
|
headers.set('Authorization', `Bearer ${newToken}`);
|
|
return fetch(`${BASE}${path}`, { ...options, headers });
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
export async function apiGet<T>(path: string): Promise<T> {
|
|
const res = await apiFetch(path);
|
|
if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
|
|
return res.json();
|
|
}
|
|
|
|
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
|
|
const res = await apiFetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw Object.assign(new Error(`POST ${path} failed: ${res.status}`), { status: res.status, detail: err });
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export async function apiPatch<T>(path: string, body: unknown): Promise<T> {
|
|
const res = await apiFetch(path, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (!res.ok) throw new Error(`PATCH ${path} failed: ${res.status}`);
|
|
return res.json();
|
|
}
|
|
|
|
export async function apiDelete(path: string): Promise<void> {
|
|
const res = await apiFetch(path, { method: 'DELETE' });
|
|
if (!res.ok) throw new Error(`DELETE ${path} failed: ${res.status}`);
|
|
}
|