import { clearAuth, getAuth, getToken, setAuth } from "@/lib/auth" export const API_URL = import.meta.env.VITE_API_URL ?? "" export function posterUrl(path: string | undefined | null): string | undefined { if (!path) return undefined if (path.startsWith("http://") || path.startsWith("https://")) return path const clean = path.startsWith("/") ? path.slice(1) : path return `${API_URL}/images/${clean}` } export function tmdbProfileUrl(path: string | undefined | null): string | undefined { if (!path) return undefined return `https://image.tmdb.org/t/p/w185${path}` } export class ApiError extends Error { status: number body: string constructor(status: number, body: string) { super(`API ${status}: ${body}`) this.status = status this.body = body } } function authHeaders(): HeadersInit { const token = getToken() return token ? { Authorization: `Bearer ${token}` } : {} } function buildUrl( path: string, params?: Record, ): string { const base = `${API_URL}/api/v1${path}` if (!params) return base const sp = new URLSearchParams() for (const [k, v] of Object.entries(params)) { if (v != null) sp.set(k, String(v)) } const qs = sp.toString() return qs ? `${base}?${qs}` : base } let refreshPromise: Promise | null = null async function tryRefresh(): Promise { const auth = getAuth() if (!auth?.refresh_token) return false try { const res = await fetch(buildUrl("/auth/refresh"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: auth.refresh_token }), }) if (!res.ok) return false const data = await res.json() setAuth({ ...auth, token: data.token, refresh_token: data.refresh_token, expires_at: data.expires_at, }) return true } catch { return false } } async function isInvalidCredentials(res: Response): Promise { try { const body = await res.clone().json() return body?.code === "INVALID_CREDENTIALS" } catch { return false } } async function request( url: string, options?: RequestInit, ): Promise { const res = await fetch(url, { ...options, headers: { ...authHeaders(), ...options?.headers, }, }) if (!res.ok) { if (res.status === 401) { // A failed re-auth check (wrong password) is not an expired session: // no refresh, no retry, no logout. if (await isInvalidCredentials(res)) { throw new ApiError(res.status, await res.text()) } if (url.includes("/auth/refresh") || url.includes("/auth/login")) { clearAuth() window.location.href = "/app/login" throw new ApiError(res.status, await res.text()) } if (!refreshPromise) { refreshPromise = tryRefresh().finally(() => { refreshPromise = null }) } const ok = await refreshPromise if (!ok) { clearAuth() window.location.href = "/app/login" throw new ApiError(res.status, await res.text()) } const retryRes = await fetch(url, { ...options, headers: { ...authHeaders(), ...options?.headers, }, }) if (!retryRes.ok) { throw new ApiError(retryRes.status, await retryRes.text()) } const retryText = await retryRes.text() return retryText ? JSON.parse(retryText) : (undefined as T) } throw new ApiError(res.status, await res.text()) } const text = await res.text() return text ? JSON.parse(text) : (undefined as T) } export async function get( path: string, params?: Record, ): Promise { return request(buildUrl(path, params)) } export async function post( path: string, body?: unknown, ): Promise { return request(buildUrl(path), { method: "POST", headers: { "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined, }) } export async function put( path: string, body?: unknown, ): Promise { return request(buildUrl(path), { method: "PUT", headers: { "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined, }) } export async function putForm( path: string, form: FormData, ): Promise { return request(buildUrl(path), { method: "PUT", body: form, }) } export async function patch( path: string, body?: unknown, ): Promise { return request(buildUrl(path), { method: "PATCH", headers: { "Content-Type": "application/json" }, body: body ? JSON.stringify(body) : undefined, }) } export async function del(path: string): Promise { return request(buildUrl(path), { method: "DELETE" }) } export async function delWithBody( path: string, body: unknown, ): Promise { return request(buildUrl(path), { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }) } export async function upload( path: string, file: File, ): Promise { const form = new FormData() form.append("file", file) return request(buildUrl(path), { method: "POST", body: form, }) } export async function uploadWithFields( path: string, file: File, fields: Record, ): Promise { const form = new FormData() form.append("file", file) for (const [k, v] of Object.entries(fields)) form.append(k, v) return request(buildUrl(path), { method: "POST", body: form, }) }