File size: 4,468 Bytes
bc45e54
 
 
034d344
 
 
 
 
 
bc45e54
 
 
 
 
034d344
bc45e54
034d344
 
 
 
 
 
bc45e54
 
 
 
034d344
bc45e54
 
 
 
 
 
 
 
 
c6bdd55
 
 
 
 
 
 
 
 
 
bc45e54
 
 
 
 
0c45ae1
 
 
 
 
 
 
 
 
bc45e54
 
0c45ae1
bc45e54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c2c3b04
 
 
 
 
 
 
 
 
bc45e54
 
 
 
 
 
 
 
 
f66acbf
bc45e54
 
 
 
 
 
 
 
 
 
 
 
 
f66acbf
 
 
bc45e54
 
25cb9ab
 
 
 
 
 
 
 
 
 
 
 
bc45e54
 
 
 
 
 
 
 
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { API_BASE_URL } from "./config";

const TOKEN_KEY = "classlens_token";
// Student and teacher logins share one token slot, so we track which kind of
// account it belongs to — lets the app pick the right "am I logged in" check
// (student vs teacher) before the first API call comes back.
const ROLE_KEY = "classlens_role";

export type UserRole = "student" | "teacher";

export function getToken(): string | null {
  return localStorage.getItem(TOKEN_KEY);
}

export function setToken(token: string, role: UserRole) {
  localStorage.setItem(TOKEN_KEY, token);
  localStorage.setItem(ROLE_KEY, role);
}

export function getRole(): UserRole | null {
  const role = localStorage.getItem(ROLE_KEY);
  return role === "student" || role === "teacher" ? role : null;
}

export function clearToken() {
  localStorage.removeItem(TOKEN_KEY);
  localStorage.removeItem(ROLE_KEY);
}

function authHeaders(): Record<string, string> {
  const token = getToken();
  return token ? { Authorization: `Bearer ${token}` } : {};
}

async function handleResponse<T>(res: Response): Promise<T> {
  if (res.status === 401) {
    // Only treat this as an expired session (clear + reload) if the request
    // actually carried a token. A login/register call sends no token, so a
    // 401 there means "wrong credentials," not "session expired" — let it
    // fall through so the caller can show the real error message instead of
    // silently reloading the login page.
    if (getToken()) {
      clearToken();
      window.location.reload();
      throw new Error("Unauthorized");
    }
  }
  if (!res.ok) {
    const text = await res.text();
    let msg = text;
    try {
      const j: unknown = JSON.parse(text);
      if (j && typeof j === "object") {
        const obj = j as { detail?: unknown; message?: unknown };
        const detail = obj.detail ?? obj.message;
        if (typeof detail === "string") msg = detail;
      }
    } catch {
      // Response body wasn't JSON — fall back to the raw text already in msg.
    }
    throw new Error(msg);
  }
  return (await res.json()) as T;
}

export async function apiGet<T>(path: string): Promise<T> {
  const res = await fetch(`${API_BASE_URL}${path}`, {
    headers: { ...authHeaders() },
  });
  return handleResponse<T>(res);
}

export async function apiPost<T>(path: string, body?: unknown): Promise<T> {
  const res = await fetch(`${API_BASE_URL}${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json", ...authHeaders() },
    body: body ? JSON.stringify(body) : undefined,
  });
  return handleResponse<T>(res);
}

export async function apiPatch<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(`${API_BASE_URL}${path}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", ...authHeaders() },
    body: JSON.stringify(body),
  });
  return handleResponse<T>(res);
}

export async function apiPut<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(`${API_BASE_URL}${path}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json", ...authHeaders() },
    body: JSON.stringify(body),
  });
  return handleResponse<T>(res);
}

export async function apiDelete<T = void>(path: string): Promise<T> {
  const res = await fetch(`${API_BASE_URL}${path}`, {
    method: "DELETE",
    headers: { ...authHeaders() },
  });
  if (res.status === 401) {
    clearToken();
    window.location.reload();
    throw new Error("Unauthorized");
  }
  if (!res.ok) {
    const text = await res.text();
    throw new Error(text);
  }
  const ct = res.headers.get("content-type") || "";
  if (ct.includes("application/json")) return res.json() as Promise<T>;
  return undefined as T;
}

/** Fetches a binary response (e.g. a stored PDF/image) as a blob, attaching
 * the same bearer auth header as the JSON helpers above — a plain <img>/
 * <iframe> src can't carry that header, so callers create an object URL from
 * this instead. */
export async function apiGetBlob(path: string): Promise<Blob> {
  const res = await fetch(`${API_BASE_URL}${path}`, {
    headers: { ...authHeaders() },
  });
  if (!res.ok) throw new Error(await res.text());
  return res.blob();
}

export async function apiUpload<T>(path: string, formData: FormData): Promise<T> {
  const res = await fetch(`${API_BASE_URL}${path}`, {
    method: "POST",
    headers: { ...authHeaders() },
    body: formData,
  });
  return handleResponse<T>(res);
}