chore: initial commit for phase05
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
const API_BASE = '/api';
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
|
||||
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, headers });
|
||||
|
||||
if (res.status === 401) {
|
||||
// Try refresh
|
||||
const refreshed = await refreshToken();
|
||||
if (refreshed) {
|
||||
headers['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`;
|
||||
const retry = await fetch(url, { ...fetchOptions, 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: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
patch: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, {
|
||||
method: 'PATCH',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
delete: <T>(path: string) =>
|
||||
request<T>(path, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
export { ApiError };
|
||||
Reference in New Issue
Block a user