60 lines
1.9 KiB
TypeScript
Executable File
60 lines
1.9 KiB
TypeScript
Executable File
const BASE = "/api/v1";
|
|
|
|
// A non-sensitive flag in localStorage that tells the SPA whether the user has
|
|
// an active session. The actual JWT lives in an httpOnly cookie and is never
|
|
// readable by JavaScript. Clearing this flag is sufficient for client-side
|
|
// route guards; the server still validates the cookie on every request.
|
|
const SESSION_KEY = "rh_session";
|
|
|
|
export function markLoggedIn(): void {
|
|
localStorage.setItem(SESSION_KEY, "1");
|
|
}
|
|
|
|
export function markLoggedOut(): void {
|
|
localStorage.removeItem(SESSION_KEY);
|
|
}
|
|
|
|
export function isLoggedIn(): boolean {
|
|
return localStorage.getItem(SESSION_KEY) === "1";
|
|
}
|
|
|
|
async function request<T>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
isFormData = false
|
|
): Promise<T> {
|
|
const headers: Record<string, string> = {
|
|
...(options.headers as Record<string, string>),
|
|
};
|
|
if (!isFormData) {
|
|
headers["Content-Type"] = "application/json";
|
|
}
|
|
const resp = await fetch(`${BASE}${path}`, {
|
|
...options,
|
|
headers,
|
|
credentials: "include", // send httpOnly cookie on every request
|
|
});
|
|
if (!resp.ok) {
|
|
if (resp.status === 401) {
|
|
markLoggedOut();
|
|
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" }),
|
|
};
|