File size: 2,823 Bytes
79b2fcc
 
 
 
 
 
 
 
 
2b4c539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79b2fcc
 
 
 
 
2b4c539
 
 
 
 
79b2fcc
 
 
 
 
 
 
2b4c539
79b2fcc
 
2b4c539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
 * Centralized API utilities.
 *
 * In production: HttpOnly cookie (hf_access_token) is sent automatically.
 * In development: auth is bypassed on the backend.
 */

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;
  }
}

/** Wrapper around fetch with credentials and common headers. */
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', // Send cookies with every request
  });

  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);
  });
}