import axios from 'axios' import qs from 'qs' import { useAuthStore } from 'stores/authStore' export const API_BASE_URL = '/api' const api = axios.create({ baseURL: API_BASE_URL, timeout: 180000, paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' }), withCredentials: true }) api.interceptors.request.use((config) => { const auth = useAuthStore() const url = config.url || '' const isPublic = url.startsWith('/auth/login') || url.startsWith('/auth/refresh') || url.startsWith('/password/forgot') || url.startsWith('/password/reset') if (!isPublic && auth?.token) { config.headers ||= {} config.headers.Authorization = `Bearer ${auth.token}` } return config }) let isLoggingOut = false api.interceptors.response.use( r => r, async (error) => { const status = error?.response?.status const requestUrl = String(error?.config?.url || '') const hasBlob = typeof Blob !== 'undefined' && error?.response?.data instanceof Blob const isPasswordChangeRequest = requestUrl.startsWith('/password/change') || requestUrl.startsWith('/me/password') if ((status >= 500 || hasBlob) && error) { const method = String(error?.config?.method || 'GET').toUpperCase() const detail = await extractApiErrorDetail(error) error.parsedMessage = detail console.error(`API ${status || '-'} ${method} ${requestUrl}: ${detail}`) } // Password change endpoints may return 401 for business reasons // (for example current password mismatch). Keep session in that case. if (status === 401 && !isPasswordChangeRequest && !isLoggingOut) { isLoggingOut = true try { useAuthStore().clearSession() } finally { isLoggingOut = false } } return Promise.reject(error) } ) export const get = (u, p = {}, c = {}) => api.get(u, { params: p, ...c }).then(r => r.data) export const post = (u, b = {}, c = {}) => api.post(u, b, c).then(r => r.data) export const put = (u, b = {}, c = {}) => api.put(u, b, c).then(r => r.data) export const del = (u, p = {}, c = {}) => api.delete(u, { params: p, ...c }).then(r => r.data) async function parseBlobErrorMessage(data) { if (!data) return '' if (typeof Blob !== 'undefined' && data instanceof Blob) { try { const text = (await data.text())?.trim() if (!text) return '' try { const json = JSON.parse(text) return ( json?.detail || json?.message || json?.error || text ) } catch { return text } } catch { return '' } } if (typeof data === 'string') return data.trim() if (typeof data === 'object') { return ( data?.detail || data?.message || data?.error || '' ) } return '' } export async function extractApiErrorDetail(err) { let detail = err?.parsedMessage || err?.response?.data?.detail || err?.response?.data?.message || err?.response?.data?.error || '' if (!detail) { detail = await parseBlobErrorMessage(err?.response?.data) } if (!detail) { detail = err?.message || 'Request failed' } return detail } export const download = async (u, p = {}, c = {}) => { try { const r = await api.get(u, { params: p, responseType: 'blob', ...c }) return r.data } catch (err) { const detail = await extractApiErrorDetail(err) const wrapped = new Error(detail) wrapped.status = err?.response?.status wrapped.original = err throw wrapped } } export default api