c1adb94abb
CI has been running `prettier --check` against a tree that was never formatted, so the check reported 63 files and failed every run. Nothing here is a behaviour change: `pnpm lint` and the four typecheck builds pass exactly as before. conduit-bringup-artifacts is added to .prettierignore instead. Those files are captured bring-up reports, not maintained sources; reflowing them would only churn a record of what happened.
138 lines
3.4 KiB
TypeScript
138 lines
3.4 KiB
TypeScript
const RAW_API_BASE = (
|
|
(import.meta.env.VITE_API_URL as string | undefined) ??
|
|
(import.meta.env.VITE_API_BASE_URL as string | undefined) ??
|
|
'/api'
|
|
).trim();
|
|
const API_BASE = (RAW_API_BASE || '/api').replace(/\/+$/, '');
|
|
|
|
interface RequestOptions extends RequestInit {
|
|
params?: Record<string, string>;
|
|
}
|
|
|
|
function toRequestBody(body: unknown): BodyInit | undefined {
|
|
if (body === undefined || body === null) return undefined;
|
|
|
|
if (
|
|
body instanceof FormData ||
|
|
body instanceof Blob ||
|
|
body instanceof URLSearchParams ||
|
|
body instanceof ArrayBuffer
|
|
) {
|
|
return body;
|
|
}
|
|
|
|
return JSON.stringify(body);
|
|
}
|
|
|
|
class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public data: unknown,
|
|
) {
|
|
super(`API Error ${status}`);
|
|
this.name = 'ApiError';
|
|
}
|
|
}
|
|
|
|
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
const { params, ...fetchOptions } = options;
|
|
|
|
let url = `${API_BASE}${path}`;
|
|
if (params) {
|
|
const searchParams = new URLSearchParams(params);
|
|
url += `?${searchParams.toString()}`;
|
|
}
|
|
|
|
const token = localStorage.getItem('access_token');
|
|
const headers: Record<string, string> = {
|
|
...(fetchOptions.headers as Record<string, string>),
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
if (fetchOptions.body && typeof fetchOptions.body === 'string') {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
...fetchOptions,
|
|
credentials: fetchOptions.credentials ?? 'include',
|
|
headers,
|
|
});
|
|
|
|
const shouldHandle401WithRefresh =
|
|
res.status === 401 &&
|
|
path !== '/auth/login' &&
|
|
path !== '/auth/register' &&
|
|
path !== '/auth/refresh';
|
|
|
|
if (shouldHandle401WithRefresh) {
|
|
// Try refresh
|
|
const refreshed = await refreshToken();
|
|
if (refreshed) {
|
|
headers['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`;
|
|
const retry = await fetch(url, {
|
|
...fetchOptions,
|
|
credentials: fetchOptions.credentials ?? 'include',
|
|
headers,
|
|
});
|
|
if (!retry.ok) throw new ApiError(retry.status, await retry.json().catch(() => null));
|
|
if (retry.status === 204) return undefined as T;
|
|
return retry.json();
|
|
}
|
|
localStorage.removeItem('access_token');
|
|
window.location.href = '/login';
|
|
throw new ApiError(401, null);
|
|
}
|
|
|
|
if (!res.ok) {
|
|
throw new ApiError(res.status, await res.json().catch(() => null));
|
|
}
|
|
|
|
if (res.status === 204) return undefined as T;
|
|
return res.json();
|
|
}
|
|
|
|
async function refreshToken(): Promise<boolean> {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/auth/refresh`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) return false;
|
|
const data = await res.json();
|
|
localStorage.setItem('access_token', data.accessToken);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string, params?: Record<string, string>) => request<T>(path, { params }),
|
|
|
|
post: <T>(path: string, body?: unknown) =>
|
|
request<T>(path, {
|
|
method: 'POST',
|
|
body: toRequestBody(body),
|
|
}),
|
|
|
|
put: <T>(path: string, body?: unknown) =>
|
|
request<T>(path, {
|
|
method: 'PUT',
|
|
body: toRequestBody(body),
|
|
}),
|
|
|
|
patch: <T>(path: string, body?: unknown) =>
|
|
request<T>(path, {
|
|
method: 'PATCH',
|
|
body: toRequestBody(body),
|
|
}),
|
|
|
|
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
|
};
|
|
|
|
export { ApiError };
|