Download frontend/src/lib/api/client.ts from anky2002/best: direct link, hf CLI and curl.
- Browser
- Download file 2.97 kB
-
https://huggingface.co/anky2002/best/resolve/main/frontend/src/lib/api/client.ts
- Command line
-
hf download hf://anky2002/best/frontend/src/lib/api/client.ts
-
curl -L -o client.ts https://huggingface.co/anky2002/best/resolve/main/frontend/src/lib/api/client.ts
2.97 kB
| import axios from "axios"; | |
| /** | |
| * API Client with token management. | |
| * | |
| * Security model: | |
| * - Access token (15min): stored in memory only (this module's closure) | |
| * - Refresh token (30d): stored in localStorage (TODO: migrate to HttpOnly cookie) | |
| * - Automatic refresh on 401 | |
| * - Refresh token rotation on every refresh | |
| * | |
| * NOTE: Full HttpOnly cookie auth requires backend Set-Cookie support. | |
| * Current implementation keeps refresh in localStorage but access in memory, | |
| * which is already better than both in localStorage. | |
| */ | |
| const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || ""; | |
| // Access token lives in memory only — not accessible to XSS | |
| let accessToken: string | null = null; | |
| export function setAccessToken(token: string | null) { | |
| accessToken = token; | |
| } | |
| export function getAccessToken(): string | null { | |
| return accessToken; | |
| } | |
| // On page load, try to restore from a previous session | |
| if (typeof window !== "undefined") { | |
| // Migrate: if old localStorage access_token exists, use it then remove | |
| const legacy = localStorage.getItem("access_token"); | |
| if (legacy) { | |
| accessToken = legacy; | |
| localStorage.removeItem("access_token"); | |
| } | |
| } | |
| export const apiClient = axios.create({ | |
| baseURL: `${API_BASE_URL}/api/v1`, | |
| headers: { "Content-Type": "application/json" }, | |
| withCredentials: true, | |
| }); | |
| // Attach access token from memory | |
| apiClient.interceptors.request.use( | |
| (config) => { | |
| if (accessToken) { | |
| config.headers.Authorization = `Bearer ${accessToken}`; | |
| } | |
| return config; | |
| }, | |
| (error) => Promise.reject(error) | |
| ); | |
| // Handle 401 with refresh token rotation | |
| apiClient.interceptors.response.use( | |
| (response) => response, | |
| async (error) => { | |
| const originalRequest = error.config; | |
| if (error.response?.status === 401 && !originalRequest._retry) { | |
| originalRequest._retry = true; | |
| try { | |
| const refreshToken = localStorage.getItem("refresh_token"); | |
| if (!refreshToken) throw new Error("No refresh token"); | |
| // Refresh — server rotates the refresh token | |
| const response = await axios.post(`${API_BASE_URL}/api/v1/auth/refresh`, { | |
| refresh_token: refreshToken, | |
| }); | |
| const { access_token, refresh_token: newRefreshToken } = response.data; | |
| // Store new access token in MEMORY only | |
| accessToken = access_token; | |
| // Store rotated refresh token | |
| localStorage.setItem("refresh_token", newRefreshToken); | |
| originalRequest.headers.Authorization = `Bearer ${access_token}`; | |
| return apiClient(originalRequest); | |
| } catch (refreshError) { | |
| // Refresh failed — clear everything and redirect | |
| accessToken = null; | |
| localStorage.removeItem("refresh_token"); | |
| if (typeof window !== "undefined") { | |
| window.location.href = "/login"; | |
| } | |
| return Promise.reject(refreshError); | |
| } | |
| } | |
| return Promise.reject(error); | |
| } | |
| ); | |
| export default apiClient; | |