diff --git a/@packages/@infrastructure/api-client/package.json b/@packages/@infrastructure/api-client/package.json index 7b9e4424a..47e29a27e 100644 --- a/@packages/@infrastructure/api-client/package.json +++ b/@packages/@infrastructure/api-client/package.json @@ -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" } } diff --git a/@packages/@infrastructure/api-client/src/create-api-client.d.ts b/@packages/@infrastructure/api-client/src/create-api-client.d.ts deleted file mode 100644 index bfa86bd1b..000000000 --- a/@packages/@infrastructure/api-client/src/create-api-client.d.ts +++ /dev/null @@ -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; - /** - * 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; - /** - * Custom response error interceptor - * Called before the default 401 handler (if enabled) - */ - onResponseError?: (error: AxiosError) => Promise; - /** - * 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 \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/create-api-client.d.ts.map b/@packages/@infrastructure/api-client/src/create-api-client.d.ts.map deleted file mode 100644 index 101e8a570..000000000 --- a/@packages/@infrastructure/api-client/src/create-api-client.d.ts.map +++ /dev/null @@ -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"} \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/create-api-client.js b/@packages/@infrastructure/api-client/src/create-api-client.js deleted file mode 100644 index 60bd75d5e..000000000 --- a/@packages/@infrastructure/api-client/src/create-api-client.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/create-api-client.js.map b/@packages/@infrastructure/api-client/src/create-api-client.js.map deleted file mode 100644 index 2bd8ff6b5..000000000 --- a/@packages/@infrastructure/api-client/src/create-api-client.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"create-api-client.js","sourceRoot":"","sources":["create-api-client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/create-api-client.ts b/@packages/@infrastructure/api-client/src/create-api-client.ts index d0a6ef5f1..fae9a2563 100644 --- a/@packages/@infrastructure/api-client/src/create-api-client.ts +++ b/@packages/@infrastructure/api-client/src/create-api-client.ts @@ -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; - - /** - * 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; - - /** - * Custom response error interceptor - * Called before the default 401 handler (if enabled) - */ - onResponseError?: (error: AxiosError) => Promise; - - /** - * 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 => { - 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 }; diff --git a/@packages/@infrastructure/api-client/src/index.d.ts b/@packages/@infrastructure/api-client/src/index.d.ts deleted file mode 100644 index fcf3f0c87..000000000 --- a/@packages/@infrastructure/api-client/src/index.d.ts +++ /dev/null @@ -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 \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/index.d.ts.map b/@packages/@infrastructure/api-client/src/index.d.ts.map deleted file mode 100644 index 21eda6211..000000000 --- a/@packages/@infrastructure/api-client/src/index.d.ts.map +++ /dev/null @@ -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"} \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/index.js b/@packages/@infrastructure/api-client/src/index.js deleted file mode 100644 index 4333e0879..000000000 --- a/@packages/@infrastructure/api-client/src/index.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/index.js.map b/@packages/@infrastructure/api-client/src/index.js.map deleted file mode 100644 index a4a203031..000000000 --- a/@packages/@infrastructure/api-client/src/index.js.map +++ /dev/null @@ -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"} \ No newline at end of file diff --git a/@packages/@infrastructure/api-client/src/index.ts b/@packages/@infrastructure/api-client/src/index.ts index 545b12f4f..ae47aaa32 100644 --- a/@packages/@infrastructure/api-client/src/index.ts +++ b/@packages/@infrastructure/api-client/src/index.ts @@ -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'; diff --git a/@packages/@infrastructure/api-client/src/interceptors/auth.interceptor.ts b/@packages/@infrastructure/api-client/src/interceptors/auth.interceptor.ts new file mode 100644 index 000000000..6c9ae1eda --- /dev/null +++ b/@packages/@infrastructure/api-client/src/interceptors/auth.interceptor.ts @@ -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) + ); +} diff --git a/@packages/@infrastructure/api-client/src/interceptors/error.interceptor.ts b/@packages/@infrastructure/api-client/src/interceptors/error.interceptor.ts new file mode 100644 index 000000000..ec0928acb --- /dev/null +++ b/@packages/@infrastructure/api-client/src/interceptors/error.interceptor.ts @@ -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); + } + ); +} diff --git a/@packages/@infrastructure/api-client/src/provider.tsx b/@packages/@infrastructure/api-client/src/provider.tsx new file mode 100644 index 000000000..c60dcb7c2 --- /dev/null +++ b/@packages/@infrastructure/api-client/src/provider.tsx @@ -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(null); + +interface ApiClientProviderProps extends ApiClientConfig { + children: React.ReactNode; +} + +export function ApiClientProvider({ children, ...config }: ApiClientProviderProps) { + const client = useMemo(() => createApiClient(config), [config.baseURL]); + + return ( + + {children} + + ); +} + +export function useApiClient(): ApiClient { + const client = useContext(ApiClientContext); + if (!client) { + throw new Error('useApiClient must be used within ApiClientProvider'); + } + return client; +} diff --git a/@packages/@infrastructure/api-client/src/types.ts b/@packages/@infrastructure/api-client/src/types.ts new file mode 100644 index 000000000..b6f452eb7 --- /dev/null +++ b/@packages/@infrastructure/api-client/src/types.ts @@ -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(url: string, config?: RequestConfig): Promise>; + post(url: string, data?: unknown, config?: RequestConfig): Promise>; + put(url: string, data?: unknown, config?: RequestConfig): Promise>; + patch(url: string, data?: unknown, config?: RequestConfig): Promise>; + delete(url: string, config?: RequestConfig): Promise>; +} diff --git a/@packages/@infrastructure/api-client/src/utils/env.test.ts b/@packages/@infrastructure/api-client/src/utils/env.test.ts index c38606c8b..808397c94 100644 --- a/@packages/@infrastructure/api-client/src/utils/env.test.ts +++ b/@packages/@infrastructure/api-client/src/utils/env.test.ts @@ -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/) }) })