74 lines
1.9 KiB
TypeScript
74 lines
1.9 KiB
TypeScript
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api'
|
||
|
||
export function apiUrl(path: string) {
|
||
return `${API_BASE_URL}${path}`
|
||
}
|
||
|
||
let csrfToken: string | null = null
|
||
|
||
export class ApiError extends Error {
|
||
status: number
|
||
payload: unknown
|
||
|
||
constructor(status: number, message: string, payload: unknown) {
|
||
super(message)
|
||
this.name = 'ApiError'
|
||
this.status = status
|
||
this.payload = payload
|
||
}
|
||
}
|
||
|
||
async function readError(response: Response) {
|
||
let payload: unknown
|
||
try {
|
||
payload = await response.json()
|
||
} catch {
|
||
payload = null
|
||
}
|
||
const detail = payload && typeof payload === 'object' && 'detail' in payload
|
||
? String((payload as { detail: unknown }).detail)
|
||
: `API 请求失败(${response.status})`
|
||
return new ApiError(response.status, detail, payload)
|
||
}
|
||
|
||
async function getCsrfToken() {
|
||
if (!csrfToken) {
|
||
const response = await fetch(apiUrl('/accounts/auth/csrf'), {
|
||
credentials: 'include'
|
||
})
|
||
if (!response.ok) {
|
||
throw await readError(response)
|
||
}
|
||
const data = (await response.json()) as { csrfToken: string }
|
||
csrfToken = data.csrfToken
|
||
}
|
||
return csrfToken
|
||
}
|
||
|
||
export async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
|
||
const headers = new Headers(init?.headers)
|
||
if (init?.body && !(init.body instanceof FormData) && !headers.has('content-type')) {
|
||
headers.set('content-type', 'application/json')
|
||
}
|
||
const method = init?.method?.toUpperCase() ?? 'GET'
|
||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method) && !headers.has('x-csrftoken')) {
|
||
headers.set('x-csrftoken', await getCsrfToken())
|
||
}
|
||
|
||
const response = await fetch(apiUrl(path), {
|
||
...init,
|
||
credentials: 'include',
|
||
headers
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw await readError(response)
|
||
}
|
||
|
||
if (response.status === 204) {
|
||
return undefined as T
|
||
}
|
||
|
||
return response.json() as Promise<T>
|
||
}
|