Spaces:
Sleeping
Sleeping
| import { API_BASE } from '../utils/constants.js' | |
| let _authToken = null | |
| let _refreshToken = null | |
| /** | |
| * Set the current auth token to include in all API requests. | |
| */ | |
| export function setAuthToken(token) { | |
| _authToken = token | |
| } | |
| /** | |
| * Set a callback to refresh the token when a 401 is received. | |
| * The callback should return the new token (or null if refresh fails). | |
| */ | |
| export function setTokenRefresh(fn) { | |
| _refreshToken = fn | |
| } | |
| /** | |
| * Get the auth headers object for direct fetch calls (e.g. chat SSE). | |
| */ | |
| export function getAuthHeaders() { | |
| return _authToken ? { Authorization: `Bearer ${_authToken}` } : {} | |
| } | |
| /** | |
| * Try to refresh the auth token via the registered callback. | |
| * Returns true if the token was refreshed, false otherwise. | |
| */ | |
| export async function refreshAuthToken() { | |
| if (!_refreshToken) return false | |
| try { | |
| const newToken = await _refreshToken() | |
| if (newToken) { | |
| _authToken = newToken | |
| return true | |
| } | |
| } catch { | |
| // Refresh failed | |
| } | |
| return false | |
| } | |
| function authHeaders() { | |
| return getAuthHeaders() | |
| } | |
| async function request(url, options = {}) { | |
| let retries = 0 | |
| const doFetch = async () => { | |
| const res = await fetch(`${API_BASE}${url}`, { | |
| headers: { 'Content-Type': 'application/json', ...authHeaders(), ...options.headers }, | |
| ...options, | |
| }) | |
| // On 401, try refreshing the token once and retry | |
| if (res.status === 401 && retries < 1) { | |
| retries++ | |
| const refreshed = await refreshAuthToken() | |
| if (refreshed) { | |
| return doFetch() | |
| } | |
| } | |
| if (!res.ok) { | |
| throw new Error(`API error: ${res.status} ${res.statusText}`) | |
| } | |
| return res.json() | |
| } | |
| return doFetch() | |
| } | |
| export async function get(url) { | |
| return request(url, { method: 'GET' }) | |
| } | |
| export async function post(url, body) { | |
| return request(url, { method: 'POST', body: JSON.stringify(body) }) | |
| } | |
| export async function patch(url, body) { | |
| return request(url, { method: 'PATCH', body: JSON.stringify(body) }) | |
| } | |
| export async function del(url) { | |
| return request(url, { method: 'DELETE' }) | |
| } | |
| /** | |
| * Upload a file (multipart/form-data) to the API with auth. | |
| */ | |
| export async function uploadFile(url, file, params = {}) { | |
| const formData = new FormData() | |
| formData.append('file', file) | |
| const query = new URLSearchParams(params).toString() | |
| const fullUrl = query ? `${url}?${query}` : url | |
| let retries = 0 | |
| const doFetch = async () => { | |
| const res = await fetch(`${API_BASE}${fullUrl}`, { | |
| method: 'POST', | |
| headers: authHeaders(), | |
| body: formData, | |
| }) | |
| // On 401, try refreshing the token once and retry | |
| if (res.status === 401 && retries < 1) { | |
| retries++ | |
| // FormData can only be sent once, need to recreate | |
| const retryFormData = new FormData() | |
| retryFormData.append('file', file) | |
| const refreshed = await refreshAuthToken() | |
| if (refreshed) { | |
| return doFetch() | |
| } | |
| } | |
| if (!res.ok) { | |
| const body = await res.text().catch(() => '') | |
| throw new Error(`Upload failed (${res.status}): ${body}`) | |
| } | |
| return res.json() | |
| } | |
| return doFetch() | |
| } | |