95 lines
2.3 KiB
TypeScript
95 lines
2.3 KiB
TypeScript
import { apiUrl } from '../lib/apiOrigin'
|
|
|
|
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 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}`)
|
|
}
|
|
|
|
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
|
|
}
|