SPB Git

spb/autoscholar

Public
2.1 KB · 65 lines typescript
1"use client";23let csrf: string | null = null;45export class ApiError extends Error {6  constructor(7    readonly status: number,8    readonly code: string,9    message: string,10    readonly details?: unknown,11  ) {12    super(message);13  }14}1516export async function getCsrf(force = false): Promise<string> {17  if (csrf && !force) return csrf;18  const r = await fetch("/api/admin/auth/session", { cache: "no-store" });19  if (!r.ok) throw new ApiError(401, "unauthorized", "Session expired. Please sign in again.");20  const j = (await r.json()) as { csrfToken: string };21  csrf = j.csrfToken;22  return csrf;23}24export function setCsrf(token: string | null) {25  csrf = token;26}2728/** Fetch wrapper for admin mutations: attaches the CSRF token, parses uniform errors (401/403/429). */29export async function api<T>(30  path: string,31  init: RequestInit & { json?: unknown; form?: FormData } = {},32): Promise<T> {33  const method = init.method ?? (init.json !== undefined || init.form ? "POST" : "GET");34  const headers: Record<string, string> = { ...(init.headers as Record<string, string>) };35  if (method !== "GET" && method !== "HEAD") headers["x-csrf-token"] = await getCsrf();36  let body: BodyInit | undefined = init.body as BodyInit | undefined;37  if (init.json !== undefined) {38    headers["content-type"] = "application/json";39    body = JSON.stringify(init.json);40  } else if (init.form) body = init.form;41  const r = await fetch(path, { ...init, method, headers, body, cache: "no-store" });42  const text = await r.text();43  let j: unknown = null;44  try {45    j = text ? JSON.parse(text) : null;46  } catch {47    j = null;48  }49  if (!r.ok) {50    const e = (j as { error?: { code?: string; message?: string; details?: unknown } } | null)?.error;51    if (r.status === 401) {52      setCsrf(null);53      if (typeof window !== "undefined") window.location.href = "/admin/login";54    }55    if (r.status === 403 && e?.message?.includes("CSRF")) setCsrf(null);56    throw new ApiError(57      r.status,58      e?.code ?? "error",59      e?.message ?? (r.status === 429 ? "Too many requests. Slow down." : `HTTP ${r.status}`),60      e?.details,61    );62  }63  return j as T;64}65
Files · main
Menu

Theme