432 lines
12 KiB
JavaScript
Raw Normal View History

2025-11-09 23:29:07 +01:00
class APIClient {
2025-11-11 01:05:13 +01:00
constructor(baseURL = '/', logger = null, perfMonitor = null, appState = null) {
2025-11-09 23:29:07 +01:00
this.baseURL = baseURL;
this.token = localStorage.getItem('token');
2025-11-11 01:05:13 +01:00
this.logger = logger;
this.perfMonitor = perfMonitor;
this.appState = appState;
this.activeRequests = 0;
2025-11-09 23:29:07 +01:00
}
setToken(token) {
this.token = token;
if (token) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
}
getToken() {
return this.token;
}
async request(endpoint, options = {}) {
2025-11-11 01:05:13 +01:00
this.activeRequests++;
this.appState?.setState({ isLoading: true });
const startTime = performance.now();
2025-11-09 23:29:07 +01:00
const url = `${this.baseURL}${endpoint}`;
2025-11-11 01:05:13 +01:00
const method = options.method || 'GET';
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
try {
this.logger?.debug(`API ${method}: ${endpoint}`);
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
const headers = {
...options.headers,
};
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
if (this.token && !options.skipAuth) {
headers['Authorization'] = `Bearer ${this.token}`;
}
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
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');
}
}
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
const response = await fetch(url, config);
const duration = performance.now() - startTime;
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
if (this.perfMonitor) {
this.perfMonitor._recordMetric('api-request', duration);
this.perfMonitor._recordMetric(`api-${method}`, duration);
}
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
if (response.status === 401) {
this.logger?.warn('Unauthorized request, clearing token');
this.setToken(null);
window.location.href = '/';
}
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
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;
}
2025-11-09 23:29:07 +01:00
2025-11-11 01:05:13 +01:00
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 });
}
}
2025-11-09 23:29:07 +01:00
}
async register(username, email, password) {
2025-11-11 01:05:13 +01:00
this.logger?.info('Attempting registration', { username, email });
2025-11-09 23:29:07 +01:00
const data = await this.request('auth/register', {
method: 'POST',
body: { username, email, password },
skipAuth: true
});
2025-11-11 01:05:13 +01:00
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 });
2025-11-09 23:29:07 +01:00
this.setToken(data.access_token);
return data;
}
async login(username, password) {
2025-11-11 01:05:13 +01:00
this.logger?.info('Attempting login', { username });
2025-11-09 23:29:07 +01:00
const data = await this.request('auth/token', {
method: 'POST',
2025-11-11 01:05:13 +01:00
body: { username, password },
2025-11-09 23:29:07 +01:00
skipAuth: true
});
2025-11-11 01:05:13 +01:00
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 });
2025-11-09 23:29:07 +01:00
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 }
});
}
2025-11-10 15:46:40 +01:00
async getFolderPath(folderId) {
return this.request(`folders/${folderId}/path`);
}
2025-11-09 23:29:07 +01:00
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 }
});
}
2025-11-11 15:06:02 +01:00
async createShare(fileId = null, folderId = null, expiresAt = null, password = null, permissionLevel = 'viewer', inviteEmail = null) {
2025-11-09 23:29:07 +01:00
return this.request('shares/', {
method: 'POST',
body: {
file_id: fileId,
folder_id: folderId,
expires_at: expiresAt,
password,
2025-11-11 15:06:02 +01:00
permission_level: permissionLevel,
invite_email: inviteEmail
2025-11-09 23:29:07 +01:00
}
});
}
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) {
2025-11-09 23:29:07 +01:00
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
});
}
2025-11-10 15:46:40 +01:00
async updateFile(fileId, content) {
return this.request(`files/${fileId}/content`, {
method: 'PUT',
body: { content }
});
}
2025-11-09 23:29:07 +01:00
}
2025-11-11 01:05:13 +01:00
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;
}
});