Files
alrahma_web_client/src/api/http.ts
T
root b74ddca8b1
Web Client CI/CD / Lint (ESLint + TypeScript) (push) Successful in 28s
Web Client CI/CD / Build (tsc + Vite) (push) Successful in 23s
Web Client CI/CD / Deploy to shared hosting (push) Successful in 33s
add school year and security fix
2026-07-06 00:55:02 -04:00

108 lines
2.6 KiB
TypeScript

import { apiUrl } from '../lib/apiOrigin'
import { selectedSchoolYearHeaders } from '../lib/schoolYearSelection'
const TOKEN_KEY = 'alrahma_api_token'
export function getStoredToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}
export function setStoredToken(token: string | null): void {
if (token) localStorage.setItem(TOKEN_KEY, token)
else localStorage.removeItem(TOKEN_KEY)
}
export function decodeJwtPayload<T = unknown>(token: string): T | null {
try {
const payload = token.split('.')[1]
if (!payload) return null
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')
const json = decodeURIComponent(
atob(base64)
.split('')
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, '0')}`)
.join(''),
)
return JSON.parse(json) as T
} catch {
return null
}
}
export function applyStoredSchoolYearHeaders(headers: Headers): Headers {
for (const [key, value] of Object.entries(selectedSchoolYearHeaders())) {
if (!headers.has(key)) headers.set(key, value)
}
return headers
}
export class ApiHttpError extends Error {
readonly status: number
readonly body: unknown
constructor(message: string, status: number, body: unknown) {
super(message)
this.name = 'ApiHttpError'
this.status = status
this.body = body
}
}
export async function apiFetch<T>(
path: string,
init: RequestInit = {},
opts: { attachAuth?: boolean } = {},
): Promise<T> {
const attachAuth = opts.attachAuth !== false
const headers = new Headers(init.headers)
if (!headers.has('Accept')) {
headers.set('Accept', 'application/json')
}
const token = getStoredToken()
if (attachAuth && token && !headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${token}`)
}
if (attachAuth && token) {
applyStoredSchoolYearHeaders(headers)
}
const url = apiUrl(path)
let res: Response
try {
res = await fetch(url, {
...init,
headers,
})
} catch (e) {
const hint = import.meta.env.DEV
? ' Start the API and ensure Vite can reach it. Default proxy is http://localhost:8000 unless VITE_PROXY_API or VITE_API_ORIGIN is set.'
: ''
const reason = e instanceof Error ? e.message : 'Failed to fetch'
throw new ApiHttpError(`Network error: ${reason}.${hint}`, 0, null)
}
const body: unknown = await res.json().catch(() => null)
if (!res.ok) {
const msg =
typeof body === 'object' && body !== null && 'message' in body
? String((body as { message?: unknown }).message ?? '')
: ''
throw new ApiHttpError(msg || `HTTP ${res.status}`, res.status, body)
}
return body as T
}