import axios from 'axios' import { useAuth } from '~/composables/useAuth'; const config = useRuntimeConfig() const { sessionData } = useAuth(); const api = axios.create({ baseURL: config.public.baseUrl }) // Flag to prevent multiple simultaneous refresh attempts let isRefreshing = false let failedQueue: any[] = [] const processQueue = (error: any, token: string | null = null) => { failedQueue.forEach(prom => { if (error) { prom.reject(error) } else { prom.resolve(token) } }) failedQueue = [] } const refreshAccessToken = async (): Promise => { try { const refreshToken = localStorage.getItem('refreshToken') if (!refreshToken) { throw new Error('No refresh token available') } const response = await fetch(config.public.baseUrl+"/api/v1/auth/refresh", { method: 'POST', body: JSON.stringify({ refresh_token: refreshToken, provider: sessionData.value?.user?.auth_provider || 'keycloak' }), }) const responseBody = await response.json() const data = responseBody.data // Update tokens in localStorage if (data.access_token) { localStorage.setItem('accessToken', data.access_token) } if (data.refresh_token) { localStorage.setItem('refreshToken', data.refresh_token) } // Update server-side session with new tokens try { await fetch('/api/auth/sessionUserStore', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ accessToken: data.access_token, refreshToken: data.refresh_token, // expiresAt: expiresAt }), }) console.log('✅ Server session updated with new tokens') } catch (sessionError) { console.error('⚠️ Failed to update server session:', sessionError) // Don't throw error here - token refresh was successful, session update is secondary } console.log('✅ Token refreshed successfully') return data.access_token } catch (error) { console.error('❌ Token refresh failed:', error) return null } } // Shared request interceptor const requestInterceptor = (config: any) => { // Only access localStorage on client-side (avoid SSR errors) if (process.client) { const accessToken = localStorage.getItem('accessToken') if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}` } } return config } // Shared response interceptor const responseInterceptor = (response: any) => response const responseErrorInterceptor = async (error: any) => { const originalRequest = error.config // centralized error console.error('API Error:', error.response) // Handle token expiration on client-side if (process.client && error.response) { const { status } = error.response // Check if response is 401 (token expired or unauthorized) if (status === 401 && !originalRequest._retry) { 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 axios(originalRequest) }) .catch(err => { return Promise.reject(err) }) } originalRequest._retry = true isRefreshing = true // Try to refresh the token const newToken = await refreshAccessToken() if (newToken) { // Token refresh successful, retry original request isRefreshing = false processQueue(null, newToken) originalRequest.headers['Authorization'] = 'Bearer ' + newToken return axios(originalRequest) } else { // Token refresh failed (refresh token expired or error), logout user isRefreshing = false processQueue(new Error('Token refresh failed'), null) // Clear expired tokens localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') // Call logout endpoint to clear session fetch('/api/auth/logout', { method: 'POST' }) .catch(err => console.error('Logout failed:', err)) // Redirect to login with error message const errorMessage = encodeURIComponent('Token is expired') window.location.href = `/auth/login?error=${errorMessage}` return Promise.reject(error) } } } return Promise.reject(error) } api.interceptors.request.use(requestInterceptor) api.interceptors.response.use(responseInterceptor, responseErrorInterceptor) export default api