mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 23:11:13 -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 };
|
||||
93
lib/api/errorHandler.ts
Normal file
93
lib/api/errorHandler.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
code?: string;
|
||||
status: number;
|
||||
details?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export function handleApiError(error: unknown): ApiError {
|
||||
if (error instanceof AxiosError) {
|
||||
const status = error.response?.status || 500;
|
||||
const data = error.response?.data;
|
||||
|
||||
// Django validation errors
|
||||
if (status === 400 && data) {
|
||||
return {
|
||||
message: 'Validation failed',
|
||||
code: 'VALIDATION_ERROR',
|
||||
status,
|
||||
details: data,
|
||||
};
|
||||
}
|
||||
|
||||
// Authentication errors
|
||||
if (status === 401) {
|
||||
return {
|
||||
message: 'Authentication required',
|
||||
code: 'UNAUTHORIZED',
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
// Permission errors
|
||||
if (status === 403) {
|
||||
return {
|
||||
message: 'Permission denied',
|
||||
code: 'FORBIDDEN',
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
// Not found
|
||||
if (status === 404) {
|
||||
return {
|
||||
message: 'Resource not found',
|
||||
code: 'NOT_FOUND',
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (status === 429) {
|
||||
return {
|
||||
message: 'Too many requests',
|
||||
code: 'RATE_LIMITED',
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
// Server errors
|
||||
if (status >= 500) {
|
||||
return {
|
||||
message: 'Server error occurred',
|
||||
code: 'SERVER_ERROR',
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: data?.message || error.message || 'An error occurred',
|
||||
code: 'API_ERROR',
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
// Network errors
|
||||
return {
|
||||
message: 'Network error occurred',
|
||||
code: 'NETWORK_ERROR',
|
||||
status: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatErrorForUser(error: ApiError): string {
|
||||
if (error.details) {
|
||||
const messages = Object.entries(error.details)
|
||||
.map(([field, errors]) => `${field}: ${errors.join(', ')}`)
|
||||
.join('\n');
|
||||
return messages;
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
3
lib/api/index.ts
Normal file
3
lib/api/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { apiClient } from './client';
|
||||
export { handleApiError, formatErrorForUser } from './errorHandler';
|
||||
export type { ApiError } from './errorHandler';
|
||||
Reference in New Issue
Block a user