mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 13:11:12 -05:00
164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
|
|
import { env } from '@/lib/env';
|
|
|
|
// Import tokenStorage dynamically to avoid circular dependencies
|
|
const getTokenStorage = () => {
|
|
if (typeof window !== 'undefined') {
|
|
// Dynamic import to avoid SSR issues
|
|
return require('@/lib/services/auth/tokenStorage').tokenStorage;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Track if we're currently refreshing to prevent multiple refresh attempts
|
|
let isRefreshing = false;
|
|
let refreshSubscribers: ((token: string) => void)[] = [];
|
|
|
|
function subscribeTokenRefresh(cb: (token: string) => void) {
|
|
refreshSubscribers.push(cb);
|
|
}
|
|
|
|
function onTokenRefreshed(token: string) {
|
|
refreshSubscribers.forEach((cb) => cb(token));
|
|
refreshSubscribers = [];
|
|
}
|
|
|
|
// Create axios instance with default config
|
|
const apiClient: AxiosInstance = axios.create({
|
|
baseURL: env.NEXT_PUBLIC_DJANGO_API_URL,
|
|
timeout: 30000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Request interceptor - Add auth token
|
|
apiClient.interceptors.request.use(
|
|
(config: InternalAxiosRequestConfig) => {
|
|
// Add auth token if available (client-side only)
|
|
if (typeof window !== 'undefined') {
|
|
const tokenStorage = getTokenStorage();
|
|
const token = tokenStorage?.getAccessToken();
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
}
|
|
|
|
// Log requests in development
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`);
|
|
}
|
|
|
|
return config;
|
|
},
|
|
(error) => {
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Response interceptor - Handle errors and token refresh
|
|
apiClient.interceptors.response.use(
|
|
(response) => {
|
|
// Log responses in development
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log(`[API Response] ${response.config.method?.toUpperCase()} ${response.config.url}`, response.status);
|
|
}
|
|
return response;
|
|
},
|
|
async (error: AxiosError) => {
|
|
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
|
|
|
|
// Log errors in development
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error('[API Error]', error.message, error.response?.status);
|
|
}
|
|
|
|
// Handle 401 Unauthorized - attempt token refresh
|
|
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
|
|
if (typeof window !== 'undefined') {
|
|
const tokenStorage = getTokenStorage();
|
|
const refreshToken = tokenStorage?.getRefreshToken();
|
|
|
|
// Only attempt refresh if we have a refresh token
|
|
if (refreshToken && !isRefreshing) {
|
|
originalRequest._retry = true;
|
|
|
|
if (!isRefreshing) {
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
// Attempt to refresh the token
|
|
const response = await axios.post(
|
|
`${env.NEXT_PUBLIC_DJANGO_API_URL}/api/v1/auth/token/refresh`,
|
|
{ refresh: refreshToken }
|
|
);
|
|
|
|
const { access, refresh: newRefreshToken } = response.data;
|
|
|
|
// Store new tokens
|
|
tokenStorage.setAccessToken(access);
|
|
if (newRefreshToken) {
|
|
tokenStorage.setRefreshToken(newRefreshToken);
|
|
}
|
|
|
|
// Notify all subscribers
|
|
onTokenRefreshed(access);
|
|
isRefreshing = false;
|
|
|
|
// Retry the original request with new token
|
|
if (originalRequest.headers) {
|
|
originalRequest.headers.Authorization = `Bearer ${access}`;
|
|
}
|
|
return apiClient(originalRequest);
|
|
} catch (refreshError) {
|
|
// Refresh failed, clear tokens and redirect to login
|
|
isRefreshing = false;
|
|
tokenStorage?.clearTokens();
|
|
|
|
// Redirect to login page
|
|
if (window.location.pathname !== '/login') {
|
|
window.location.href = '/login?session_expired=true';
|
|
}
|
|
|
|
return Promise.reject(refreshError);
|
|
}
|
|
} else {
|
|
// If already refreshing, wait for the refresh to complete
|
|
return new Promise((resolve) => {
|
|
subscribeTokenRefresh((token: string) => {
|
|
if (originalRequest.headers) {
|
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
resolve(apiClient(originalRequest));
|
|
});
|
|
});
|
|
}
|
|
} else {
|
|
// No refresh token available, clear storage and redirect
|
|
tokenStorage?.clearTokens();
|
|
if (window.location.pathname !== '/login') {
|
|
window.location.href = '/login?session_expired=true';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Retry logic for network errors
|
|
const config = error.config as AxiosRequestConfig & { _retryCount?: number };
|
|
if (!error.response && config && !config._retryCount) {
|
|
config._retryCount = (config._retryCount || 0) + 1;
|
|
if (config._retryCount <= 3) {
|
|
// Exponential backoff
|
|
const delay = Math.min(1000 * Math.pow(2, config._retryCount - 1), 10000);
|
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
return apiClient(config);
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
export { apiClient };
|
|
export type { AxiosError };
|