File size: 2,920 Bytes
fc5156b
90adb95
 
 
 
fc5156b
165a274
 
 
 
 
 
 
 
 
 
 
 
fc5156b
90adb95
 
165a274
90adb95
165a274
fc5156b
 
 
 
 
 
 
 
 
 
 
 
 
165a274
 
 
 
 
 
 
 
 
90adb95
 
 
165a274
fc5156b
90adb95
 
 
 
 
165a274
 
90adb95
 
 
 
165a274
 
fc5156b
165a274
 
 
fc5156b
 
 
165a274
fc5156b
165a274
f2f300a
165a274
f22b512
 
165a274
 
 
 
fc5156b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 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 } }),
  history: (document_id) =>
    request(`/history${document_id ? `?document_id=${document_id}` : ""}`),
  evaluate: () => request("/evaluate", { method: "POST" }),
  seedDemo: () => request("/demo/seed", { method: "POST" }),
};