| |
| |
| |
| |
| |
| |
|
|
| import { triggerLogin } from '@/hooks/useAuth'; |
|
|
| export interface ApiUploadProgress { |
| loaded: number; |
| total: number | null; |
| percent: number | null; |
| } |
|
|
| async function handleUnauthorized(response: Response): Promise<void> { |
| if (response.status !== 401) return; |
| try { |
| const authStatus = await fetch('/auth/status', { credentials: 'include' }); |
| const data = await authStatus.json(); |
| if (data.auth_enabled) { |
| triggerLogin(); |
| throw new Error('Authentication required — redirecting to login.'); |
| } |
| } catch (e) { |
| if (e instanceof Error && e.message.includes('redirecting')) throw e; |
| } |
| } |
|
|
| |
| export async function apiFetch( |
| path: string, |
| options: RequestInit = {} |
| ): Promise<Response> { |
| const headers = new Headers(options.headers); |
| const isFormData = options.body instanceof FormData; |
| if (!isFormData && !headers.has('Content-Type')) { |
| headers.set('Content-Type', 'application/json'); |
| } |
|
|
| const response = await fetch(path, { |
| ...options, |
| headers, |
| credentials: 'include', |
| }); |
|
|
| await handleUnauthorized(response); |
|
|
| return response; |
| } |
|
|
| function headersFromXhr(rawHeaders: string): Headers { |
| const headers = new Headers(); |
| rawHeaders.trim().split(/[\r\n]+/).forEach((line) => { |
| const separator = line.indexOf(':'); |
| if (separator <= 0) return; |
| headers.append( |
| line.slice(0, separator).trim(), |
| line.slice(separator + 1).trim(), |
| ); |
| }); |
| return headers; |
| } |
|
|
| export async function apiUpload( |
| path: string, |
| formData: FormData, |
| options: { onProgress?: (progress: ApiUploadProgress) => void } = {}, |
| ): Promise<Response> { |
| return new Promise<Response>((resolve, reject) => { |
| const xhr = new XMLHttpRequest(); |
| xhr.open('POST', path); |
| xhr.withCredentials = true; |
| xhr.upload.onprogress = (event) => { |
| const total = event.lengthComputable ? event.total : null; |
| const percent = total |
| ? Math.min(100, Math.round((event.loaded / total) * 100)) |
| : null; |
| options.onProgress?.({ loaded: event.loaded, total, percent }); |
| }; |
| xhr.onerror = () => reject(new Error('Network error while uploading.')); |
| xhr.onabort = () => reject(new Error('Dataset upload was canceled.')); |
| xhr.onload = () => { |
| const response = new Response(xhr.responseText, { |
| status: xhr.status, |
| statusText: xhr.statusText, |
| headers: headersFromXhr(xhr.getAllResponseHeaders()), |
| }); |
| handleUnauthorized(response).then(() => resolve(response)).catch(reject); |
| }; |
| xhr.send(formData); |
| }); |
| } |
|
|