import axios, { AxiosInstance, AxiosError } from 'axios'; import type { RegisterResponse, TokenResponse, User } from '@/types'; const BASE_URL = import.meta.env.VITE_API_URL ? `${import.meta.env.VITE_API_URL}/api/v1` : '/api/v1'; // ── Axios Instance ─────────────────────────────────────────── const api: AxiosInstance = axios.create({ baseURL: BASE_URL, headers: { 'Content-Type': 'application/json' }, timeout: 60000, }); // ── Token Storage ──────────────────────────────────────────── const TOKEN_KEY = 'copilot_access_token'; const REFRESH_KEY = 'copilot_refresh_token'; export const tokenStorage = { getAccess: () => localStorage.getItem(TOKEN_KEY), getRefresh: () => localStorage.getItem(REFRESH_KEY), setTokens: (access: string, refresh: string) => { localStorage.setItem(TOKEN_KEY, access); localStorage.setItem(REFRESH_KEY, refresh); }, clear: () => { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_KEY); }, }; // ── Request Interceptor: attach token ──────────────────────── api.interceptors.request.use((config) => { const token = tokenStorage.getAccess(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // ── Response Interceptor: auto-refresh on 401 ──────────────── let isRefreshing = false; let failedQueue: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void }> = []; const processQueue = (error: unknown, token: string | null = null) => { failedQueue.forEach((p) => (token ? p.resolve(token) : p.reject(error))); failedQueue = []; }; api.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as typeof error.config & { _retry?: boolean }; if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }) .then((token) => { originalRequest.headers!['Authorization'] = `Bearer ${token}`; return api(originalRequest); }) .catch((err) => Promise.reject(err)); } originalRequest._retry = true; isRefreshing = true; const refreshToken = tokenStorage.getRefresh(); if (!refreshToken) { tokenStorage.clear(); window.location.href = '/login'; return Promise.reject(error); } try { const { data } = await axios.post(`${BASE_URL}/auth/refresh`, { refresh_token: refreshToken, }); tokenStorage.setTokens(data.access_token, data.refresh_token); processQueue(null, data.access_token); originalRequest.headers!['Authorization'] = `Bearer ${data.access_token}`; return api(originalRequest); } catch (refreshError) { processQueue(refreshError, null); tokenStorage.clear(); window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); // ── Auth API ────────────────────────────────────────────────── export const authApi = { register: (data: { email: string; password: string; full_name: string; tenant_name: string }) => api.post('/auth/register', data).then((r) => r.data), login: (email: string, password: string) => { const form = new URLSearchParams(); form.append('username', email); form.append('password', password); return api .post('/auth/login', form, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }) .then((r) => r.data); }, logout: () => api.post('/auth/logout').then((r) => r.data), me: () => api.get('/auth/me').then((r) => r.data), refresh: (refresh_token: string) => api.post('/auth/refresh', { refresh_token }).then((r) => r.data), }; // ── Chat API ────────────────────────────────────────────────── export const chatApi = { sendMessage: (message: string, session_id?: string) => api .post('/chat/', { message, session_id }) .then((r) => r.data), streamMessage: async ( message: string, session_id: string | undefined, onToken: (token: string) => void, onMeta: (meta: { session_id: string; agent_type: string; routing_decision: string }) => void, onSources: (sources: any) => void, onDone: (info: { tokens_used: number; latency_ms: number }) => void, onError: (error: string) => void, ) => { const token = tokenStorage.getAccess(); try { const res = await fetch(`${BASE_URL}/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ message, session_id }), }); if (!res.ok) { onError(`HTTP ${res.status}`); return; } const reader = res.body?.getReader(); if (!reader) { onError('No response stream'); return; } const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; try { const data = JSON.parse(line.slice(6)); switch (data.type) { case 'meta': onMeta(data); break; case 'token': onToken(data.content); break; case 'sources': onSources(data); break; case 'done': onDone(data); break; } } catch { // skip malformed JSON } } } } catch (err: any) { onError(err.message || 'Stream connection failed'); } }, getSessions: () => api.get('/chat/sessions').then((r) => r.data), getSessionMessages: (sessionId: string) => api.get(`/chat/sessions/${sessionId}`).then((r) => r.data), deleteSession: (sessionId: string) => api.delete(`/chat/sessions/${sessionId}`).then((r) => r.data), }; // ── Documents API ───────────────────────────────────────────── export const documentsApi = { upload: (file: File, docType: string = 'general') => { const formData = new FormData(); formData.append('file', file); formData.append('doc_type', docType); return api .post('/documents/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }) .then((r) => r.data); }, list: (docType?: string) => { const params: Record = {}; if (docType) params['doc_type'] = docType; return api.get('/documents/', { params }).then((r) => r.data); }, get: (id: string) => api.get(`/documents/${id}`).then((r) => r.data), getStatus: (id: string) => api.get(`/documents/${id}/status`).then((r) => r.data), delete: (id: string) => api.delete(`/documents/${id}`).then((r) => r.data), }; // ── Admin API ───────────────────────────────────────────────── export const adminApi = { getUsers: () => api.get('/admin/users').then((r) => r.data), inviteUser: (data: { email: string; full_name: string; role: string; password?: string }) => api.post('/admin/users/invite', data).then((r) => r.data), getAuditLogs: () => api.get('/admin/audit-logs').then((r) => r.data), getTenant: () => api.get('/admin/tenant').then((r) => r.data), }; // ── Health API ──────────────────────────────────────────────── export const healthApi = { check: () => api.get('/health', { baseURL: '' }).then((r) => r.data), }; export default api;