class APIClient { constructor(baseURL = '/', logger = null, perfMonitor = null, appState = null) { this.baseURL = baseURL; this.token = localStorage.getItem('token'); this.logger = logger; this.perfMonitor = perfMonitor; this.appState = appState; this.activeRequests = 0; } setToken(token) { this.token = token; if (token) { localStorage.setItem('token', token); } else { localStorage.removeItem('token'); } } getToken() { return this.token; } async request(endpoint, options = {}) { this.activeRequests++; this.appState?.setState({ isLoading: true }); const startTime = performance.now(); const url = `${this.baseURL}${endpoint}`; const method = options.method || 'GET'; try { this.logger?.debug(`API ${method}: ${endpoint}`); const headers = { ...options.headers, }; if (this.token && !options.skipAuth) { headers['Authorization'] = `Bearer ${this.token}`; } const config = { ...options, headers, }; if (config.body) { if (config.body instanceof FormData) { let hasFile = false; for (let pair of config.body.entries()) { if (pair[1] instanceof File) { hasFile = true; break; } } if (!hasFile) { this.logger?.error('FormData without File objects not allowed - JSON only communication enforced'); throw new Error('FormData is only allowed for file uploads'); } this.logger?.debug('File upload detected, allowing FormData'); } else if (typeof config.body === 'object') { headers['Content-Type'] = 'application/json'; config.body = JSON.stringify(config.body); this.logger?.debug('Request body serialized to JSON'); } } const response = await fetch(url, config); const duration = performance.now() - startTime; if (this.perfMonitor) { this.perfMonitor._recordMetric('api-request', duration); this.perfMonitor._recordMetric(`api-${method}`, duration); } if (response.status === 401) { this.logger?.warn('Unauthorized request, clearing token'); this.setToken(null); window.location.href = '/'; } if (!response.ok) { let errorData; try { errorData = await response.json(); } catch (e) { errorData = { message: 'Unknown error' }; } const errorMessage = errorData.detail || errorData.message || 'Request failed'; this.logger?.error(`API ${method} ${endpoint} failed: ${errorMessage}`, { status: response.status, duration: `${duration.toFixed(2)}ms` }); document.dispatchEvent(new CustomEvent('show-toast', { detail: { message: errorMessage, type: 'error' } })); throw new Error(errorMessage); } this.logger?.debug(`API ${method} ${endpoint} success`, { status: response.status, duration: `${duration.toFixed(2)}ms` }); if (response.status === 204) { return null; } const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { this.logger?.error(`Invalid response content-type: ${contentType}. Expected application/json`); throw new Error('Server returned non-JSON response'); } return response.json(); } catch (error) { this.logger?.error(`API ${method} ${endpoint} exception`, error); throw error; } finally { this.activeRequests--; if (this.activeRequests === 0) { this.appState?.setState({ isLoading: false }); } } } async register(username, email, password) { this.logger?.info('Attempting registration', { username, email }); const data = await this.request('auth/register', { method: 'POST', body: { username, email, password }, skipAuth: true }); if (!data || !data.access_token) { this.logger?.error('Invalid registration response: missing access_token', data); throw new Error('Invalid registration response'); } this.logger?.info('Registration successful', { username }); this.setToken(data.access_token); return data; } async login(username, password) { this.logger?.info('Attempting login', { username }); const data = await this.request('auth/token', { method: 'POST', body: { username, password }, skipAuth: true }); if (!data || !data.access_token) { this.logger?.error('Invalid login response: missing access_token', data); throw new Error('Invalid authentication response'); } this.logger?.info('Login successful', { username }); this.setToken(data.access_token); return data; } logout() { this.setToken(null); window.location.href = '/'; } async getCurrentUser() { return this.request('users/me'); } async listFolders(parentId = null) { const params = parentId ? `?parent_id=${parentId}` : ''; return this.request(`folders/${params}`); } async createFolder(name, parentId = null) { return this.request('folders/', { method: 'POST', body: { name, parent_id: parentId } }); } async getFolderPath(folderId) { return this.request(`folders/${folderId}/path`); } async deleteFolder(folderId) { return this.request(`folders/${folderId}`, { method: 'DELETE' }); } async uploadFile(file, folderId = null) { const formData = new FormData(); formData.append('file', file); const endpoint = folderId ? `files/upload?folder_id=${folderId}` : 'files/upload'; return this.request(endpoint, { method: 'POST', body: formData }); } async listFiles(folderId = null) { const params = folderId ? `?folder_id=${folderId}` : ''; return this.request(`files/${params}`); } async downloadFile(fileId) { const url = `${this.baseURL}files/download/${fileId}`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Download failed'); } return response.blob(); } async deleteFile(fileId) { return this.request(`files/${fileId}`, { method: 'DELETE' }); } async moveFile(fileId, targetFolderId) { return this.request(`files/${fileId}/move`, { method: 'POST', body: { target_folder_id: targetFolderId } }); } async renameFile(fileId, newName) { return this.request(`files/${fileId}/rename`, { method: 'POST', body: { new_name: newName } }); } async copyFile(fileId, targetFolderId) { return this.request(`files/${fileId}/copy`, { method: 'POST', body: { target_folder_id: targetFolderId } }); } async createShare(fileId = null, folderId = null, expiresAt = null, password = null, permissionLevel = 'viewer') { return this.request('shares/', { method: 'POST', body: { file_id: fileId, folder_id: folderId, expires_at: expiresAt, password, permission_level: permissionLevel } }); } async deleteShare(shareId) { return this.request(`shares/${shareId}`, { method: 'DELETE' }); } async searchFiles(query, filters = {}) { const params = new URLSearchParams({ q: query, ...filters }); return this.request(`search/files?${params}`); } async searchFolders(query) { return this.request(`search/folders?q=${encodeURIComponent(query)}`); } async getPhotos() { return this.request('files/photos'); } async getThumbnailUrl(fileId) { return `${this.baseURL}files/thumbnail/${fileId}`; } async listDeletedFiles() { return this.request('files/deleted'); } async restoreFile(fileId) { return this.request(`files/${fileId}/restore`, { method: 'POST' }); } async listUsers() { return this.request('admin/users'); } async createUser(userData) { return this.request('admin/users', { method: 'POST', body: userData }); } async updateUser(userId, userData) { return this.request(`admin/users/${userId}`, { method: 'PUT', body: userData }); } async deleteUser(userId) { return this.request(`admin/users/${userId}`, { method: 'DELETE' }); } async starFile(fileId) { return this.request(`files/${fileId}/star`, { method: 'POST' }); } async unstarFile(fileId) { return this.request(`files/${fileId}/unstar`, { method: 'POST' }); } async starFolder(folderId) { return this.request(`folders/${folderId}/star`, { method: 'POST' }); } async unstarFolder(folderId) { return this.request(`folders/${folderId}/unstar`, { method: 'POST' }); } async listStarredFiles() { return this.request('starred/files'); } async listStarredFolders() { return this.request('starred/folders'); } async listRecentFiles() { return this.request('files/recent'); } async listMyShares() { return this.request('shares/my'); } async updateShare(shareId, shareData) { return this.request(`shares/${shareId}`, { method: 'PUT', body: shareData }); } async batchFileOperations(operation, fileIds, targetFolderId = null) { const payload = { file_ids: fileIds, operation: operation }; if (targetFolderId !== null) { payload.target_folder_id = targetFolderId; } return this.request('files/batch', { method: 'POST', body: payload }); } async batchFolderOperations(operation, folderIds, targetFolderId = null) { const payload = { folder_ids: folderIds, operation: operation }; if (targetFolderId !== null) { payload.target_folder_id = targetFolderId; } return this.request('folders/batch', { method: 'POST', body: payload }); } async updateFile(fileId, content) { return this.request(`files/${fileId}/content`, { method: 'PUT', body: { content } }); } } export { APIClient }; let _sharedInstance = null; export function setSharedAPIInstance(instance) { _sharedInstance = instance; } export const api = new Proxy({}, { get(target, prop) { if (!_sharedInstance) { console.warn('API instance accessed before initialization. This may cause issues with logging and state management.'); _sharedInstance = new APIClient(); } const instance = _sharedInstance; const value = instance[prop]; if (typeof value === 'function') { return value.bind(instance); } return value; } });