forked from 0xWheatyz/SPARC
cb7d7121c5
Add modern React frontend to replace Streamlit dashboard: - Vite build system with TypeScript - Tailwind CSS for styling - Component structure in src/ - Production Dockerfile with nginx - Development server on port 5173 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
155 lines
4.6 KiB
TypeScript
155 lines
4.6 KiB
TypeScript
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
|
import type { TokenResponse, User, CompanyAnalysis, BatchAnalysisResult, JobStatus, Analytics } from '../types';
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
|
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Token management
|
|
let accessToken: string | null = localStorage.getItem('access_token');
|
|
let refreshToken: string | null = localStorage.getItem('refresh_token');
|
|
|
|
export const setTokens = (tokens: TokenResponse) => {
|
|
accessToken = tokens.access_token;
|
|
refreshToken = tokens.refresh_token;
|
|
localStorage.setItem('access_token', tokens.access_token);
|
|
localStorage.setItem('refresh_token', tokens.refresh_token);
|
|
};
|
|
|
|
export const clearTokens = () => {
|
|
accessToken = null;
|
|
refreshToken = null;
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
};
|
|
|
|
export const getAccessToken = () => accessToken;
|
|
|
|
// Request interceptor to add auth header
|
|
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
|
if (accessToken) {
|
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// Response interceptor to handle token refresh
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
async (error: AxiosError) => {
|
|
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
|
|
|
if (error.response?.status === 401 && !originalRequest._retry && refreshToken) {
|
|
originalRequest._retry = true;
|
|
|
|
try {
|
|
const response = await axios.post<TokenResponse>(`${API_BASE_URL}/auth/refresh`, {
|
|
refresh_token: refreshToken,
|
|
});
|
|
|
|
setTokens(response.data);
|
|
originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`;
|
|
|
|
return api(originalRequest);
|
|
} catch {
|
|
clearTokens();
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Auth API
|
|
export const authApi = {
|
|
register: async (email: string, password: string): Promise<User> => {
|
|
const response = await api.post<User>('/auth/register', { email, password });
|
|
return response.data;
|
|
},
|
|
|
|
login: async (email: string, password: string): Promise<TokenResponse> => {
|
|
const response = await api.post<TokenResponse>('/auth/login', { email, password });
|
|
setTokens(response.data);
|
|
return response.data;
|
|
},
|
|
|
|
getMe: async (): Promise<User> => {
|
|
const response = await api.get<User>('/auth/me');
|
|
return response.data;
|
|
},
|
|
|
|
logout: () => {
|
|
clearTokens();
|
|
},
|
|
};
|
|
|
|
// Analysis API
|
|
export const analysisApi = {
|
|
analyzeCompany: async (companyName: string): Promise<CompanyAnalysis> => {
|
|
const response = await api.get<CompanyAnalysis>(`/analyze/${encodeURIComponent(companyName)}`);
|
|
return response.data;
|
|
},
|
|
|
|
analyzeBatch: async (companies: string[], maxWorkers = 3): Promise<BatchAnalysisResult> => {
|
|
const response = await api.post<BatchAnalysisResult>('/analyze/batch', {
|
|
companies,
|
|
max_workers: maxWorkers,
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
analyzeBatchAsync: async (companies: string[], maxWorkers = 3): Promise<JobStatus> => {
|
|
const response = await api.post<JobStatus>('/analyze/batch/async', {
|
|
companies,
|
|
max_workers: maxWorkers,
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
getJobStatus: async (jobId: string): Promise<JobStatus> => {
|
|
const response = await api.get<JobStatus>(`/jobs/${jobId}`);
|
|
return response.data;
|
|
},
|
|
|
|
listJobs: async (status?: string, limit = 10): Promise<JobStatus[]> => {
|
|
const params = new URLSearchParams();
|
|
if (status) params.append('status', status);
|
|
params.append('limit', limit.toString());
|
|
const response = await api.get<JobStatus[]>(`/jobs?${params}`);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Analytics API
|
|
export const analyticsApi = {
|
|
getAnalytics: async (days = 30): Promise<Analytics> => {
|
|
const response = await api.get<Analytics>(`/analytics?days=${days}`);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Admin API
|
|
export const adminApi = {
|
|
listUsers: async (limit = 100, offset = 0): Promise<User[]> => {
|
|
const response = await api.get<User[]>(`/admin/users?limit=${limit}&offset=${offset}`);
|
|
return response.data;
|
|
},
|
|
|
|
updateUserRole: async (userId: number, role: 'admin' | 'user'): Promise<User> => {
|
|
const response = await api.patch<User>(`/admin/users/${userId}/role`, { role });
|
|
return response.data;
|
|
},
|
|
|
|
deleteUser: async (userId: number): Promise<void> => {
|
|
await api.delete(`/admin/users/${userId}`);
|
|
},
|
|
};
|
|
|
|
export default api;
|