| 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'; |
|
|
| |
| const api: AxiosInstance = axios.create({ |
| baseURL: BASE_URL, |
| headers: { 'Content-Type': 'application/json' }, |
| timeout: 60000, |
| }); |
|
|
| |
| 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); |
| }, |
| }; |
|
|
| |
| api.interceptors.request.use((config) => { |
| const token = tokenStorage.getAccess(); |
| if (token) { |
| config.headers.Authorization = `Bearer ${token}`; |
| } |
| return config; |
| }); |
|
|
| |
| 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<TokenResponse>(`${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); |
| } |
| ); |
|
|
| |
| export const authApi = { |
| register: (data: { email: string; password: string; full_name: string; tenant_name: string }) => |
| api.post<RegisterResponse>('/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<TokenResponse>('/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<User>('/auth/me').then((r) => r.data), |
|
|
| refresh: (refresh_token: string) => |
| api.post<TokenResponse>('/auth/refresh', { refresh_token }).then((r) => r.data), |
| }; |
|
|
| |
| 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 { |
| |
| } |
| } |
| } |
| } 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), |
| }; |
|
|
| |
| 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<string, string> = {}; |
| 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), |
| }; |
|
|
| |
| 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), |
| }; |
|
|
| |
| export const healthApi = { |
| check: () => api.get('/health', { baseURL: '' }).then((r) => r.data), |
| }; |
|
|
| export default api; |
|
|