import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios' const api = axios.create({ baseURL: '', headers: { 'Content-Type': 'application/json', }, }) // Flag to prevent multiple refresh attempts let isRefreshing = false let failedQueue: Array<{ resolve: (value?: unknown) => void reject: (reason?: unknown) => void }> = [] const processQueue = (error: Error | null, token: string | null = null) => { failedQueue.forEach((prom) => { if (error) { prom.reject(error) } else { prom.resolve(token) } }) failedQueue = [] } // Helper to get tokens from storage const getStoredTokens = () => { const stored = localStorage.getItem('auth-storage') if (stored) { try { const { state } = JSON.parse(stored) return { accessToken: state?.token, refreshToken: state?.refreshToken, } } catch { return { accessToken: null, refreshToken: null } } } return { accessToken: null, refreshToken: null } } // Helper to update stored tokens const updateStoredTokens = (accessToken: string, refreshToken: string) => { const stored = localStorage.getItem('auth-storage') if (stored) { try { const data = JSON.parse(stored) data.state.token = accessToken data.state.refreshToken = refreshToken localStorage.setItem('auth-storage', JSON.stringify(data)) } catch { // Ignore parse errors } } } // Add auth token to requests api.interceptors.request.use((config) => { const { accessToken } = getStoredTokens() if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}` } return config }) // Handle auth errors with automatic refresh api.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean } // If 401 and we haven't retried yet if (error.response?.status === 401 && !originalRequest._retry) { // Don't try to refresh if this was the refresh endpoint itself if (originalRequest.url === '/api/auth/refresh') { localStorage.removeItem('auth-storage') window.location.href = '/login' return Promise.reject(error) } if (isRefreshing) { // If already refreshing, queue this request return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }) }) .then((token) => { originalRequest.headers.Authorization = `Bearer ${token}` return api(originalRequest) }) .catch((err) => Promise.reject(err)) } originalRequest._retry = true isRefreshing = true const { refreshToken } = getStoredTokens() if (!refreshToken) { localStorage.removeItem('auth-storage') window.location.href = '/login' return Promise.reject(error) } try { // Call refresh endpoint const response = await axios.post('/api/auth/refresh', { refresh_token: refreshToken, }) const { access_token, refresh_token: newRefreshToken } = response.data // Update stored tokens updateStoredTokens(access_token, newRefreshToken) // Update auth header for retry originalRequest.headers.Authorization = `Bearer ${access_token}` // Process queued requests processQueue(null, access_token) // Retry original request return api(originalRequest) } catch (refreshError) { // Refresh failed - logout processQueue(refreshError as Error, null) localStorage.removeItem('auth-storage') window.location.href = '/login' return Promise.reject(refreshError) } finally { isRefreshing = false } } return Promise.reject(error) } ) export default api