import axios from 'axios'; import { auth } from '../lib/auth-client'; const API_BASE_URL = import.meta.env.VITE_API_URL || ''; export const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); /** * 🔐 ENTERPRISE AUTH INTERCEPTOR * * Automatically adds JWT token to ALL requests. * Backend extracts user_id from the verified token - NOT from request body. * This prevents users from manipulating user_id to access other users' data. */ api.interceptors.request.use(async (config) => { try { const isUrlAdminRoute = config.url && config.url.includes('/api/v1/admin'); // 1. Admin Token Bypass (highest priority for admin routes) if (isUrlAdminRoute) { const adminToken = sessionStorage.getItem('admin_token'); if (adminToken) { config.headers['Authorization'] = `Bearer ${adminToken}`; return config; } } // 2. Check for Developer API Token (for embedded widgets) const urlParams = new URLSearchParams(window.location.search); const urlToken = urlParams.get('token'); if (urlToken && urlToken.startsWith('dv_')) { sessionStorage.setItem('developerToken', urlToken); } const developerToken = sessionStorage.getItem('developerToken'); if (developerToken) { config.headers['Authorization'] = `Bearer ${developerToken}`; return config; } // 3. Get current session const { data: { session } } = await auth.getSession(); if (session?.access_token) { // 🔐 Send JWT token - Backend validates this cryptographically config.headers['Authorization'] = `Bearer ${session.access_token}`; // Also send user ID header for backward compatibility during migration config.headers['X-User-ID'] = session.user.id; // Send active workspace if selected const userStoreStr = localStorage.getItem('user-store'); if (userStoreStr) { try { const store = JSON.parse(userStoreStr); const workspaceId = store?.state?.activeWorkspaceId; if (workspaceId) { config.headers['X-Workspace-ID'] = workspaceId; } } catch (e) {} } } else { // Guest user - send guest ID in header (backend generates consistent guest ID) const guestId = localStorage.getItem('guestUserId'); if (guestId) { config.headers['X-User-ID'] = guestId; } } } catch (error) { console.warn('Auth interceptor error:', error); } return config; }); // Handle auth errors - redirect to login api.interceptors.response.use( (response) => response, (error) => { // Handle rate limiting (429 Too Many Requests) if (error.response?.status === 429) { const retryAfter = error.response?.headers?.['retry-after'] || 60; const message = `⚠️ API rate limit reached. Please wait ${retryAfter} seconds before trying again.`; console.warn('Rate limit hit:', message); // Expose error message for UI to display error.rateLimitMessage = message; error.isRateLimited = true; } // Handle server errors if (error.response?.status >= 500) { error.serverErrorMessage = 'Server is temporarily unavailable. Please try again in a moment.'; } if (error.response?.status === 401) { // Redirect to login if not already there, but ignore admin pages const path = window.location.pathname; if (path !== '/login' && path !== '/signup' && path !== '/' && !path.startsWith('/admin')) { window.location.href = '/login'; } } return Promise.reject(error); } ); // Import user ID helper import { getUserIdSync } from '../utils/userId'; // Get current user ID helper - uses unique ID per user/guest const getUserId = (): string => { return getUserIdSync(); }; // API Service Methods export const apiService = { // File operations uploadFiles: async (files: File[], signal?: AbortSignal) => { const formData = new FormData(); files.forEach(file => formData.append('files', file)); const userId = getUserId(); return api.post(`/api/v1/files/upload/${userId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, signal, // Pass abort signal to axios }); }, listFiles: async () => { const userId = getUserId(); return api.get(`/api/v1/files/list/${userId}`); }, deleteFile: async (fileId: string) => { const userId = getUserId(); return api.delete(`/api/v1/files/${userId}/${fileId}`); }, getLiveConnections: async () => { return api.get('/api/v1/connections'); }, deleteLiveConnection: async (connectionId: string) => { return api.delete(`/api/v1/connections/${connectionId}`); }, adoptGuestConnection: async (guestConnectionId: string, connectionMeta: any) => { return api.post('/api/v1/connections/adopt', { guest_connection_id: guestConnectionId, source_type: connectionMeta.source_type || 'api_push', host: connectionMeta.host || 'localhost', database_name: connectionMeta.database_name || '', target_table: connectionMeta.target_table || 'live_data', }); }, acceptInvite: async (token: string) => { return api.post('/api/v1/collaboration/invite/accept', { token }); }, deleteAllFiles: async () => { const userId = getUserId(); return api.delete(`/api/v1/files/${userId}/all`); }, rebuildIndex: async () => { const userId = getUserId(); return api.post(`/api/v1/files/${userId}/rebuild`); }, // Chat operations - USES AUTHENTICATED USER ID sendMessage: async ( message: string, model: string = 'llama', mode: string = 'rag', conversationId?: string, compareFiles?: string[], attachedFiles?: any[], enabledMcps?: Record, conversationHistory?: Array<{ role: string; content: string }> // For persistent memory ) => { return api.post('/api/v1/chat/message', { message, model, mode, conversationId, compareFiles, attachedFiles, conversationHistory: conversationHistory || [], // Pass conversation history for memory enabledMcps: enabledMcps || { data_cleaner: true, vectorizer: true, graph_builder: true, sql_executor: true, vision_ocr: true, data_transformer: true, data_validator: true, alert_engine: true, insight_engine: true, forecast_engine: true, }, }); }, // Streaming chat - word-by-word like ChatGPT streamMessage: async ( message: string, model: string = 'deepseek', mode: string = 'rag', onChunk: (chunk: string) => void, onDone: () => void, onError: (error: string) => void ) => { const userId = getUserId(); const baseUrl = import.meta.env.VITE_API_URL || ''; try { const response = await fetch(`${baseUrl}/api/v1/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-ID': userId, }, body: JSON.stringify({ message, model, mode, userId, }), }); if (!response.ok) { onError(`HTTP error: ${response.status}`); return; } const reader = response.body?.getReader(); if (!reader) { onError('No response body'); return; } const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value, { stream: true }); const lines = text.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { onDone(); return; } try { const parsed = JSON.parse(data); if (parsed.content) { onChunk(parsed.content); } if (parsed.error) { onError(parsed.error); return; } } catch { // Ignore parse errors for incomplete chunks } } } } onDone(); } catch (error: any) { onError(error.message || 'Stream error'); } }, getChatHistory: async () => { const userId = getUserId(); return api.get(`/api/v1/chat/history/${userId}`); }, getConversation: async (conversationId: string) => { const userId = getUserId(); return api.get(`/api/v1/chat/history/${userId}/${conversationId}`); }, deleteConversation: async (conversationId: string) => { const userId = getUserId(); return api.delete(`/api/v1/chat/history/${userId}/${conversationId}`); }, // Analytics operations getAnalyticsOverview: async () => { const userId = getUserId(); return api.get(`/api/v1/analytics/overview/${userId}`); }, // Schema-driven smart overview (Power BI style - works with ANY data) getSmartOverview: async () => { const userId = getUserId(); return api.get(`/api/v1/analytics/smart-overview/${userId}`); }, // Power BI-style unified analytics - single source of truth getUnifiedAnalytics: async (filterParams: string = '') => { const userId = getUserId(); return api.get(`/api/v1/analytics/unified/${userId}${filterParams}`); }, getRevenueDetails: async (period: string = 'all') => { const userId = getUserId(); return api.get(`/api/v1/analytics/revenue/${userId}`, { params: { period }, }); }, getCustomerAnalytics: async () => { const userId = getUserId(); return api.get(`/api/v1/analytics/customers/${userId}`); }, getProductAnalytics: async () => { const userId = getUserId(); return api.get(`/api/v1/analytics/products/${userId}`); }, // Enterprise Smart Analytics getInsights: async () => { const userId = getUserId(); return api.get(`/api/v1/analytics/insights/${userId}`); }, getDataProfile: async () => { const userId = getUserId(); return api.get(`/api/v1/analytics/data-profile/${userId}`); }, // $500K Enterprise Dashboard Stats getDashboardStats: async () => { const userId = getUserId(); return api.get('/api/v1/analytics/dashboard-stats', { params: { user_id: userId }, }); }, // Report operations generateReport: async (reportType: string, dateRange?: { start: string; end: string }, fallbackModel?: any) => { const userId = getUserId(); return api.post('/api/v1/reports/generate', { userId: userId, reportType: reportType, dateRange: dateRange || 'all', format: 'json', // Pass local model metadata as fallback if backend session is lost fallbackModel: fallbackModel || null }); }, getReport: async (reportId: string) => { return api.get(`/api/v1/reports/${reportId}`); }, exportReportPDF: async (reportId: string) => { return api.get(`/api/v1/reports/${reportId}/export/pdf`, { responseType: 'blob', }); }, // Real-time Exchange Rates getExchangeRates: async (baseCurrency: string = 'USD') => { return api.get('/api/v1/analytics/exchange-rates', { params: { base: baseCurrency }, }); }, convertCurrency: async (amount: number, fromCurrency: string, toCurrency: string) => { return api.get('/api/v1/analytics/convert-currency', { params: { amount, from_currency: fromCurrency, to_currency: toCurrency }, }); }, // Google Sheets Import importGoogleSheet: async (sheetUrl: string, sheetName?: string) => { const userId = getUserId(); return api.post(`/api/v1/files/${userId}/import-google-sheet`, null, { params: { sheet_url: sheetUrl, sheet_name: sheetName }, }); }, previewGoogleSheet: async (sheetUrl: string) => { const userId = getUserId(); return api.get(`/api/v1/files/${userId}/preview-google-sheet`, { params: { sheet_url: sheetUrl }, }); }, // AI Providers getAIProviders: async () => { return api.get('/api/v1/analytics/ai-providers'); }, // Charts - Plotly Generation generateChart: async (chartType: string, dataSource: string = 'revenue') => { const userId = getUserId(); return api.post('/api/v1/charts/generate', { user_id: userId, chart_type: chartType, data_source: dataSource, }); }, getChartTypes: async () => { return api.get('/api/v1/charts/available-types'); }, // ===== SCHEMA-DRIVEN ANALYTICS API ===== // Single source of truth for frontend schema consumption // Get full schema intelligence (domain, metrics, dimensions, time_column, etc.) getSchema: async (refresh: boolean = false) => { const userId = getUserId(); return api.get(`/api/v1/schema/${userId}`, { params: { refresh }, }); }, // Get only metric columns (optimized for dropdowns) getSchemaMetrics: async () => { const userId = getUserId(); return api.get(`/api/v1/schema/${userId}/metrics`); }, // Get only dimension columns (optimized for dropdowns) getSchemaDimensions: async () => { const userId = getUserId(); return api.get(`/api/v1/schema/${userId}/dimensions`); }, // Get chart-ready data based on selected columns getChartData: async (params: { chartType: 'line' | 'bar' | 'pie' | 'histogram' | 'scatter'; xColumn: string; yColumn?: string; groupBy?: string; startDate?: string; endDate?: string; limit?: number; }) => { const userId = getUserId(); return api.get(`/api/v1/schema/${userId}/chart-data`, { params: { chart_type: params.chartType, x_column: params.xColumn, y_column: params.yColumn, group_by: params.groupBy, start_date: params.startDate, end_date: params.endDate, limit: params.limit || 20, }, }); }, // Anomalies API getAnomaliesOverview: async () => { return api.get('/api/v1/anomalies/overview'); }, triggerAnomalyScan: async () => { return api.post('/api/v1/anomalies/scan'); }, triggerAutoFix: async () => { return api.post('/api/v1/anomalies/auto-fix'); }, // Simulator API getSimulatorVariables: async () => { return api.get('/api/v1/simulator/variables'); }, runSimulation: async (variables: Record, scenarioName?: string) => { return api.post('/api/v1/simulator/run', { variables, scenario_name: scenarioName }); }, getSimulatorOverview: async () => { return api.get('/api/v1/simulator/overview'); }, // Scenarios saveScenario: async (data: { name: string; description?: string; variables: Record; prediction?: number; confidence?: number; metrics?: any; tags?: string[] }) => { return api.post('/api/v1/simulator/scenarios/save', data); }, listScenarios: async () => { return api.get('/api/v1/simulator/scenarios'); }, deleteScenario: async (id: string) => { return api.delete(`/api/v1/simulator/scenarios/${id}`); }, cloneScenario: async (id: string) => { return api.post(`/api/v1/simulator/scenarios/${id}/clone`); }, importScenario: async (data: any) => { return api.post('/api/v1/simulator/scenarios/import', data); }, // Forecast getSimulatorForecast: async (variables: Record, periods: number = 12, interval: string = 'monthly') => { return api.post('/api/v1/simulator/forecast', { variables, periods, interval }); }, // AI Insights getSimulatorInsights: async (variables: Record) => { return api.post('/api/v1/simulator/insights', { variables }); }, // Variable Importance getFeatureImportance: async () => { return api.post('/api/v1/simulator/importance'); }, getPartialDependence: async (feature: string) => { return api.post(`/api/v1/simulator/partial-dependence?feature=${encodeURIComponent(feature)}`); }, // AI Suggested Scenarios getSuggestedScenarios: async () => { return api.post('/api/v1/simulator/suggest-scenarios'); }, // Comparison compareScenarios: async (scenarioIds: string[] = [], scenarioValues?: Record[]) => { return api.post('/api/v1/simulator/compare', { scenario_ids: scenarioIds, scenario_values: scenarioValues }); }, // Optimization runOptimization: async (data: { objective?: string; objective_type?: string; constraints?: any; max_iterations?: number }) => { return api.post('/api/v1/simulator/optimize', data); }, getOptimizationHistory: async () => { return api.get('/api/v1/simulator/optimize/history'); }, // History getSimulationHistory: async (page: number = 1, limit: number = 20, search: string = '') => { return api.get('/api/v1/simulator/history', { params: { page, limit, search } }); }, getSimulationDetail: async (id: string) => { return api.get(`/api/v1/simulator/history/${id}`); }, restoreSimulation: async (id: string) => { return api.post(`/api/v1/simulator/history/${id}/restore`); }, // Reports generateSimulatorReport: async (data: { title?: string; report_type?: string; format?: string; include_scenarios?: string[]; include_charts?: boolean }) => { return api.post('/api/v1/simulator/reports/generate', data); }, listSimulatorReports: async () => { return api.get('/api/v1/simulator/reports'); }, downloadSimulatorReport: async (id: string) => { return api.get(`/api/v1/simulator/reports/${id}/download`); }, // Global Search API globalSearch: async (query: string) => { return api.get('/api/v1/search', { params: { q: query } }); }, // Multi-Dataset Join API getJoinSuggestions: async (file1: string, file2: string) => { const userId = getUserId(); const response = await api.get(`/api/v1/files/${userId}/join-suggestions`, { params: { file1, file2 }, }); return response.data; }, joinDatasets: async (params: { file1: string, file2: string, left_on: string, right_on: string, how: string, output_filename: string }) => { const userId = getUserId(); const response = await api.post(`/api/v1/files/${userId}/join`, params); return response.data; }, }; export default apiService;