93 lines
2.2 KiB
JavaScript
93 lines
2.2 KiB
JavaScript
const API_BASE_URL = String(import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '')
|
|
|
|
let accessToken = ''
|
|
let refreshHandler = null
|
|
|
|
export function setAccessToken(token) {
|
|
accessToken = token || ''
|
|
}
|
|
|
|
export function clearAccessToken() {
|
|
accessToken = ''
|
|
}
|
|
|
|
export function setRefreshHandler(handler) {
|
|
refreshHandler = handler
|
|
}
|
|
|
|
function buildUrl(path) {
|
|
if (!API_BASE_URL) return path
|
|
if (/^https?:\/\//i.test(path)) return path
|
|
return `${API_BASE_URL}${path.startsWith('/') ? '' : '/'}${path}`
|
|
}
|
|
|
|
async function readBody(res) {
|
|
if (res.status === 204) return null
|
|
const contentType = res.headers.get('content-type') || ''
|
|
if (contentType.includes('application/json')) return res.json()
|
|
return res.text()
|
|
}
|
|
|
|
function normalizeError(data) {
|
|
if (!data) return new Error('Request failed')
|
|
if (typeof data === 'string') return new Error(data)
|
|
if (typeof data?.message === 'string') return new Error(data.message)
|
|
return new Error('Request failed')
|
|
}
|
|
|
|
async function tryRefresh() {
|
|
if (!refreshHandler) return false
|
|
try {
|
|
const token = await refreshHandler()
|
|
if (!token) {
|
|
clearAccessToken()
|
|
return false
|
|
}
|
|
setAccessToken(token)
|
|
return true
|
|
} catch {
|
|
clearAccessToken()
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function apiRequest(
|
|
path,
|
|
{ method = 'GET', body, headers = {}, skipAuth = false, skipRefresh = false } = {},
|
|
) {
|
|
const url = buildUrl(path)
|
|
|
|
const finalHeaders = new Headers(headers)
|
|
if (!skipAuth && accessToken) {
|
|
finalHeaders.set('Authorization', `Bearer ${accessToken}`)
|
|
}
|
|
|
|
let finalBody = body
|
|
if (body && !(body instanceof FormData) && typeof body === 'object') {
|
|
finalHeaders.set('Content-Type', 'application/json')
|
|
finalBody = JSON.stringify(body)
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: finalHeaders,
|
|
body: finalBody,
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (res.status === 401 && !skipRefresh) {
|
|
const refreshed = await tryRefresh()
|
|
if (refreshed) {
|
|
return apiRequest(path, { method, body, headers, skipAuth, skipRefresh: true })
|
|
}
|
|
}
|
|
|
|
const data = await readBody(res)
|
|
|
|
if (!res.ok) {
|
|
throw normalizeError(data)
|
|
}
|
|
|
|
return data
|
|
}
|