fix: Centralize API clients and add global ErrorBoundary (Phase 3)
Migrate 6 services from duplicate Axios instances to centralized apiClient (auto-refresh, multi-tab sync, proper 401 handling): - trading.service.ts, notification.service.ts, payment.service.ts - education.service.ts, chat.service.ts, investment.service.ts Add global ErrorBoundary wrapping all App routes. Move assistant module ErrorBoundary to shared components/ErrorBoundary.tsx. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
42d18759b5
commit
1d76747e9b
@ -4,6 +4,7 @@ import { Suspense, lazy } from 'react';
|
||||
// Layout
|
||||
import MainLayout from './components/layout/MainLayout';
|
||||
import AuthLayout from './components/layout/AuthLayout';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
|
||||
// Loading component
|
||||
const LoadingSpinner = () => (
|
||||
@ -68,6 +69,7 @@ const PredictionsPage = lazy(() => import('./modules/admin/pages/PredictionsPage
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<Routes>
|
||||
{/* Auth routes */}
|
||||
@ -149,6 +151,7 @@ function App() {
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
206
src/components/ErrorBoundary.tsx
Normal file
206
src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Global ErrorBoundary Component
|
||||
* Catches JavaScript errors in child components and displays fallback UI
|
||||
*/
|
||||
|
||||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
RefreshCw,
|
||||
Home,
|
||||
Bug,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Copy,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
|
||||
export interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
||||
onReset?: () => void;
|
||||
showDetails?: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
showStack: boolean;
|
||||
copied: boolean;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
showStack: false,
|
||||
copied: false,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
this.setState({ errorInfo });
|
||||
|
||||
if (this.props.onError) {
|
||||
this.props.onError(error, errorInfo);
|
||||
}
|
||||
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
handleReset = (): void => {
|
||||
this.setState({
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
showStack: false,
|
||||
});
|
||||
|
||||
if (this.props.onReset) {
|
||||
this.props.onReset();
|
||||
}
|
||||
};
|
||||
|
||||
handleRefresh = (): void => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
handleGoHome = (): void => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
toggleStack = (): void => {
|
||||
this.setState((prev) => ({ showStack: !prev.showStack }));
|
||||
};
|
||||
|
||||
copyError = async (): Promise<void> => {
|
||||
const { error, errorInfo } = this.state;
|
||||
const errorText = `Error: ${error?.message}\n\nStack: ${error?.stack}\n\nComponent Stack: ${errorInfo?.componentStack}`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(errorText);
|
||||
this.setState({ copied: true });
|
||||
setTimeout(() => this.setState({ copied: false }), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const { hasError, error, errorInfo, showStack, copied } = this.state;
|
||||
const { children, fallback, showDetails = true } = this.props;
|
||||
|
||||
if (hasError) {
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-[400px] flex items-center justify-center p-6 bg-gray-900/50 rounded-xl border border-gray-700">
|
||||
<div className="max-w-lg w-full">
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="p-4 bg-red-500/20 rounded-full">
|
||||
<AlertTriangle className="w-12 h-12 text-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold text-white text-center mb-2">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="text-gray-400 text-center mb-6">
|
||||
An unexpected error occurred. You can try refreshing the page or return to the home screen.
|
||||
</p>
|
||||
|
||||
{error && showDetails && (
|
||||
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<Bug className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-red-400 mb-1">Error Message</p>
|
||||
<p className="text-sm text-gray-300 break-words">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error.stack && (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={this.toggleStack}
|
||||
className="flex items-center gap-2 text-sm text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
{showStack ? (
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
{showStack ? 'Hide' : 'Show'} Stack Trace
|
||||
</button>
|
||||
|
||||
{showStack && (
|
||||
<div className="mt-2 relative">
|
||||
<pre className="text-xs text-gray-400 bg-gray-800 p-3 rounded overflow-x-auto max-h-40">
|
||||
{error.stack}
|
||||
</pre>
|
||||
<button
|
||||
onClick={this.copyError}
|
||||
className="absolute top-2 right-2 p-1.5 bg-gray-700 hover:bg-gray-600 rounded transition-colors"
|
||||
title="Copy error"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
Try Again
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleRefresh}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
Refresh Page
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleGoHome}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<Home className="w-5 h-5" />
|
||||
Go Home
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center mt-4">
|
||||
If the problem persists, please contact support with the error details above.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@ -63,9 +63,9 @@ export type { LLMConfig, ModelInfo, ConfigPreset, ModelId, ReasoningStyle, Analy
|
||||
export { default as ContextMemoryDisplay } from './ContextMemoryDisplay';
|
||||
export type { ContextMessage, ContextSummary, ContextMemoryState } from './ContextMemoryDisplay';
|
||||
|
||||
// Error Handling & Status (OQI-007)
|
||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
||||
export type { ErrorBoundaryProps, ErrorBoundaryState } from './ErrorBoundary';
|
||||
// Error Handling & Status (OQI-007) - Re-export from shared component
|
||||
export { default as ErrorBoundary } from '../../../components/ErrorBoundary';
|
||||
export type { ErrorBoundaryProps, ErrorBoundaryState } from '../../../components/ErrorBoundary';
|
||||
|
||||
export { default as ConnectionStatus } from './ConnectionStatus';
|
||||
export type { ConnectionState, ConnectionMetrics, ConnectionStatusProps } from './ConnectionStatus';
|
||||
|
||||
@ -3,52 +3,15 @@
|
||||
* API client for LLM Copilot chat endpoints
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { apiClient } from '../lib/apiClient';
|
||||
import type {
|
||||
ChatSession,
|
||||
SendMessageResponse,
|
||||
CreateSessionResponse,
|
||||
} from '../types/chat.types';
|
||||
|
||||
// ============================================================================
|
||||
// API Configuration
|
||||
// ============================================================================
|
||||
|
||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || 'http://localhost:3000';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: `${API_BASE_URL}/api/v1/llm`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add request interceptor for auth token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Add response interceptor for error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||
// All paths prefixed with /llm/ since apiClient baseURL is /api/v1
|
||||
|
||||
// ============================================================================
|
||||
// Chat Service
|
||||
@ -59,7 +22,7 @@ export const chatService = {
|
||||
* Create a new chat session
|
||||
*/
|
||||
async createSession(): Promise<CreateSessionResponse> {
|
||||
const response = await api.post('/sessions');
|
||||
const response = await apiClient.post('/llm/sessions');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@ -67,7 +30,7 @@ export const chatService = {
|
||||
* Get all chat sessions for the current user
|
||||
*/
|
||||
async getSessions(): Promise<ChatSession[]> {
|
||||
const response = await api.get('/sessions');
|
||||
const response = await apiClient.get('/llm/sessions');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@ -75,7 +38,7 @@ export const chatService = {
|
||||
* Get a specific chat session with all messages
|
||||
*/
|
||||
async getSession(sessionId: string): Promise<ChatSession> {
|
||||
const response = await api.get(`/sessions/${sessionId}`);
|
||||
const response = await apiClient.get(`/llm/sessions/${sessionId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@ -86,7 +49,7 @@ export const chatService = {
|
||||
sessionId: string,
|
||||
message: string
|
||||
): Promise<SendMessageResponse> {
|
||||
const response = await api.post(`/sessions/${sessionId}/chat`, {
|
||||
const response = await apiClient.post(`/llm/sessions/${sessionId}/chat`, {
|
||||
message,
|
||||
});
|
||||
return response.data;
|
||||
@ -96,14 +59,14 @@ export const chatService = {
|
||||
* Delete a chat session
|
||||
*/
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
await api.delete(`/sessions/${sessionId}`);
|
||||
await apiClient.delete(`/llm/sessions/${sessionId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Quick analysis of a symbol (public endpoint, no auth required)
|
||||
*/
|
||||
async analyzeSymbol(symbol: string): Promise<{ analysis: string }> {
|
||||
const response = await api.get(`/analyze/${symbol}`);
|
||||
const response = await apiClient.get(`/llm/analyze/${symbol}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* API client for courses, lessons, quizzes, and gamification
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { apiClient as api } from '../lib/apiClient';
|
||||
import type {
|
||||
CourseListItem,
|
||||
CourseDetail,
|
||||
@ -27,23 +27,7 @@ import type {
|
||||
GamificationSummary,
|
||||
} from '../types/education.types';
|
||||
|
||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||
|
||||
// ============================================================================
|
||||
// Categories
|
||||
|
||||
@ -3,9 +3,10 @@
|
||||
* API client for investment accounts, products, transactions, and withdrawals
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { apiClient } from '../lib/apiClient';
|
||||
|
||||
const API_BASE = '/api/v1/investment';
|
||||
// apiClient baseURL is /api/v1, so prefix paths with /investment
|
||||
const API_PREFIX = '/investment';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@ -120,12 +121,12 @@ export interface AccountDetail extends InvestmentAccount {
|
||||
|
||||
export async function getProducts(riskProfile?: string): Promise<Product[]> {
|
||||
const params = riskProfile ? { riskProfile } : {};
|
||||
const response = await axios.get(`${API_BASE}/products`, { params });
|
||||
const response = await apiClient.get(`${API_PREFIX}/products`, { params });
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getProductById(productId: string): Promise<Product> {
|
||||
const response = await axios.get(`${API_BASE}/products/${productId}`);
|
||||
const response = await apiClient.get(`${API_PREFIX}/products/${productId}`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@ -133,7 +134,7 @@ export async function getProductPerformance(
|
||||
productId: string,
|
||||
period: 'week' | 'month' | '3months' | 'year' = 'month'
|
||||
): Promise<ProductPerformance[]> {
|
||||
const response = await axios.get(`${API_BASE}/products/${productId}/performance`, {
|
||||
const response = await apiClient.get(`${API_PREFIX}/products/${productId}/performance`, {
|
||||
params: { period },
|
||||
});
|
||||
return response.data.data;
|
||||
@ -144,27 +145,27 @@ export async function getProductPerformance(
|
||||
// ============================================================================
|
||||
|
||||
export async function getUserAccounts(): Promise<InvestmentAccount[]> {
|
||||
const response = await axios.get(`${API_BASE}/accounts`);
|
||||
const response = await apiClient.get(`${API_PREFIX}/accounts`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getAccountSummary(): Promise<AccountSummary> {
|
||||
const response = await axios.get(`${API_BASE}/accounts/summary`);
|
||||
const response = await apiClient.get(`${API_PREFIX}/accounts/summary`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getAccountById(accountId: string): Promise<AccountDetail> {
|
||||
const response = await axios.get(`${API_BASE}/accounts/${accountId}`);
|
||||
const response = await apiClient.get(`${API_PREFIX}/accounts/${accountId}`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function createAccount(productId: string, initialDeposit: number): Promise<InvestmentAccount> {
|
||||
const response = await axios.post(`${API_BASE}/accounts`, { productId, initialDeposit });
|
||||
const response = await apiClient.post(`${API_PREFIX}/accounts`, { productId, initialDeposit });
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function closeAccount(accountId: string): Promise<void> {
|
||||
await axios.post(`${API_BASE}/accounts/${accountId}/close`);
|
||||
await apiClient.post(`${API_PREFIX}/accounts/${accountId}/close`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@ -180,14 +181,14 @@ export async function getTransactions(
|
||||
offset?: number;
|
||||
}
|
||||
): Promise<{ transactions: Transaction[]; total: number }> {
|
||||
const response = await axios.get(`${API_BASE}/accounts/${accountId}/transactions`, {
|
||||
const response = await apiClient.get(`${API_PREFIX}/accounts/${accountId}/transactions`, {
|
||||
params: options,
|
||||
});
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function createDeposit(accountId: string, amount: number): Promise<Transaction> {
|
||||
const response = await axios.post(`${API_BASE}/accounts/${accountId}/deposit`, { amount });
|
||||
const response = await apiClient.post(`${API_PREFIX}/accounts/${accountId}/deposit`, { amount });
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@ -199,7 +200,7 @@ export async function createWithdrawal(
|
||||
cryptoInfo?: { network: string; address: string };
|
||||
}
|
||||
): Promise<Withdrawal> {
|
||||
const response = await axios.post(`${API_BASE}/accounts/${accountId}/withdraw`, {
|
||||
const response = await apiClient.post(`${API_PREFIX}/accounts/${accountId}/withdraw`, {
|
||||
amount,
|
||||
...destination,
|
||||
});
|
||||
@ -211,7 +212,7 @@ export async function createWithdrawal(
|
||||
// ============================================================================
|
||||
|
||||
export async function getDistributions(accountId: string): Promise<Distribution[]> {
|
||||
const response = await axios.get(`${API_BASE}/accounts/${accountId}/distributions`);
|
||||
const response = await apiClient.get(`${API_PREFIX}/accounts/${accountId}/distributions`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@ -221,7 +222,7 @@ export async function getDistributions(accountId: string): Promise<Distribution[
|
||||
|
||||
export async function getWithdrawals(status?: string): Promise<Withdrawal[]> {
|
||||
const params = status ? { status } : {};
|
||||
const response = await axios.get(`${API_BASE}/withdrawals`, { params });
|
||||
const response = await apiClient.get(`${API_PREFIX}/withdrawals`, { params });
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
|
||||
@ -3,25 +3,7 @@
|
||||
* API client for notifications management
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
import { apiClient as api } from '../lib/apiClient';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* API client for subscriptions, payments, billing, and wallet
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { apiClient as api } from '../lib/apiClient';
|
||||
import type {
|
||||
PricingPlan,
|
||||
Subscription,
|
||||
@ -24,23 +24,7 @@ import type {
|
||||
PlanInterval,
|
||||
} from '../types/payment.types';
|
||||
|
||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add auth token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||
|
||||
// ============================================================================
|
||||
// Pricing Plans
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* API client for trading and market data endpoints
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { apiClient } from '../lib/apiClient';
|
||||
import type {
|
||||
Candle,
|
||||
Ticker,
|
||||
@ -26,43 +26,8 @@ import type {
|
||||
AccountSummary,
|
||||
} from '../types/trading.types';
|
||||
|
||||
// ============================================================================
|
||||
// API Configuration
|
||||
// ============================================================================
|
||||
|
||||
const API_BASE_URL = import.meta.env?.VITE_API_URL || '/api/v1';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Add request interceptor for auth token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Add response interceptor for error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
// Uses centralized apiClient from lib/apiClient.ts (auto-refresh, multi-tab sync)
|
||||
const api = apiClient;
|
||||
|
||||
// ============================================================================
|
||||
// Market Data API
|
||||
|
||||
Loading…
Reference in New Issue
Block a user