Files
bssapp/ui/src/services/api.js

150 lines
3.5 KiB
JavaScript

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