File size: 3,131 Bytes
7949d20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa3f9d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7949d20
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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()
}