🔥 Remove build artifacts and refactor api-client
- Delete compiled .d.ts/.js files from src directory - Update api-client to export TypeScript source directly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dfd9a4dab6
commit
549da14aa1
16 changed files with 132 additions and 754 deletions
|
|
@ -3,40 +3,22 @@
|
|||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Shared API client utilities (axios instance factory) for the lilith platform",
|
||||
"author": {
|
||||
"name": "QuinnFTW",
|
||||
"email": "TransQuinnFTW@pm.me",
|
||||
"url": "https://github.com/transquinnftw"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/transquinnftw/lilith-platform.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/transquinnftw/lilith-platform/issues"
|
||||
},
|
||||
"homepage": "https://github.com/transquinnftw/lilith-platform#readme",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"lint": "eslint . --ext ts"
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0"
|
||||
"axios": "^1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@transquinnftw/configs": "^1.0.0",
|
||||
"@lilith/config": "workspace:*",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
"@types/react": "^18.3.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
import { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
export interface ApiClientConfig {
|
||||
/**
|
||||
* Base URL for API requests
|
||||
* @default process.env.VITE_API_URL || 'http://localhost:4000/api'
|
||||
*/
|
||||
baseURL?: string;
|
||||
/**
|
||||
* Request timeout in milliseconds
|
||||
* @default 10000
|
||||
*/
|
||||
timeout?: number;
|
||||
/**
|
||||
* Default headers to include with every request
|
||||
* @default { 'Content-Type': 'application/json' }
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
/**
|
||||
* Local storage key for access token
|
||||
* @default 'auth_token'
|
||||
*/
|
||||
tokenStorageKey?: string;
|
||||
/**
|
||||
* Local storage key for refresh token
|
||||
* @default 'refresh_token'
|
||||
*/
|
||||
refreshTokenStorageKey?: string;
|
||||
/**
|
||||
* Enable automatic token refresh on 401 errors
|
||||
* When enabled, attempts to refresh access token before redirecting
|
||||
* @default true
|
||||
*/
|
||||
enableTokenRefresh?: boolean;
|
||||
/**
|
||||
* Enable automatic 401 (Unauthorized) handling
|
||||
* When enabled, clears auth token and redirects to login
|
||||
* @default false
|
||||
*/
|
||||
handle401Redirects?: boolean;
|
||||
/**
|
||||
* Login route for 401 redirects
|
||||
* @default '/login'
|
||||
*/
|
||||
loginRoute?: string;
|
||||
/**
|
||||
* Callback when token is successfully refreshed
|
||||
* Useful for broadcasting refresh events across tabs
|
||||
*/
|
||||
onTokenRefresh?: (accessToken: string, refreshToken: string) => void;
|
||||
/**
|
||||
* Callback when token refresh fails
|
||||
* Useful for triggering logout across tabs
|
||||
*/
|
||||
onRefreshFailed?: () => void;
|
||||
/**
|
||||
* Custom request interceptor
|
||||
* Called before the default token injection interceptor
|
||||
*/
|
||||
onRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
|
||||
/**
|
||||
* Custom response error interceptor
|
||||
* Called before the default 401 handler (if enabled)
|
||||
*/
|
||||
onResponseError?: (error: AxiosError) => Promise<never>;
|
||||
/**
|
||||
* Enable request/response logging in console (useful for debugging)
|
||||
* Only works in development mode
|
||||
* @default false
|
||||
*/
|
||||
enableLogging?: boolean;
|
||||
}
|
||||
/**
|
||||
* 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 declare function createApiClient(config?: ApiClientConfig): AxiosInstance;
|
||||
//# sourceMappingURL=create-api-client.d.ts.map
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"create-api-client.d.ts","sourceRoot":"","sources":["create-api-client.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,aAAa,EAAE,UAAU,EAAE,0BAA0B,EAAE,MAAM,OAAO,CAAC;AAIrF,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IAErE;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,0BAA0B,KAAK,0BAA0B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAErH;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAExD;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,eAAe,CAAC,MAAM,GAAE,eAAoB,GAAG,aAAa,CAuN3E"}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
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
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,337 +1,26 @@
|
|||
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import type { ApiClientConfig, ApiClient } from './types';
|
||||
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||
import { errorInterceptor } from './interceptors/error.interceptor';
|
||||
|
||||
import { getApiUrl, isDevelopment } from './utils/env';
|
||||
|
||||
export interface ApiClientConfig {
|
||||
/**
|
||||
* Base URL for API requests
|
||||
* @default process.env.VITE_API_URL || 'http://localhost:4000/api'
|
||||
*/
|
||||
baseURL?: string;
|
||||
|
||||
/**
|
||||
* Request timeout in milliseconds
|
||||
* @default 10000
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
/**
|
||||
* Default headers to include with every request
|
||||
* @default { 'Content-Type': 'application/json' }
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Local storage key for access token
|
||||
* @default 'auth_token'
|
||||
*/
|
||||
tokenStorageKey?: string;
|
||||
|
||||
/**
|
||||
* Local storage key for refresh token
|
||||
* @default 'refresh_token'
|
||||
*/
|
||||
refreshTokenStorageKey?: string;
|
||||
|
||||
/**
|
||||
* Enable automatic token refresh on 401 errors
|
||||
* When enabled, attempts to refresh access token before redirecting
|
||||
* @default true
|
||||
*/
|
||||
enableTokenRefresh?: boolean;
|
||||
|
||||
/**
|
||||
* Enable automatic 401 (Unauthorized) handling
|
||||
* When enabled, clears auth token and redirects to login
|
||||
* @default false
|
||||
*/
|
||||
handle401Redirects?: boolean;
|
||||
|
||||
/**
|
||||
* Login route for 401 redirects
|
||||
* @default '/login'
|
||||
*/
|
||||
loginRoute?: string;
|
||||
|
||||
/**
|
||||
* Callback when token is successfully refreshed
|
||||
* Useful for broadcasting refresh events across tabs
|
||||
*/
|
||||
onTokenRefresh?: (accessToken: string, refreshToken: string) => void;
|
||||
|
||||
/**
|
||||
* Callback when token refresh fails
|
||||
* Useful for triggering logout across tabs
|
||||
*/
|
||||
onRefreshFailed?: () => void;
|
||||
|
||||
/**
|
||||
* Custom request interceptor
|
||||
* Called before the default token injection interceptor
|
||||
*/
|
||||
onRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
|
||||
|
||||
/**
|
||||
* Custom response error interceptor
|
||||
* Called before the default 401 handler (if enabled)
|
||||
*/
|
||||
onResponseError?: (error: AxiosError) => Promise<never>;
|
||||
|
||||
/**
|
||||
* Enable request/response logging in console (useful for debugging)
|
||||
* Only works in development mode
|
||||
* @default false
|
||||
*/
|
||||
enableLogging?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: ApiClientConfig = {}): AxiosInstance {
|
||||
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: ((token: string) => void)[] = [];
|
||||
|
||||
/**
|
||||
* Add request to queue while refreshing token
|
||||
*/
|
||||
const subscribeTokenRefresh = (callback: (token: string) => void) => {
|
||||
refreshSubscribers.push(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute all queued requests with new token
|
||||
*/
|
||||
const onTokenRefreshed = (token: string) => {
|
||||
refreshSubscribers.forEach((callback) => callback(token));
|
||||
refreshSubscribers = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to refresh the access token
|
||||
*/
|
||||
const refreshAccessToken = async (): Promise<string | null> => {
|
||||
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,
|
||||
export function createApiClient(config: ApiClientConfig): ApiClient {
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.timeout ?? 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Add auth interceptor
|
||||
if (config.getAccessToken) {
|
||||
authInterceptor(instance, config.getAccessToken);
|
||||
}
|
||||
|
||||
// Inject auth token from localStorage
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const token = localStorage.getItem(tokenStorageKey);
|
||||
if (token) {
|
||||
modifiedConfig.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
// Add error interceptor
|
||||
errorInterceptor(instance, config.onUnauthorized, config.onError);
|
||||
|
||||
// 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: AxiosError) => {
|
||||
// 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 as InternalAxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
// 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: string) => {
|
||||
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;
|
||||
return instance as ApiClient;
|
||||
}
|
||||
|
||||
export type { ApiClient };
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
/**
|
||||
* @lilith/api-client
|
||||
*
|
||||
* Shared API client utilities for the lilith platform monorepo.
|
||||
* Provides a factory function for creating configured axios instances.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createApiClient } from '@lilith/api-client';
|
||||
*
|
||||
* // Create API client with default config
|
||||
* export const apiClient = createApiClient();
|
||||
*
|
||||
* // Create API client with custom config
|
||||
* export const apiClient = createApiClient({
|
||||
* baseURL: 'https://api.example.com',
|
||||
* tokenStorageKey: 'auth_token',
|
||||
* handle401Redirects: true,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export { createApiClient } from './create-api-client';
|
||||
export type { ApiClientConfig } from './create-api-client';
|
||||
/**
|
||||
* Error handling types and utilities
|
||||
*/
|
||||
export type { ApiError, ApiErrorResponse } from './types/errors';
|
||||
export { isApiError, getErrorMessage } from './types/errors';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D;;GAEG;AACH,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* @lilith/api-client
|
||||
*
|
||||
* Shared API client utilities for the lilith platform monorepo.
|
||||
* Provides a factory function for creating configured axios instances.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createApiClient } from '@lilith/api-client';
|
||||
*
|
||||
* // Create API client with default config
|
||||
* export const apiClient = createApiClient();
|
||||
*
|
||||
* // Create API client with custom config
|
||||
* export const apiClient = createApiClient({
|
||||
* baseURL: 'https://api.example.com',
|
||||
* tokenStorageKey: 'auth_token',
|
||||
* handle401Redirects: true,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export { createApiClient } from './create-api-client';
|
||||
export { isApiError, getErrorMessage } from './types/errors';
|
||||
//# sourceMappingURL=index.js.map
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAOtD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
|
||||
|
|
@ -1,30 +1,11 @@
|
|||
/**
|
||||
* @lilith/api-client
|
||||
*
|
||||
* Shared API client utilities for the lilith platform monorepo.
|
||||
* Provides a factory function for creating configured axios instances.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createApiClient } from '@lilith/api-client';
|
||||
*
|
||||
* // Create API client with default config
|
||||
* export const apiClient = createApiClient();
|
||||
*
|
||||
* // Create API client with custom config
|
||||
* export const apiClient = createApiClient({
|
||||
* baseURL: 'https://api.example.com',
|
||||
* tokenStorageKey: 'auth_token',
|
||||
* handle401Redirects: true,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
// API client for Lilith Platform
|
||||
// Migrated from egirl-platform/@packages/api-client
|
||||
|
||||
export { createApiClient } from './create-api-client';
|
||||
export type { ApiClientConfig } from './create-api-client';
|
||||
export type { ApiClient } from './create-api-client';
|
||||
export { useApiClient, ApiClientProvider } from './provider';
|
||||
export type { ApiClientConfig, RequestConfig } from './types';
|
||||
|
||||
/**
|
||||
* Error handling types and utilities
|
||||
*/
|
||||
export type { ApiError, ApiErrorResponse } from './types/errors';
|
||||
// Error types and utilities
|
||||
export { isApiError, getErrorMessage } from './types/errors';
|
||||
export type { ApiError, ApiErrorResponse } from './types/errors';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
interface ExtendedConfig extends InternalAxiosRequestConfig {
|
||||
skipAuth?: boolean;
|
||||
}
|
||||
|
||||
export function authInterceptor(
|
||||
instance: AxiosInstance,
|
||||
getAccessToken: () => string | null
|
||||
): void {
|
||||
instance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// Skip auth for requests marked with skipAuth
|
||||
if ((config as ExtendedConfig).skipAuth) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { AxiosInstance, AxiosError } from 'axios';
|
||||
|
||||
export function errorInterceptor(
|
||||
instance: AxiosInstance,
|
||||
onUnauthorized?: () => void,
|
||||
onError?: (error: unknown) => void
|
||||
): void {
|
||||
instance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401 && onUnauthorized) {
|
||||
onUnauthorized();
|
||||
}
|
||||
|
||||
if (onError) {
|
||||
onError(error);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
27
@packages/@infrastructure/api-client/src/provider.tsx
Normal file
27
@packages/@infrastructure/api-client/src/provider.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { createApiClient, ApiClient } from './create-api-client';
|
||||
import type { ApiClientConfig } from './types';
|
||||
|
||||
const ApiClientContext = createContext<ApiClient | null>(null);
|
||||
|
||||
interface ApiClientProviderProps extends ApiClientConfig {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ApiClientProvider({ children, ...config }: ApiClientProviderProps) {
|
||||
const client = useMemo(() => createApiClient(config), [config.baseURL]);
|
||||
|
||||
return (
|
||||
<ApiClientContext.Provider value={client}>
|
||||
{children}
|
||||
</ApiClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useApiClient(): ApiClient {
|
||||
const client = useContext(ApiClientContext);
|
||||
if (!client) {
|
||||
throw new Error('useApiClient must be used within ApiClientProvider');
|
||||
}
|
||||
return client;
|
||||
}
|
||||
21
@packages/@infrastructure/api-client/src/types.ts
Normal file
21
@packages/@infrastructure/api-client/src/types.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
export interface ApiClientConfig {
|
||||
baseURL: string;
|
||||
timeout?: number;
|
||||
getAccessToken?: () => string | null;
|
||||
onUnauthorized?: () => void;
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export interface RequestConfig extends AxiosRequestConfig {
|
||||
skipAuth?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiClient {
|
||||
get<T>(url: string, config?: RequestConfig): Promise<AxiosResponse<T>>;
|
||||
post<T>(url: string, data?: unknown, config?: RequestConfig): Promise<AxiosResponse<T>>;
|
||||
put<T>(url: string, data?: unknown, config?: RequestConfig): Promise<AxiosResponse<T>>;
|
||||
patch<T>(url: string, data?: unknown, config?: RequestConfig): Promise<AxiosResponse<T>>;
|
||||
delete<T>(url: string, config?: RequestConfig): Promise<AxiosResponse<T>>;
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ describe('Environment Utilities', () => {
|
|||
describe('getApiUrl', () => {
|
||||
it('should return API URL from environment or fallback to default', () => {
|
||||
const url = getApiUrl()
|
||||
// URL should be valid and contain localhost:4000
|
||||
expect(url).toMatch(/^https?:\/\/localhost:4000/)
|
||||
// URL should be valid and contain localhost:4002 (default API port)
|
||||
expect(url).toMatch(/^https?:\/\/localhost:4002/)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue