Spaces:
Running
Running
| import axios from "axios"; | |
| const API_BASE_URL = | |
| import.meta.env.VITE_API_URL || "http://localhost:3000/api"; | |
| const api = axios.create({ | |
| baseURL: API_BASE_URL, | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| timeout: 10000, | |
| withCredentials: true, | |
| }); | |
| let accessToken = typeof window !== "undefined" | |
| ? localStorage.getItem("access_token") | |
| : null; | |
| let refreshTimer = null; | |
| // Parse JWT token to get expiration time | |
| function parseJwt(token) { | |
| try { | |
| const base64Url = token.split('.')[1]; | |
| const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | |
| const jsonPayload = decodeURIComponent( | |
| atob(base64) | |
| .split('') | |
| .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) | |
| .join('') | |
| ); | |
| return JSON.parse(jsonPayload); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| // Schedule token refresh before expiration | |
| function scheduleTokenRefresh(token) { | |
| // Clear existing timer | |
| if (refreshTimer) { | |
| clearTimeout(refreshTimer); | |
| refreshTimer = null; | |
| } | |
| if (!token) return; | |
| const decoded = parseJwt(token); | |
| if (!decoded?.exp) return; | |
| const expiresAt = decoded.exp * 1000; // Convert to milliseconds | |
| const now = Date.now(); | |
| const refreshBefore = 60 * 1000; // Refresh 60 seconds before expiration | |
| const timeUntilRefresh = expiresAt - now - refreshBefore; | |
| if (timeUntilRefresh <= 0) { | |
| // Token already expired or about to expire, refresh immediately | |
| silentRefresh(); | |
| return; | |
| } | |
| refreshTimer = setTimeout(() => { | |
| silentRefresh(); | |
| }, timeUntilRefresh); | |
| } | |
| // Silent refresh token | |
| async function silentRefresh() { | |
| try { | |
| const rawToken = typeof window !== "undefined" | |
| ? localStorage.getItem("refreshToken") | |
| : null; | |
| let refreshToken = rawToken; | |
| if (rawToken && rawToken.startsWith("{")) { | |
| try { | |
| const parsed = JSON.parse(rawToken); | |
| if (parsed && typeof parsed === "object" && parsed.refreshToken) { | |
| refreshToken = parsed.refreshToken; | |
| } | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| if (!refreshToken) { | |
| clearAccessToken(); | |
| return; | |
| } | |
| const { data } = await axios.post( | |
| `${API_BASE_URL}/auth/refresh`, | |
| { refreshToken }, | |
| { withCredentials: true } | |
| ); | |
| setAccessToken(data.accessToken); | |
| if (typeof window !== "undefined") { | |
| localStorage.setItem("refreshToken", data.refreshToken); | |
| } | |
| // Notify WebSocket to reconnect with the new token | |
| import("./socket.service").then(({ default: socketService }) => { | |
| if (socketService.isConnected()) { | |
| console.log("[API Silent Refresh] Token refreshed, reconnecting WebSocket..."); | |
| socketService.reconnect(); | |
| } | |
| }); | |
| } catch { | |
| clearAuth(); | |
| } | |
| } | |
| export const setAccessToken = (token) => { | |
| accessToken = token; | |
| if (token && typeof window !== "undefined") { | |
| localStorage.setItem("access_token", token); | |
| scheduleTokenRefresh(token); | |
| } else if (typeof window !== "undefined") { | |
| localStorage.removeItem("access_token"); | |
| if (refreshTimer) { | |
| clearTimeout(refreshTimer); | |
| refreshTimer = null; | |
| } | |
| } | |
| }; | |
| export const clearAccessToken = () => { | |
| accessToken = null; | |
| if (typeof window !== "undefined") { | |
| localStorage.removeItem("access_token"); | |
| } | |
| }; | |
| export const clearAuth = () => { | |
| clearAccessToken(); | |
| if (refreshTimer) { | |
| clearTimeout(refreshTimer); | |
| refreshTimer = null; | |
| } | |
| if (typeof window !== "undefined") { | |
| localStorage.removeItem("refreshToken"); | |
| localStorage.removeItem("auth_user"); | |
| if (window.location.pathname !== "/") { | |
| window.location.replace("/"); | |
| } | |
| } | |
| }; | |
| api.interceptors.request.use( | |
| (config) => { | |
| const publicPaths = ["/auth/login", "/auth/register", "/auth/refresh", "/auth/forgot-password"]; | |
| const isPublic = publicPaths.some((p) => config.url?.includes(p)); | |
| if (accessToken && !isPublic) { | |
| config.headers.Authorization = `Bearer ${accessToken}`; | |
| } | |
| return config; | |
| }, | |
| (error) => Promise.reject(error), | |
| ); | |
| let isRefreshing = false; | |
| let refreshSubscribers = []; | |
| const onRefreshed = (token) => { | |
| refreshSubscribers.forEach((cb) => cb(token)); | |
| refreshSubscribers = []; | |
| }; | |
| const subscribeTokenRefresh = (cb) => { | |
| refreshSubscribers.push(cb); | |
| }; | |
| api.interceptors.response.use( | |
| (response) => response, | |
| async (error) => { | |
| const original = error.config; | |
| const status = error.response?.status; | |
| const publicPaths = ["/auth/login", "/auth/register", "/auth/refresh", "/auth/forgot-password"]; | |
| const isPublic = publicPaths.some((p) => original.url?.includes(p)); | |
| if (status === 401 && !original._retry && !isPublic) { | |
| if (isRefreshing) { | |
| return new Promise((resolve) => { | |
| subscribeTokenRefresh((token) => { | |
| original.headers.Authorization = `Bearer ${token}`; | |
| resolve(api(original)); | |
| }); | |
| }); | |
| } | |
| original._retry = true; | |
| isRefreshing = true; | |
| try { | |
| const rawToken = | |
| typeof window !== "undefined" | |
| ? localStorage.getItem("refreshToken") | |
| : null; | |
| let refreshToken = rawToken; | |
| if (rawToken && rawToken.startsWith("{")) { | |
| try { | |
| const parsed = JSON.parse(rawToken); | |
| if (parsed && typeof parsed === "object" && parsed.refreshToken) { | |
| refreshToken = parsed.refreshToken; | |
| } | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| if (!refreshToken) { | |
| throw new Error("No refresh token"); | |
| } | |
| const { data } = await axios.post( | |
| `${API_BASE_URL}/auth/refresh`, | |
| { refreshToken }, | |
| { withCredentials: true } | |
| ); | |
| setAccessToken(data.accessToken); | |
| if (typeof window !== "undefined") { | |
| localStorage.setItem("refreshToken", data.refreshToken); | |
| } | |
| onRefreshed(data.accessToken); | |
| original.headers.Authorization = `Bearer ${data.accessToken}`; | |
| // Notify WebSocket to reconnect with the new token | |
| import("./socket.service").then(({ default: socketService }) => { | |
| if (socketService.isConnected()) { | |
| console.log("[API] Token refreshed, reconnecting WebSocket..."); | |
| socketService.reconnect(); | |
| } | |
| }); | |
| return api(original); | |
| } catch (refreshError) { | |
| clearAuth(); | |
| return Promise.reject(refreshError); | |
| } finally { | |
| isRefreshing = false; | |
| } | |
| } | |
| return Promise.reject(error); | |
| }, | |
| ); | |
| export default api; | |