const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000/api/v1' const ACCESS_TOKEN_KEY = 'meshkee_access_token' const REFRESH_TOKEN_KEY = 'meshkee_refresh_token' export class ApiError extends Error { status: number constructor(message: string, status: number) { super(message) this.name = 'ApiError' this.status = status } } export function isAbortError(err: unknown): boolean { return err instanceof DOMException && err.name === 'AbortError' } export function getAccessToken() { return localStorage.getItem(ACCESS_TOKEN_KEY) } export function getRefreshToken() { return localStorage.getItem(REFRESH_TOKEN_KEY) } export function setTokens(accessToken: string, refreshToken: string) { localStorage.setItem(ACCESS_TOKEN_KEY, accessToken) localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken) } export function clearTokens() { localStorage.removeItem(ACCESS_TOKEN_KEY) localStorage.removeItem(REFRESH_TOKEN_KEY) } function parseErrorMessage(payload: unknown, fallback: string) { if (!payload || typeof payload !== 'object') { return fallback } const message = (payload as { message?: string | string[] }).message if (Array.isArray(message)) { return message.join(', ') } if (typeof message === 'string') { return message } return fallback } async function refreshAccessToken() { const refreshToken = getRefreshToken() if (!refreshToken) { return false } const response = await fetch(`${API_BASE_URL}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }) if (!response.ok) { clearTokens() return false } const data = await response.json() setTokens(data.accessToken, data.refreshToken) return true } interface RequestOptions extends Omit { body?: unknown auth?: boolean } export async function apiRequest(path: string, options: RequestOptions = {}): Promise { const { body, auth = false, headers, ...rest } = options const requestHeaders = new Headers(headers) if (body !== undefined) { requestHeaders.set('Content-Type', 'application/json') } if (auth) { const accessToken = getAccessToken() if (accessToken) { requestHeaders.set('Authorization', `Bearer ${accessToken}`) } } const response = await fetch(`${API_BASE_URL}${path}`, { ...rest, headers: requestHeaders, body: body !== undefined ? JSON.stringify(body) : undefined, }) if (response.status === 401 && auth) { const refreshed = await refreshAccessToken() if (refreshed) { return apiRequest(path, options) } } const payload = await response.json().catch(() => null) if (!response.ok) { throw new ApiError( parseErrorMessage(payload, `Request failed with status ${response.status}`), response.status, ) } return payload as T }