import axios from 'axios'; import {toast} from 'sonner'; import type { AccessKey, Bucket, BucketDetails, ClusterHealth, ClusterStatistics, ClusterStatus, GarageMetrics, MultiNodeResponse, MultiNodeStatisticsResponse, ObjectListResponse, ObjectMetadata, S3Object, StorageMetrics, } from '@/types'; import type { AuthUser } from '@/types/auth'; const api = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json', }, }); // Separate axios instance for auth endpoints (which are not under /api) const authApiClient = axios.create({ baseURL: '/auth', headers: { 'Content-Type': 'application/json', }, }); api.interceptors.request.use((config) => { const token = localStorage.getItem('auth-token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); authApiClient.interceptors.request.use((config) => { const token = localStorage.getItem('auth-token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); api.interceptors.response.use( (response) => { // If response has success=false in data, treat it as an error if (response.data && response.data.success === false && response.data.error) { const error = response.data.error; const errorMessage = error.message || 'An error occurred'; const errorCode = error.code || 'UNKNOWN_ERROR'; // Display toast with error details toast.error(errorMessage, { description: `Error Code: ${errorCode}`, }); // Reject the promise so it's treated as an error return Promise.reject(new Error(errorMessage)); } return response; }, (error) => { // Handle 401 Unauthorized - redirect to login if (error.response?.status === 401) { // Clear auth token localStorage.removeItem('auth-token'); // Only redirect if not already on login page if (window.location.pathname !== '/login') { window.location.href = '/login'; } return Promise.reject(error); } // Handle axios errors if (error.response) { // Server responded with error status const data = error.response.data; if (data && data.error) { const errorMessage = data.error.message || 'An error occurred'; const errorCode = data.error.code || 'UNKNOWN_ERROR'; toast.error(errorMessage, { description: `Error Code: ${errorCode}`, }); } else { // Generic HTTP error toast.error(`Request failed: ${error.response.status}`, { description: error.response.statusText || 'Unknown error', }); } } else if (error.request) { // Request made but no response received toast.error('Network Error', { description: 'Unable to reach the server. Please check your connection.', }); } else { // Something else happened toast.error('Error', { description: error.message || 'An unexpected error occurred', }); } return Promise.reject(error); } ); // Auth API export const authApi = { getConfig: async () => { const response = await authApiClient.get<{ admin: { enabled: boolean }; oidc: { enabled: boolean; provider?: string }; }>('/config'); return response; }, loginAdmin: async (username: string, password: string) => { const response = await authApiClient.post<{ success: boolean; token: string; user: AuthUser }>('/login', { username, password, }); return response; }, me: async () => { const response = await authApiClient.get<{ success: boolean; user: AuthUser }>('/me'); return response; }, logoutAdmin: async () => { // For admin, just clear local storage (no server logout needed) return Promise.resolve(); }, logoutOIDC: async () => { const response = await authApiClient.post('/oidc/logout'); return response; }, loginOIDC: () => { window.location.href = '/auth/oidc/login'; }, }; // Bucket API export const bucketsApi = { list: async (): Promise => { const response = await api.get('/v1/buckets'); return response.data.data.buckets || []; }, get: async (name: string): Promise => { const response = await api.get(`/v1/buckets/${name}`); return response.data.data; }, create: async (bucketName: string, bucketRegion?: string): Promise => { await api.post('/v1/buckets', { name: bucketName, region: bucketRegion }); }, delete: async (name: string): Promise => { await api.delete(`/v1/buckets/${name}`); }, grantPermission: async ( bucketName: string, accessKeyId: string, permissions: { read: boolean; write: boolean; owner: boolean } ): Promise => { await api.post(`/v1/buckets/${bucketName}/permissions`, { accessKeyId, permissions, }); }, updateSettings: async (name: string, settings: Partial): Promise => { await api.patch(`/v1/buckets/${name}/settings`, settings); }, }; // Objects API export const objectsApi = { list: async (bucket: string, prefix?: string, maxKeys?: number, continuationToken?: string): Promise => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const params: any = {}; if (prefix) params.prefix = prefix; if (maxKeys) params.max_keys = maxKeys; if (continuationToken) params.continuation_token = continuationToken; const response = await api.get(`/v1/buckets/${bucket}/objects`, { params }); const data = response.data.data; // Combine objects and prefixes (folders) // eslint-disable-next-line @typescript-eslint/no-explicit-any const objects: S3Object[] = data.objects?.map((obj: any) => ({ key: obj.key, size: obj.size, lastModified: obj.last_modified, etag: obj.etag, storageClass: obj.storage_class, isFolder: false, })) || []; const folders: S3Object[] = data.prefixes?.map((prefix: string) => ({ key: prefix, size: 0, lastModified: null, isFolder: true, })) || []; return { bucket: data.bucket, objects: [...folders, ...objects], prefixes: data.prefixes || [], count: data.count, isTruncated: data.is_truncated || false, nextContinuationToken: data.next_continuation_token, }; }, get: async (bucket: string, key: string): Promise => { const response = await api.get(`/v1/buckets/${bucket}/objects/${encodeURIComponent(key)}`, { responseType: 'blob' }); return response.data; }, getMetadata: async (bucket: string, key: string): Promise => { const response = await api.head(`/v1/buckets/${bucket}/objects/${encodeURIComponent(key)}`); return response.data.data; }, upload: async (bucket: string, key: string, file: File): Promise => { const formData = new FormData(); formData.append('file', file); formData.append('key', key); await api.post(`/v1/buckets/${bucket}/objects`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any uploadMultiple: async (bucket: string, files: File[]): Promise => { const formData = new FormData(); files.forEach(file => { formData.append('files', file); }); const response = await api.post(`/v1/buckets/${bucket}/objects/upload-multiple`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); return response.data.data; }, delete: async (bucket: string, key: string): Promise => { await api.delete(`/v1/buckets/${bucket}/objects/${encodeURIComponent(key)}`); }, deleteMultiple: async (bucket: string, keys: string[], prefix?: string): Promise => { const payload = { keys, ...(prefix && { prefix }) }; await api.post(`/v1/buckets/${bucket}/objects/delete-multiple`, payload); }, getPresignedUrl: async (bucket: string, key: string, expiresIn: number = 3600): Promise => { const response = await api.post(`/v1/buckets/${bucket}/objects/${encodeURIComponent(key)}/presign`, {}, { params: { expires_in: expiresIn } }); return response.data.data.url; }, }; // Access Control API (Users/Keys) export const accessApi = { listKeys: async (): Promise => { const response = await api.get('/v1/users'); return response.data.data.users || []; }, getKey: async (accessKey: string): Promise => { const response = await api.get(`/v1/users/${accessKey}`); return response.data.data; }, getSecretKey: async (accessKey: string): Promise => { const response = await api.get(`/v1/users/${accessKey}/secret`); return response.data.data.secretKey; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any createKey: async (name: string, permissions?: any[]): Promise => { const response = await api.post('/v1/users', { name, permissions }); return response.data.data; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any updateKey: async (accessKey: string, updates: any): Promise => { await api.patch(`/v1/users/${accessKey}`, updates); }, deleteKey: async (accessKey: string): Promise => { await api.delete(`/v1/users/${accessKey}`); }, }; // Analytics API export const analyticsApi = { getMetrics: async (): Promise => { const response = await api.get('/v1/monitoring/dashboard'); return response.data.data; }, }; // Garage Cluster & Monitoring API export const garageApi = { getClusterHealth: async (): Promise => { const response = await api.get('/v1/cluster/health'); return response.data.data; }, getClusterStatus: async (): Promise => { const response = await api.get('/v1/cluster/status'); return response.data.data; }, getClusterStatistics: async (): Promise => { const response = await api.get('/v1/cluster/statistics'); return response.data.data; }, getNodeInfo: async (nodeId: string = 'self'): Promise => { const response = await api.get(`/v1/cluster/nodes/${nodeId}`); return response.data.data; }, getNodeStatistics: async (nodeId: string): Promise => { const response = await api.get(`/v1/cluster/nodes/${nodeId}/statistics`); return response.data.data; }, getFullMetrics: async (): Promise => { // Fetch all cluster-related metrics const [health, statistics, storageMetrics] = await Promise.all([ garageApi.getClusterHealth(), garageApi.getClusterStatistics(), analyticsApi.getMetrics(), ]); return { ...storageMetrics, clusterHealth: health, clusterStatistics: statistics, }; }, }; // Monitoring API export const monitoringApi = { // eslint-disable-next-line @typescript-eslint/no-explicit-any getMetrics: async (): Promise => { const response = await api.get('/v1/monitoring/metrics'); return response.data.data; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any checkAdminHealth: async (): Promise => { const response = await api.get('/v1/monitoring/admin-health'); return response.data.data; }, }; export default api;