mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 13:51:12 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
163
lib/api/client.ts
Normal file
163
lib/api/client.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user