Init
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// Fired when any API call returns 401 (session expired/absent). App registers it
|
||||
// to drop to the login screen.
|
||||
let onUnauthorized: (() => void) | null = null;
|
||||
export function setUnauthorizedHandler(fn: (() => void) | null): void {
|
||||
onUnauthorized = fn;
|
||||
}
|
||||
|
||||
interface ApiOptions extends RequestInit {
|
||||
// When true, a 401 body is returned to the caller instead of throwing +
|
||||
// firing the logout hook. Used by /login to surface { success, error }.
|
||||
allowAuthErrorBody?: boolean;
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(url: string, options?: ApiOptions): Promise<T> {
|
||||
const { allowAuthErrorBody, ...init } = options ?? {};
|
||||
const res = await fetch(url, init);
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
if (!allowAuthErrorBody) { onUnauthorized?.(); throw new Error('unauthenticated'); }
|
||||
// else fall through and return the { success, error } body
|
||||
} else if (res.status === 400 || res.status === 409) {
|
||||
// Validation / conflict responses carry a { error, message } body the caller reads.
|
||||
} else {
|
||||
const text = await res.text().catch(() => res.statusText);
|
||||
throw new Error(text || `HTTP ${res.status}`);
|
||||
}
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function apiGet<T>(url: string): Promise<T> {
|
||||
return apiFetch<T>(url);
|
||||
}
|
||||
|
||||
export function apiPost<T>(url: string, body: unknown, options?: ApiOptions): Promise<T> {
|
||||
return apiFetch<T>(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user