| 1 |
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios' |
| 2 |
import type { |
| 3 |
AuthRequest, |
| 4 |
AuthResponse, |
| 5 |
DirectoryListing, |
| 6 |
FileItem, |
| 7 |
UploadResponse, |
| 8 |
NetworkStatus, |
| 9 |
NodeStatus |
| 10 |
} from '@shared/types' |
| 11 |
|
| 12 |
class ApiClient { |
| 13 |
private client: AxiosInstance |
| 14 |
|
| 15 |
constructor(baseURL: string = '/api') { |
| 16 |
this.client = axios.create({ |
| 17 |
baseURL, |
| 18 |
timeout: 30000, |
| 19 |
headers: { |
| 20 |
'Content-Type': 'application/json', |
| 21 |
}, |
| 22 |
}) |
| 23 |
|
| 24 |
// Request interceptor to add auth token |
| 25 |
this.client.interceptors.request.use( |
| 26 |
(config) => { |
| 27 |
const token = localStorage.getItem('auth_token') |
| 28 |
if (token) { |
| 29 |
config.headers.Authorization = `Bearer ${token}` |
| 30 |
} |
| 31 |
return config |
| 32 |
}, |
| 33 |
(error) => Promise.reject(error) |
| 34 |
) |
| 35 |
|
| 36 |
// Response interceptor for error handling |
| 37 |
this.client.interceptors.response.use( |
| 38 |
(response) => response, |
| 39 |
async (error) => { |
| 40 |
if (error.response?.status === 401) { |
| 41 |
// Try to refresh token |
| 42 |
const refreshToken = localStorage.getItem('refresh_token') |
| 43 |
if (refreshToken) { |
| 44 |
try { |
| 45 |
const response = await this.refreshAuth(refreshToken) |
| 46 |
localStorage.setItem('auth_token', response.token) |
| 47 |
|
| 48 |
// Retry original request |
| 49 |
const originalRequest = error.config |
| 50 |
originalRequest.headers.Authorization = `Bearer ${response.token}` |
| 51 |
return this.client.request(originalRequest) |
| 52 |
} catch (refreshError) { |
| 53 |
// Refresh failed, redirect to login |
| 54 |
localStorage.removeItem('auth_token') |
| 55 |
localStorage.removeItem('refresh_token') |
| 56 |
window.location.href = '/login' |
| 57 |
} |
| 58 |
} else { |
| 59 |
// No refresh token, redirect to login |
| 60 |
window.location.href = '/login' |
| 61 |
} |
| 62 |
} |
| 63 |
return Promise.reject(error) |
| 64 |
} |
| 65 |
) |
| 66 |
} |
| 67 |
|
| 68 |
// Authentication |
| 69 |
async login(credentials: AuthRequest): Promise<AuthResponse> { |
| 70 |
const response = await this.client.post<AuthResponse>('/auth/login', credentials) |
| 71 |
return response.data |
| 72 |
} |
| 73 |
|
| 74 |
async refreshAuth(refreshToken: string): Promise<AuthResponse> { |
| 75 |
const response = await this.client.post<AuthResponse>('/auth/refresh', { refreshToken }) |
| 76 |
return response.data |
| 77 |
} |
| 78 |
|
| 79 |
async logout(): Promise<void> { |
| 80 |
await this.client.post('/auth/logout') |
| 81 |
} |
| 82 |
|
| 83 |
async getCurrentUser(): Promise<{ id: string; username: string }> { |
| 84 |
const response = await this.client.get('/auth/me') |
| 85 |
return response.data |
| 86 |
} |
| 87 |
|
| 88 |
// Files |
| 89 |
async listFiles(path: string = '/'): Promise<DirectoryListing> { |
| 90 |
const response = await this.client.get<DirectoryListing>('/files', { |
| 91 |
params: { path }, |
| 92 |
}) |
| 93 |
return response.data |
| 94 |
} |
| 95 |
|
| 96 |
async uploadFile( |
| 97 |
file: File, |
| 98 |
path: string = '/', |
| 99 |
options: { |
| 100 |
encrypted?: boolean; |
| 101 |
onProgress?: (progress: number) => void; |
| 102 |
} = {} |
| 103 |
): Promise<UploadResponse> { |
| 104 |
const formData = new FormData() |
| 105 |
formData.append('file', file) |
| 106 |
formData.append('path', path) |
| 107 |
if (options.encrypted !== undefined) { |
| 108 |
formData.append('encrypted', options.encrypted.toString()) |
| 109 |
} |
| 110 |
|
| 111 |
const response = await this.client.post<UploadResponse>('/files/upload', formData, { |
| 112 |
headers: { |
| 113 |
'Content-Type': 'multipart/form-data', |
| 114 |
}, |
| 115 |
onUploadProgress: (progressEvent) => { |
| 116 |
if (options.onProgress && progressEvent.total) { |
| 117 |
const progress = (progressEvent.loaded / progressEvent.total) * 100 |
| 118 |
options.onProgress(progress) |
| 119 |
} |
| 120 |
}, |
| 121 |
}) |
| 122 |
return response.data |
| 123 |
} |
| 124 |
|
| 125 |
async downloadFile(fileId: string): Promise<Blob> { |
| 126 |
const response = await this.client.get(`/files/${fileId}/download`, { |
| 127 |
responseType: 'blob', |
| 128 |
}) |
| 129 |
return response.data |
| 130 |
} |
| 131 |
|
| 132 |
async getFileInfo(fileId: string): Promise<FileItem> { |
| 133 |
const response = await this.client.get<FileItem>(`/files/${fileId}/info`) |
| 134 |
return response.data |
| 135 |
} |
| 136 |
|
| 137 |
async deleteFile(fileId: string): Promise<void> { |
| 138 |
await this.client.delete(`/files/${fileId}`) |
| 139 |
} |
| 140 |
|
| 141 |
// Status |
| 142 |
async getHealthStatus(): Promise<any> { |
| 143 |
const response = await this.client.get('/health') |
| 144 |
return response.data |
| 145 |
} |
| 146 |
|
| 147 |
async getNetworkStatus(): Promise<NetworkStatus> { |
| 148 |
const response = await this.client.get<NetworkStatus>('/status/network') |
| 149 |
return response.data |
| 150 |
} |
| 151 |
|
| 152 |
async getNodeStatus(): Promise<NodeStatus> { |
| 153 |
const response = await this.client.get<NodeStatus>('/status/node') |
| 154 |
return response.data |
| 155 |
} |
| 156 |
|
| 157 |
// WebSocket connection for real-time updates |
| 158 |
createStatusWebSocket(): WebSocket { |
| 159 |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' |
| 160 |
const wsUrl = `${protocol}//${window.location.host}/api/status/ws` |
| 161 |
return new WebSocket(wsUrl) |
| 162 |
} |
| 163 |
|
| 164 |
// Generic request method |
| 165 |
async request<T>(config: AxiosRequestConfig): Promise<T> { |
| 166 |
const response = await this.client.request<T>(config) |
| 167 |
return response.data |
| 168 |
} |
| 169 |
} |
| 170 |
|
| 171 |
// Create singleton instance |
| 172 |
export const apiClient = new ApiClient() |
| 173 |
export default apiClient |