import { FetchError, type FetchOptions, ofetch } from "ofetch"; import type { ValidationIssue } from "@imen/shared"; /** Raw client without auth handling (login / refresh / logout use it directly) */ export const rawApi = ofetch.create({ baseURL: "/api", credentials: "include" }); type AuthHooks = { token: () => string; refresh: () => Promise; onExpired: () => void; }; let hooks: AuthHooks | null = null; /** Wired by the auth store, which owns the access token */ export const setAuthHooks = (h: AuthHooks) => { hooks = h; }; /** * Authenticated request. On 401 it refreshes the access token once (using the httpOnly * refresh cookie) and retries; if that fails the session is over. */ export async function api(url: string, opts: FetchOptions<"json"> = {}, retried = false): Promise { const token = hooks?.token(); try { return await rawApi(url, { ...opts, headers: { ...(opts.headers as Record), ...(token && { Authorization: `Bearer ${token}` }) }, }); } catch (e) { if (e instanceof FetchError && e.status === 401 && !retried && hooks) { if (await hooks.refresh()) return api(url, opts, true); hooks.onExpired(); } throw e; } } interface ApiErrorBody { message?: string | string[]; issues?: ValidationIssue[]; } export const errorBody = (e: unknown): ApiErrorBody => e instanceof FetchError ? ((e.data as ApiErrorBody | undefined) ?? {}) : {}; /** Human-readable (Persian) error message for notifications */ export function errorMessage(e: unknown): string { if (e instanceof FetchError) { if (!e.status) return "ارتباط با سرور برقرار نشد"; const msg = errorBody(e).message; if (Array.isArray(msg)) return msg.join("، "); if (msg) return msg; if (e.status === 413) return "حجم فایل بیش از حد مجاز است"; if (e.status === 429) return "تعداد درخواست‌ها زیاد است؛ کمی بعد تلاش کنید"; return `خطای سرور (${e.status})`; } return "خطای ناشناخته"; }