Spaces:
Running
Running
| 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<string, boolean>, | |
| 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<string, any>, 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<string, any>; 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<string, any>, periods: number = 12, interval: string = 'monthly') => { | |
| return api.post('/api/v1/simulator/forecast', { variables, periods, interval }); | |
| }, | |
| // AI Insights | |
| getSimulatorInsights: async (variables: Record<string, any>) => { | |
| 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<string, any>[]) => { | |
| 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; | |