app / frontend /src /api.js
Aissugur Kurganbekov
Claude Opus 4.8
Tamga — evidence-backed customs document audit
373030b
Raw History Blame Contribute Delete
3.03 kB
// Thin API client. All requests go through the Vite proxy at /api -> backend.
// A JWT access token (from login/register/google) is kept in localStorage and
// sent as `Authorization: Bearer <token>`. A 401 on an AUTHENTICATED request
// (one that carried a token) clears it and notifies the app with a reason; a 401
// on a login attempt (no token) just throws so the form shows the error inline.
const BASE = "/api";
const TOKEN_KEY = "ff_token";
export const tokenStore = {
get: () => localStorage.getItem(TOKEN_KEY),
set: (t) => localStorage.setItem(TOKEN_KEY, t),
clear: () => localStorage.removeItem(TOKEN_KEY),
};
let onUnauthorized = null;
export function setOnUnauthorized(fn) {
onUnauthorized = fn;
}
async function handle(res, hadToken) {
if (res.status === 401 && hadToken) {
tokenStore.clear();
onUnauthorized?.("Your session expired — please sign in again.");
}
if (!res.ok) {
let detail = res.statusText;
try {
const body = await res.json();
detail = body.detail || detail;
} catch (_) {
/* ignore parse errors */
}
throw new Error(detail);
}
return res.json();
}
function request(path, { method = "GET", body, json } = {}) {
const headers = {};
const token = tokenStore.get();
if (token) headers["Authorization"] = `Bearer ${token}`;
let payload = body;
if (json !== undefined) {
headers["Content-Type"] = "application/json";
payload = JSON.stringify(json);
}
return fetch(`${BASE}${path}`, { method, headers, body: payload }).then((res) =>
handle(res, !!token)
);
}
const storeToken = (r) => {
tokenStore.set(r.access_token);
return r;
};
export const api = {
// ---- Auth ----
register: (creds) => request("/auth/register", { method: "POST", json: creds }).then(storeToken),
login: (creds) => request("/auth/login", { method: "POST", json: creds }).then(storeToken),
googleLogin: (credential) =>
request("/auth/google", { method: "POST", json: { credential } }).then(storeToken),
me: () => request("/auth/me"),
logout: () => tokenStore.clear(),
// ---- App ----
health: () => request("/health"),
listDocuments: () => request("/documents"),
uploadDocument: (file) => {
const form = new FormData();
form.append("file", file);
return request("/documents/upload", { method: "POST", body: form });
},
deleteDocument: (id) => request(`/documents/${id}`, { method: "DELETE" }),
ask: (document_id, question, mode = "fast") =>
request("/ask", { method: "POST", json: { document_id, question, mode } }),
audit: (document_id, audit_type = "general") =>
request("/audit", { method: "POST", json: { document_id, audit_type } }),
auditPacket: (document_ids) =>
request("/audit/packet", { method: "POST", json: { document_ids } }),
history: (document_id) =>
request(`/history${document_id ? `?document_id=${document_id}` : ""}`),
evaluate: () => request("/evaluate", { method: "POST" }),
seedDemo: () => request("/demo/seed", { method: "POST" }),
};