import axios from 'axios'; const client = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json', }, timeout: 15000, }); /* ─── Token helpers ─── */ function getAccessToken() { return localStorage.getItem('accessToken'); } function getRefreshToken() { return localStorage.getItem('refreshToken'); } function parseJwt(token) { try { return JSON.parse(atob(token.split('.')[1])); } catch (e) { return null; } } function setTokens(accessToken, refreshToken) { localStorage.setItem('accessToken', accessToken); if (refreshToken) { localStorage.setItem('refreshToken', refreshToken); } // Sync the 'user' object in localStorage with the data from the new access token const payload = parseJwt(accessToken); if (payload) { const existingUser = JSON.parse(localStorage.getItem('user') || '{}'); // Ensure we don't overwrite user-specific static data if missing from JWT const updatedUser = { ...existingUser, ...payload }; // The user ID usually comes as 'sub' in JWT, but frontend expects 'id' if (payload.sub && !updatedUser.id) updatedUser.id = payload.sub; // Explicitly update role and email which are crucial for the UI if (payload.role) updatedUser.role = payload.role; if (payload.email) updatedUser.email = payload.email; localStorage.setItem('user', JSON.stringify(updatedUser)); } } function clearTokens() { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('user'); } /* ─── Request interceptor: attach access token ─── */ client.interceptors.request.use( (config) => { const token = getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); /* ─── Response interceptor: auto-refresh on 401 ─── */ let isRefreshing = false; let failedQueue = []; function processQueue(error, token) { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; } client.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; /* If 401 with TOKEN_EXPIRED and we haven't retried yet */ if ( error.response?.status === 401 && error.response?.data?.code === 'TOKEN_EXPIRED' && !originalRequest._retry ) { if (isRefreshing) { /* Queue this request until the refresh completes */ return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }) .then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return client(originalRequest); }) .catch((err) => Promise.reject(err)); } originalRequest._retry = true; isRefreshing = true; try { const refreshToken = getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token'); } /* Call refresh endpoint directly (bypass interceptor) */ const { data } = await axios.post('/api/auth/refresh', { refreshToken, }); setTokens(data.accessToken, data.refreshToken); processQueue(null, data.accessToken); originalRequest.headers.Authorization = `Bearer ${data.accessToken}`; return client(originalRequest); } catch (refreshError) { processQueue(refreshError, null); clearTokens(); /* Redirect to login */ window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } /* If 401 on non-refresh, clear and redirect */ if ( error.response?.status === 401 && !originalRequest.url?.includes('/auth/refresh') ) { clearTokens(); window.location.href = '/login'; } return Promise.reject(error); } ); export { client as default, setTokens, clearTokens, getAccessToken, getRefreshToken };