MedRAG / frontend /src /api /client.js
hetsheta's picture
Allow VITE_API_BASE_URL to configure production backend URL
44ed713
Raw
History Blame Contribute Delete
4.98 kB
import axios from 'axios'
const apiBaseURL = import.meta.env.VITE_API_BASE_URL || ''
const api = axios.create({ baseURL: `${apiBaseURL}/api/v1` })
const AUTH_PATHS = ['/auth/login', '/auth/register', '/auth/refresh']
// Attach access token to every request
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// Auto-refresh on 401
api.interceptors.response.use(
(res) => res,
async (err) => {
const original = err.config || {}
const requestUrl = original.url || ''
const isAuthRequest = AUTH_PATHS.some((path) => requestUrl.includes(path))
if (err.response?.status === 401 && !original._retry && !isAuthRequest) {
original._retry = true
try {
const refresh = localStorage.getItem('refresh_token')
if (!refresh) throw new Error('Missing refresh token')
const { data } = await axios.post(`${apiBaseURL}/api/v1/auth/refresh`, { refresh_token: refresh })
localStorage.setItem('access_token', data.access_token)
localStorage.setItem('refresh_token', data.refresh_token)
original.headers.Authorization = `Bearer ${data.access_token}`
return api(original)
} catch {
localStorage.clear()
window.location.href = '/login'
}
}
return Promise.reject(err)
}
)
export function getApiErrorMessage(err, fallback = 'Something went wrong') {
const detail = err?.response?.data?.detail
if (typeof detail === 'string' && detail.trim()) return detail
if (Array.isArray(detail) && detail.length) {
return detail.map((item) => item?.msg || item).filter(Boolean).join(', ')
}
return fallback
}
// ─── Auth ──────────────────────────────────────────────────────────────────────
export const authApi = {
register: (d) => api.post('/auth/register', d).then(r => r.data),
login: (d) => api.post('/auth/login', d).then(r => r.data),
me: () => api.get('/auth/me').then(r => r.data),
}
// ─── Conversations ─────────────────────────────────────────────────────────────
export const convApi = {
list: () => api.get('/conversations').then(r => r.data),
create: (title = 'New Chat') => api.post('/conversations', { title }).then(r => r.data),
messages: (id) => api.get(`/conversations/${id}/messages`).then(r => r.data),
rename: (id, title) => api.patch(`/conversations/${id}`, { title }).then(r => r.data),
delete: (id) => api.delete(`/conversations/${id}`),
}
// ─── Query ─────────────────────────────────────────────────────────────────────
export const queryApi = {
ask: (payload) => api.post('/query', payload).then(r => r.data),
}
export const audioApi = {
transcribe: (formData) => api.post('/audio/transcribe', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
}).then(r => r.data),
speak: (payload) => api.post('/audio/speak', payload, {
responseType: 'blob',
}).then(r => r.data),
}
// ─── Documents ─────────────────────────────────────────────────────────────────
export const docsApi = {
list: (conversationId) => api.get('/documents', {
params: conversationId ? { conversation_id: conversationId } : undefined,
}).then(r => r.data),
upload: (formData) => api.post('/documents/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
}).then(r => r.data),
clearConversation: (conversationId) => api.delete(`/documents/conversation/${conversationId}`),
delete: (id) => api.delete(`/documents/${id}`),
}
// ─── Admin ─────────────────────────────────────────────────────────────────────
export const adminApi = {
stats: () => api.get('/admin/stats').then(r => r.data),
users: () => api.get('/admin/users').then(r => r.data),
setRole: (id, role) => api.patch(`/admin/users/${id}/role`, null, { params: { role } }).then(r => r.data),
toggle: (id) => api.patch(`/admin/users/${id}/toggle`).then(r => r.data),
}
// ─── Health ────────────────────────────────────────────────────────────────────
export const healthApi = {
check: () => api.get('/health').then(r => r.data),
}
export default api