const API_BASE = process.env.NEXT_PUBLIC_API_URL || ''; import * as tus from 'tus-js-client'; interface RequestOptions { method?: string; body?: any; headers?: Record; } class ApiClient { private accessToken: string | null = null; private refreshToken: string | null = null; constructor() { if (typeof window !== 'undefined') { this.accessToken = localStorage.getItem('access_token'); this.refreshToken = localStorage.getItem('refresh_token'); } } setTokens(accessToken: string, refreshToken: string) { this.accessToken = accessToken; this.refreshToken = refreshToken; localStorage.setItem('access_token', accessToken); localStorage.setItem('refresh_token', refreshToken); } clearTokens() { this.accessToken = null; this.refreshToken = null; localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); } getAccessToken() { return this.accessToken; } isAuthenticated() { return !!this.accessToken; } private async request(endpoint: string, options: RequestOptions = {}) { const headers: Record = { ...options.headers, }; if (this.accessToken) { headers['Authorization'] = `Bearer ${this.accessToken}`; } if (options.body && !(options.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; } const response = await fetch(`${API_BASE}${endpoint}`, { method: options.method || 'GET', headers, body: options.body instanceof FormData ? options.body : options.body ? JSON.stringify(options.body) : undefined, }); // Try to refresh token on 401 if (response.status === 401 && this.refreshToken && endpoint !== '/api/auth/refresh') { const refreshed = await this.refreshAccessToken(); if (refreshed) { headers['Authorization'] = `Bearer ${this.accessToken}`; return fetch(`${API_BASE}${endpoint}`, { method: options.method || 'GET', headers, body: options.body instanceof FormData ? options.body : options.body ? JSON.stringify(options.body) : undefined, }); } } return response; } private async refreshAccessToken(): Promise { try { const response = await fetch(`${API_BASE}/api/auth/refresh`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.refreshToken}`, }, }); if (response.ok) { const data = await response.json(); this.accessToken = data.access_token; localStorage.setItem('access_token', data.access_token); return true; } } catch { // Refresh failed } this.clearTokens(); return false; } // --- Setup --- async checkSetup() { const res = await this.request('/api/setup/status'); return res.json(); } async setup(username: string, password: string) { const res = await this.request('/api/setup', { method: 'POST', body: { username, password }, }); return res.json(); } // --- Auth --- async login(username: string, password: string, totpCode?: string) { const res = await this.request('/api/auth/login', { method: 'POST', body: { username, password, totp_code: totpCode }, }); const data = await res.json(); if (res.ok && data.access_token) { this.setTokens(data.access_token, data.refresh_token); } return { ok: res.ok, status: res.status, data }; } logout() { this.clearTokens(); } async changePassword(oldPassword: string, newPassword: string) { const res = await this.request('/api/auth/password', { method: 'PUT', body: { old_password: oldPassword, new_password: newPassword }, }); return res.json(); } // --- Files --- async listDirectory(path: string = '.') { const res = await this.request(`/api/files/?path=${encodeURIComponent(path)}`); return res.json(); } async getExtendedFileInfo(path: string) { const res = await this.request(`/api/files/info/?path=${encodeURIComponent(path)}`); return res.json(); } async createFolder(path: string) { const res = await this.request('/api/files/mkdir', { method: 'POST', body: { path }, }); return res.json(); } async rename(oldPath: string, newName: string) { const res = await this.request('/api/files/rename', { method: 'POST', body: { old_path: oldPath, new_name: newName }, }); return res.json(); } async move(sourcePath: string, destPath: string) { const res = await this.request('/api/files/move', { method: 'POST', body: { source_path: sourcePath, dest_path: destPath }, }); return res.json(); } async copy(sourcePath: string, destPath: string) { const res = await this.request('/api/files/copy', { method: 'POST', body: { source_path: sourcePath, dest_path: destPath }, }); return res.json(); } async deleteFile(path: string) { const res = await this.request(`/api/files/?path=${encodeURIComponent(path)}`, { method: 'DELETE', }); return res.json(); } upload(file: File, destPath: string, callbacks?: { onProgress?: (percent: number) => void }): Promise { return new Promise((resolve, reject) => { const upload = new tus.Upload(file, { endpoint: `${API_BASE}/api/tus/`, chunkSize: 5 * 1024 * 1024, // 5MB chunks bypass Next.js 10MB proxy limit retryDelays: [0, 3000, 5000, 10000, 20000], headers: this.getStreamHeaders(), metadata: { filename: file.name, filetype: file.type, path: destPath, }, onError: function (error) { reject(error); }, onProgress: function (bytesUploaded, bytesTotal) { const percentage = (bytesUploaded / bytesTotal) * 100; callbacks?.onProgress?.(percentage); }, onSuccess: function () { resolve(); }, }); upload.start(); }); } getDownloadUrl(path: string) { const url = `${API_BASE}/api/files/download/?path=${encodeURIComponent(path)}`; return this.accessToken ? `${url}&token=${this.accessToken}` : url; } getDownloadFolderUrl(path: string) { const url = `${API_BASE}/api/files/download-folder?path=${encodeURIComponent(path)}`; return this.accessToken ? `${url}&token=${this.accessToken}` : url; } getThumbnailUrl(checksum: string) { const url = `${API_BASE}/api/files/thumbnail/${encodeURIComponent(checksum)}`; return this.accessToken ? `${url}?token=${this.accessToken}` : url; } getStreamHeaders(): Record { return this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {}; } // --- Search --- async search(query: string) { const res = await this.request(`/api/search?q=${encodeURIComponent(query)}`); return res.json(); } // --- Pinned --- async listPinned() { const res = await this.request('/api/files/pinned'); return res.json(); } async togglePin(path: string) { const res = await this.request('/api/files/pin', { method: 'POST', body: { path }, }); return res.json(); } async getTasks() { const res = await this.request('/api/tasks'); return res.json(); } async getSettings() { const res = await this.request('/api/settings'); return res.json(); } async updateSettings(settings: any) { const res = await this.request('/api/settings', { method: 'PUT', body: settings, }); return res.json(); } async getStorageDashboard() { const res = await this.request('/api/storage/dashboard'); return res.json(); } // --- Trash --- async listTrash() { const res = await this.request('/api/trash'); return res.json(); } async restoreFromTrash(path: string) { const res = await this.request('/api/trash/restore', { method: 'POST', body: { path }, }); return res.json(); } async emptyTrash() { const res = await this.request('/api/trash', { method: 'DELETE' }); return res.json(); } // --- Audit --- async getAuditLog(limit = 100, offset = 0) { const res = await this.request(`/api/audit?limit=${limit}&offset=${offset}`); return res.json(); } // --- 2FA --- async enable2FA() { const res = await this.request('/api/auth/2fa/enable', { method: 'POST' }); return res.json(); } async confirm2FA(code: string) { const res = await this.request('/api/auth/2fa/confirm', { method: 'POST', body: { code }, }); return res.json(); } async disable2FA() { const res = await this.request('/api/auth/2fa/disable', { method: 'POST' }); return res.json(); } // --- Sharing --- async createShare(path: string, password?: string, expiresIn?: number, maxDownloads?: number) { const res = await this.request('/api/share', { method: 'POST', body: { path, password, expires_in: expiresIn, max_downloads: maxDownloads }, }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed to create share'); return data; } async listShares() { const res = await this.request('/api/share'); return res.json(); } async revokeShare(id: string) { const res = await this.request(`/api/share/${id}`, { method: 'DELETE' }); return res.json(); } async getPublicShare(id: string, password?: string, path?: string) { // Note: No auth headers here, so we use fetch directly to avoid interceptors let url = `${API_BASE}/api/public/share/${id}`; if (path) { url += `?path=${encodeURIComponent(path)}`; } const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw err; } return res.json(); } getPublicDownloadUrl(id: string, password?: string, path?: string) { let url = `${API_BASE}/api/public/download/${id}`; const params = new URLSearchParams(); if (password) params.set('pwd', password); if (path) params.set('path', path); const qs = params.toString(); if (qs) { url += `?${qs}`; } return url; } } export const api = new ApiClient(); export default api;