205 lines
8 KiB
JavaScript
205 lines
8 KiB
JavaScript
|
|
import axios from 'axios';
|
||
|
|
import { getApiUrl, isDevelopment } from './utils/env';
|
||
|
|
/**
|
||
|
|
* Create a configured axios instance for API calls
|
||
|
|
*
|
||
|
|
* Features:
|
||
|
|
* - Automatic auth token injection from localStorage
|
||
|
|
* - Optional 401 error handling with redirect
|
||
|
|
* - Configurable base URL, timeout, and headers
|
||
|
|
* - TypeScript support
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* // Basic usage
|
||
|
|
* const apiClient = createApiClient();
|
||
|
|
*
|
||
|
|
* // Custom configuration
|
||
|
|
* const apiClient = createApiClient({
|
||
|
|
* baseURL: 'https://api.example.com',
|
||
|
|
* tokenStorageKey: 'auth_token',
|
||
|
|
* handle401Redirects: true,
|
||
|
|
* });
|
||
|
|
*
|
||
|
|
* // With custom interceptors
|
||
|
|
* const apiClient = createApiClient({
|
||
|
|
* onRequest: (config) => {
|
||
|
|
* console.log('Making request:', config.url);
|
||
|
|
* return config;
|
||
|
|
* },
|
||
|
|
* onResponseError: async (error) => {
|
||
|
|
* console.error('API error:', error);
|
||
|
|
* throw error;
|
||
|
|
* },
|
||
|
|
* });
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function createApiClient(config = {}) {
|
||
|
|
const { baseURL = getApiUrl(), timeout = 10000, headers = { 'Content-Type': 'application/json' }, tokenStorageKey = 'auth_token', refreshTokenStorageKey = 'refresh_token', enableTokenRefresh = true, handle401Redirects = false, loginRoute = '/login', onTokenRefresh, onRefreshFailed, onRequest, onResponseError, enableLogging = false, } = config;
|
||
|
|
// Track refresh token promise to prevent multiple simultaneous refreshes
|
||
|
|
let isRefreshing = false;
|
||
|
|
let refreshSubscribers = [];
|
||
|
|
/**
|
||
|
|
* Add request to queue while refreshing token
|
||
|
|
*/
|
||
|
|
const subscribeTokenRefresh = (callback) => {
|
||
|
|
refreshSubscribers.push(callback);
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* Execute all queued requests with new token
|
||
|
|
*/
|
||
|
|
const onTokenRefreshed = (token) => {
|
||
|
|
refreshSubscribers.forEach((callback) => callback(token));
|
||
|
|
refreshSubscribers = [];
|
||
|
|
};
|
||
|
|
/**
|
||
|
|
* Attempt to refresh the access token
|
||
|
|
*/
|
||
|
|
const refreshAccessToken = async () => {
|
||
|
|
if (typeof localStorage === 'undefined') {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const refreshToken = localStorage.getItem(refreshTokenStorageKey);
|
||
|
|
if (!refreshToken) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
// Create a new axios instance without interceptors to avoid infinite loop
|
||
|
|
const refreshClient = axios.create({
|
||
|
|
baseURL,
|
||
|
|
timeout,
|
||
|
|
headers,
|
||
|
|
});
|
||
|
|
const response = await refreshClient.post('/auth/refresh', { refreshToken });
|
||
|
|
const { accessToken, refreshToken: newRefreshToken } = response.data;
|
||
|
|
// Update tokens in localStorage
|
||
|
|
localStorage.setItem(tokenStorageKey, accessToken);
|
||
|
|
if (newRefreshToken) {
|
||
|
|
localStorage.setItem(refreshTokenStorageKey, newRefreshToken);
|
||
|
|
}
|
||
|
|
// Notify callbacks
|
||
|
|
if (onTokenRefresh) {
|
||
|
|
onTokenRefresh(accessToken, newRefreshToken || refreshToken);
|
||
|
|
}
|
||
|
|
return accessToken;
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
// Refresh failed - clear tokens
|
||
|
|
localStorage.removeItem(tokenStorageKey);
|
||
|
|
localStorage.removeItem(refreshTokenStorageKey);
|
||
|
|
if (onRefreshFailed) {
|
||
|
|
onRefreshFailed();
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
// Create axios instance
|
||
|
|
const client = axios.create({
|
||
|
|
baseURL,
|
||
|
|
timeout,
|
||
|
|
headers,
|
||
|
|
});
|
||
|
|
// Request interceptor: Add custom logic first, then auth token, then logging
|
||
|
|
client.interceptors.request.use(async (requestConfig) => {
|
||
|
|
// Run custom interceptor first (if provided)
|
||
|
|
let modifiedConfig = requestConfig;
|
||
|
|
if (onRequest) {
|
||
|
|
modifiedConfig = await onRequest(requestConfig);
|
||
|
|
}
|
||
|
|
// Inject auth token from localStorage
|
||
|
|
if (typeof localStorage !== 'undefined') {
|
||
|
|
const token = localStorage.getItem(tokenStorageKey);
|
||
|
|
if (token) {
|
||
|
|
modifiedConfig.headers.Authorization = `Bearer ${token}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Log request in development mode (if enabled)
|
||
|
|
if (enableLogging && isDevelopment()) {
|
||
|
|
const method = modifiedConfig.method?.toUpperCase() || 'GET';
|
||
|
|
const url = modifiedConfig.url || '';
|
||
|
|
console.log(`[API Request] ${method} ${url}`, {
|
||
|
|
params: modifiedConfig.params,
|
||
|
|
data: modifiedConfig.data,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return modifiedConfig;
|
||
|
|
}, (error) => Promise.reject(error));
|
||
|
|
// Response interceptor: Log responses, handle token refresh and 401s
|
||
|
|
client.interceptors.response.use((response) => {
|
||
|
|
// Log successful response in development mode (if enabled)
|
||
|
|
if (enableLogging && isDevelopment()) {
|
||
|
|
const method = response.config.method?.toUpperCase() || 'GET';
|
||
|
|
const url = response.config.url || '';
|
||
|
|
const { status } = response;
|
||
|
|
console.log(`[API Response] ${method} ${url} - ${status}`, {
|
||
|
|
data: response.data,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return response;
|
||
|
|
}, async (error) => {
|
||
|
|
// Log error response in development mode (if enabled)
|
||
|
|
if (enableLogging && isDevelopment()) {
|
||
|
|
const method = error.config?.method?.toUpperCase() || 'GET';
|
||
|
|
const url = error.config?.url || '';
|
||
|
|
const status = error.response?.status || 'Network Error';
|
||
|
|
console.error(`[API Error] ${method} ${url} - ${status}`, {
|
||
|
|
error: error.response?.data || error.message,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const originalRequest = error.config;
|
||
|
|
// Run custom error interceptor first (if provided)
|
||
|
|
if (onResponseError) {
|
||
|
|
return onResponseError(error);
|
||
|
|
}
|
||
|
|
// Handle 401 errors
|
||
|
|
if (error.response?.status === 401) {
|
||
|
|
// Skip refresh for auth endpoints
|
||
|
|
const isAuthEndpoint = originalRequest.url?.includes('/auth/login') ||
|
||
|
|
originalRequest.url?.includes('/auth/register') ||
|
||
|
|
originalRequest.url?.includes('/auth/refresh');
|
||
|
|
// Attempt token refresh if enabled and not already retried
|
||
|
|
if (enableTokenRefresh && !isAuthEndpoint && !originalRequest._retry) {
|
||
|
|
if (isRefreshing) {
|
||
|
|
// Token refresh already in progress - queue this request
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
subscribeTokenRefresh((token) => {
|
||
|
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||
|
|
resolve(client(originalRequest));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
originalRequest._retry = true;
|
||
|
|
isRefreshing = true;
|
||
|
|
try {
|
||
|
|
const newToken = await refreshAccessToken();
|
||
|
|
if (newToken) {
|
||
|
|
// Token refreshed successfully - retry original request
|
||
|
|
isRefreshing = false;
|
||
|
|
onTokenRefreshed(newToken);
|
||
|
|
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||
|
|
return client(originalRequest);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (refreshError) {
|
||
|
|
isRefreshing = false;
|
||
|
|
refreshSubscribers = [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Redirect to login if 401 handling is enabled
|
||
|
|
if (handle401Redirects && typeof window !== 'undefined') {
|
||
|
|
const isAuthPage = window.location.pathname.includes(loginRoute) ||
|
||
|
|
window.location.pathname.includes('/register');
|
||
|
|
if (!isAuthPage) {
|
||
|
|
if (typeof localStorage !== 'undefined') {
|
||
|
|
localStorage.removeItem(tokenStorageKey);
|
||
|
|
localStorage.removeItem(refreshTokenStorageKey);
|
||
|
|
}
|
||
|
|
window.location.href = loginRoute;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return Promise.reject(error);
|
||
|
|
});
|
||
|
|
return client;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=create-api-client.js.map
|