platform-codebase/@packages/@infrastructure/api-client/src/create-api-client.js

205 lines
8 KiB
JavaScript
Raw Normal View History

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