/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import axios, { type AxiosRequestConfig } from 'axios'
import { t } from 'i18next'
import { toast } from 'sonner'
import {
applyAuthRotation,
clearAuthentication,
refreshAuthentication,
} from '@/lib/auth-session'
import { getServerErrorMessageKey } from '@/lib/server-error-message'
import { useAuthStore } from '@/stores/auth-store'
declare module 'axios' {
export interface AxiosRequestConfig {
skipBusinessError?: boolean
skipErrorHandler?: boolean
disableDuplicate?: boolean
skipAuthRefresh?: boolean
authRetry?: boolean
acceptAuthRotation?: boolean
}
}
export type ApiRequestConfig = AxiosRequestConfig
export const api = axios.create({
baseURL: '',
withCredentials: true,
headers: {
'Cache-Control': 'no-store',
},
})
const inFlightGet = new Map>()
const originalGet = api.get.bind(api)
api.get = ((url: string, config: ApiRequestConfig = {}) => {
if (config.disableDuplicate) return originalGet(url, config)
const params = config.params ? JSON.stringify(config.params) : '{}'
const sessionSID = useAuthStore.getState().auth.session?.sid || 'anonymous'
const key = `${sessionSID}:${url}?${params}`
const existingRequest = inFlightGet.get(key)
if (existingRequest) return existingRequest
const request = originalGet(url, config).finally(() => {
inFlightGet.delete(key)
})
inFlightGet.set(key, request)
return request
}) as typeof api.get
function redirectToSignIn(): void {
if (
typeof window !== 'undefined' &&
window.location.pathname !== '/sign-in'
) {
window.location.replace('/sign-in')
}
}
api.interceptors.response.use(
(response) => {
if (response.config.acceptAuthRotation && response.data?.success === true) {
applyAuthRotation(response.data.data)
}
if (
!response.config.skipBusinessError &&
typeof response.data?.success === 'boolean' &&
!response.data.success
) {
const messageKey = getServerErrorMessageKey(response.data)
toast.error(
messageKey
? t(messageKey)
: response.data.message || t('Request failed')
)
}
return response
},
async (error) => {
const config = error?.config as ApiRequestConfig | undefined
const skipErrorHandler = config?.skipErrorHandler
const status = error?.response?.status
if (status === 401) {
if (config && !config.skipAuthRefresh && !config.authRetry) {
config.authRetry = true
const outcome = await refreshAuthentication()
if (outcome.kind === 'authenticated') {
const token = useAuthStore.getState().auth.accessToken
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
}
}
return api.request(config)
}
if (outcome.kind === 'anonymous' || outcome.kind === 'out_of_sync') {
if (!skipErrorHandler) toast.error(t('Session expired!'))
redirectToSignIn()
}
} else if (config?.authRetry) {
clearAuthentication(false)
if (!skipErrorHandler) toast.error(t('Session expired!'))
redirectToSignIn()
} else if (!skipErrorHandler) {
toast.error(t('Session expired!'))
}
} else if (!skipErrorHandler) {
const messageKey = getServerErrorMessageKey(error)
const message = messageKey
? t(messageKey)
: error?.response?.data?.message ||
error?.message ||
t('Request failed')
toast.error(message)
}
throw error
}
)
api.interceptors.request.use((config) => {
const accessToken = useAuthStore.getState().auth.accessToken
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`
}
return config
})