Files
thrilltrack-explorer/lib/api/client.ts

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 };