Files
rehearshalhub/web/src/api/client.ts
Mistral Vibe cd6fabb31c fix: correct avatar upload and DiceBear URL version
- Add api.upload() to client.ts that passes FormData without setting
  Content-Type, letting the browser set multipart/form-data with the
  correct boundary (was causing 422 on the upload endpoint)
- Use api.upload() instead of api.post() for avatar file upload
- Update DiceBear URLs from v6 to 9.x in both frontend and backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:17:21 +02:00

54 lines
1.6 KiB
TypeScript

const BASE = "/api/v1";
function getToken(): string | null {
return localStorage.getItem("rh_token");
}
export function setToken(token: string): void {
localStorage.setItem("rh_token", token);
}
export function clearToken(): void {
localStorage.removeItem("rh_token");
}
async function request<T>(
path: string,
options: RequestInit = {},
isFormData = false
): Promise<T> {
const token = getToken();
const headers: Record<string, string> = {
...(options.headers as Record<string, string>),
};
if (!isFormData) {
headers["Content-Type"] = "application/json";
}
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const resp = await fetch(`${BASE}${path}`, { ...options, headers });
if (!resp.ok) {
if (resp.status === 401) {
clearToken();
window.location.href = "/login";
throw new Error("Session expired");
}
const error = await resp.json().catch(() => ({ detail: resp.statusText }));
throw new Error(error.detail ?? resp.statusText);
}
if (resp.status === 204) return undefined as T;
return resp.json();
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) =>
request<T>(path, { method: "POST", body: JSON.stringify(body) }),
upload: <T>(path: string, formData: FormData) =>
request<T>(path, { method: "POST", body: formData }, true),
patch: <T>(path: string, body: unknown) =>
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
delete: (path: string) => request<void>(path, { method: "DELETE" }),
};