Punit1's picture
Initial commit
939c0c0
Raw
History Blame Contribute Delete
8.59 kB
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<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);
}
);
// ── Auth API ──────────────────────────────────────────────────
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),
};
// ── 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<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),
};
// ── 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;