/** * TASKMATE - API Client * ===================== */ class ApiClient { constructor() { this.baseUrl = '/api'; this.token = null; this.refreshToken = null; this.csrfToken = null; this.refreshingToken = false; this.requestQueue = []; this.refreshTimer = null; this.init(); } init() { // Token aus Storage laden this.token = localStorage.getItem('auth_token'); this.refreshToken = localStorage.getItem('refresh_token'); this.csrfToken = sessionStorage.getItem('csrf_token'); console.log('[API] init() - Token loaded:', this.token ? this.token.substring(0, 20) + '...' : 'NULL'); // Starte Timer wenn Token und Refresh-Token vorhanden sind if (this.token && this.refreshToken) { this.startTokenRefreshTimer(); } } // Token Management setToken(token) { console.log('[API] setToken:', token ? token.substring(0, 20) + '...' : 'NULL'); this.token = token; if (token) { localStorage.setItem('auth_token', token); // Starte proaktiven Token-Refresh Timer (nach 10 Minuten) this.startTokenRefreshTimer(); } else { this.token = null; localStorage.removeItem('auth_token'); localStorage.removeItem('current_user'); this.clearTokenRefreshTimer(); } } setRefreshToken(token) { this.refreshToken = token; if (token) { localStorage.setItem('refresh_token', token); } else { localStorage.removeItem('refresh_token'); } } getToken() { // IMMER aus localStorage lesen um Synchronisationsprobleme zu vermeiden // (z.B. wenn Token nach Login gesetzt wird während andere Requests laufen) const token = localStorage.getItem('auth_token'); this.token = token; // Cache aktualisieren return token; } setCsrfToken(token) { this.csrfToken = token; if (token) { sessionStorage.setItem('csrf_token', token); } else { sessionStorage.removeItem('csrf_token'); } } getCsrfToken() { // IMMER aus sessionStorage lesen um Synchronisationsprobleme zu vermeiden const token = sessionStorage.getItem('csrf_token'); this.csrfToken = token; // Cache aktualisieren return token; } // Refresh Access Token using Refresh Token async refreshAccessToken() { if (this.refreshingToken) { // Warte auf laufenden Refresh return new Promise((resolve) => { const checkRefresh = () => { if (!this.refreshingToken) { resolve(); } else { setTimeout(checkRefresh, 100); } }; checkRefresh(); }); } this.refreshingToken = true; try { const refreshToken = localStorage.getItem('refresh_token'); if (!refreshToken) { throw new Error('Kein Refresh-Token vorhanden'); } console.log('[API] Refreshing access token...'); const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }) }); if (!response.ok) { throw new Error(`Refresh failed: ${response.status}`); } const data = await response.json(); this.setToken(data.token); if (data.csrfToken) { this.setCsrfToken(data.csrfToken); } console.log('[API] Token refresh successful'); window.dispatchEvent(new CustomEvent('auth:token-refreshed', { detail: { token: data.token } })); } catch (error) { console.log('[API] Token refresh error:', error.message); throw error; } finally { this.refreshingToken = false; } } // Handle authentication failure handleAuthFailure() { console.log('[API] Authentication failed - clearing tokens'); this.setToken(null); this.setRefreshToken(null); this.setCsrfToken(null); window.dispatchEvent(new CustomEvent('auth:logout')); } // Proaktiver Token-Refresh Timer startTokenRefreshTimer() { this.clearTokenRefreshTimer(); // Refresh nach 10 Minuten (Token läuft nach 15 Minuten ab) this.refreshTimer = setTimeout(async () => { if (this.refreshToken && !this.refreshingToken) { try { console.log('[API] Proactive token refresh...'); await this.refreshAccessToken(); } catch (error) { console.log('[API] Proactive refresh failed:', error.message); // Bei Fehler nicht automatisch ausloggen, warten bis Token wirklich abläuft } } }, 10 * 60 * 1000); // 10 Minuten } clearTokenRefreshTimer() { if (this.refreshTimer) { clearTimeout(this.refreshTimer); this.refreshTimer = null; } } // Base Request Method async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const headers = { 'Content-Type': 'application/json', ...options.headers }; // Sicherstellen, dass Token aktuell ist if (!this.token && localStorage.getItem('auth_token')) { this.init(); } // Add auth token const token = this.getToken(); console.log('[API] Request:', endpoint, 'Token:', token ? token.substring(0, 20) + '...' : 'NULL'); if (token) { headers['Authorization'] = `Bearer ${token}`; } // Add CSRF token for mutating requests if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) { const csrfToken = this.getCsrfToken(); if (csrfToken) { headers['X-CSRF-Token'] = csrfToken; } } const config = { method: options.method || 'GET', headers, credentials: 'same-origin' }; if (options.body && config.method !== 'GET') { config.body = JSON.stringify(options.body); } try { const response = await fetch(url, config); // Update CSRF token if provided const newCsrfToken = response.headers.get('X-CSRF-Token'); if (newCsrfToken) { this.setCsrfToken(newCsrfToken); } // Update auth token if refreshed by server const newAuthToken = response.headers.get('X-New-Token'); if (newAuthToken) { this.setToken(newAuthToken); window.dispatchEvent(new CustomEvent('auth:token-refreshed', { detail: { token: newAuthToken } })); } // Handle 401 Unauthorized if (response.status === 401) { console.log('[API] 401 received for:', endpoint); // Versuche Token mit Refresh-Token zu erneuern if (this.refreshToken && !this.refreshingToken && !options._tokenRefreshAttempted) { console.log('[API] Attempting token refresh...'); try { await this.refreshAccessToken(); // Wiederhole original Request mit neuem Token return this.request(endpoint, { ...options, _tokenRefreshAttempted: true }); } catch (refreshError) { console.log('[API] Token refresh failed:', refreshError.message); // Fallback zum Logout this.handleAuthFailure(); throw new ApiError('Sitzung abgelaufen', 401); } } // Kein Refresh-Token oder Refresh bereits versucht this.handleAuthFailure(); throw new ApiError('Sitzung abgelaufen', 401); } // Handle CSRF errors - update token and retry once if (response.status === 403) { const errorData = await response.json().catch(() => ({})); if (errorData.code === 'CSRF_ERROR' && errorData.csrfToken) { // Store the new CSRF token this.setCsrfToken(errorData.csrfToken); // Retry the request once with the new token if (!options._csrfRetried) { return this.request(endpoint, { ...options, _csrfRetried: true }); } } throw new ApiError( errorData.error || 'Zugriff verweigert', response.status, errorData ); } // Handle other errors if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new ApiError( errorData.error || `HTTP Error ${response.status}`, response.status, errorData ); } // Parse response const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); } return response; } catch (error) { if (error instanceof ApiError) { throw error; } // Network error if (!navigator.onLine) { throw new ApiError('Keine Internetverbindung', 0, { offline: true }); } throw new ApiError('Netzwerkfehler: ' + error.message, 0); } } // Convenience Methods get(endpoint, options = {}) { return this.request(endpoint, { ...options, method: 'GET' }); } post(endpoint, body, options = {}) { return this.request(endpoint, { ...options, method: 'POST', body }); } put(endpoint, body, options = {}) { return this.request(endpoint, { ...options, method: 'PUT', body }); } patch(endpoint, body, options = {}) { return this.request(endpoint, { ...options, method: 'PATCH', body }); } delete(endpoint, options = {}) { return this.request(endpoint, { ...options, method: 'DELETE' }); } // File Upload async uploadFile(endpoint, file, onProgress, _retried = false) { const url = `${this.baseUrl}${endpoint}`; const formData = new FormData(); formData.append('files', file); const token = this.getToken(); const csrfToken = this.getCsrfToken(); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url); if (token) { xhr.setRequestHeader('Authorization', `Bearer ${token}`); } if (csrfToken) { xhr.setRequestHeader('X-CSRF-Token', csrfToken); } xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable && onProgress) { const percentage = Math.round((e.loaded / e.total) * 100); onProgress(percentage); } }); xhr.addEventListener('load', async () => { if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch { resolve(xhr.responseText); } } else if (xhr.status === 403 && !_retried) { // Handle CSRF error - get new token and retry try { const errorData = JSON.parse(xhr.responseText); if (errorData.code === 'CSRF_ERROR' && errorData.csrfToken) { this.setCsrfToken(errorData.csrfToken); // Retry with new token try { const result = await this.uploadFile(endpoint, file, onProgress, true); resolve(result); } catch (retryError) { reject(retryError); } return; } } catch {} reject(new ApiError('Upload fehlgeschlagen', xhr.status)); } else { try { const error = JSON.parse(xhr.responseText); reject(new ApiError(error.error || 'Upload fehlgeschlagen', xhr.status)); } catch { reject(new ApiError('Upload fehlgeschlagen', xhr.status)); } } }); xhr.addEventListener('error', () => { reject(new ApiError('Netzwerkfehler beim Upload', 0)); }); xhr.send(formData); }); } // Download File async downloadFile(endpoint, filename) { const url = `${this.baseUrl}${endpoint}`; const token = this.getToken(); const response = await fetch(url, { headers: token ? { 'Authorization': `Bearer ${token}` } : {} }); if (!response.ok) { throw new ApiError('Download fehlgeschlagen', response.status); } const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(downloadUrl); } // ===================== // AUTH ENDPOINTS // ===================== async login(username, password) { console.log('[API] login() called'); const response = await this.post('/auth/login', { username, password }); console.log('[API] login() response:', response ? 'OK' : 'NULL', 'token:', response?.token ? 'EXISTS' : 'MISSING'); this.setToken(response.token); // Store refresh token if provided (new auth system) if (response.refreshToken) { this.setRefreshToken(response.refreshToken); } // Store CSRF token from login response if (response.csrfToken) { this.setCsrfToken(response.csrfToken); } return response; } async logout() { try { await this.post('/auth/logout', {}); } finally { this.setToken(null); this.setRefreshToken(null); this.setCsrfToken(null); } } async changePassword(currentPassword, newPassword) { return this.post('/auth/password', { currentPassword, newPassword }); } async updateUserColor(color) { return this.put('/auth/color', { color }); } async getUsers() { return this.get('/auth/users'); } // ===================== // PROJECT ENDPOINTS // ===================== async getProjects() { return this.get('/projects'); } async getProject(id) { return this.get(`/projects/${id}`); } async createProject(data) { return this.post('/projects', data); } async updateProject(id, data) { return this.put(`/projects/${id}`, data); } async deleteProject(id, force = true) { return this.delete(`/projects/${id}?force=${force}`); } // ===================== // COLUMN ENDPOINTS // ===================== async getColumns(projectId) { return this.get(`/columns/${projectId}`); } async createColumn(projectId, data) { return this.post('/columns', { ...data, projectId }); } async updateColumn(projectId, columnId, data) { return this.put(`/columns/${columnId}`, data); } async deleteColumn(projectId, columnId) { return this.delete(`/columns/${columnId}`); } async reorderColumns(projectId, columnId, newPosition) { return this.put(`/columns/${columnId}/position`, { newPosition }); } // ===================== // TASK ENDPOINTS // ===================== async getTasks(projectId, params = {}) { const queryString = new URLSearchParams(params).toString(); const endpoint = `/tasks/project/${projectId}${queryString ? '?' + queryString : ''}`; return this.get(endpoint); } async getTask(projectId, taskId) { return this.get(`/tasks/${taskId}`); } async createTask(projectId, data) { return this.post('/tasks', { ...data, projectId }); } async updateTask(projectId, taskId, data) { return this.put(`/tasks/${taskId}`, data); } async deleteTask(projectId, taskId) { return this.delete(`/tasks/${taskId}`); } async moveTask(projectId, taskId, columnId, position) { return this.put(`/tasks/${taskId}/move`, { columnId, position }); } async duplicateTask(projectId, taskId) { return this.post(`/tasks/${taskId}/duplicate`, {}); } async archiveTask(projectId, taskId) { return this.put(`/tasks/${taskId}/archive`, { archived: true }); } async restoreTask(projectId, taskId) { return this.put(`/tasks/${taskId}/archive`, { archived: false }); } async getTaskHistory(projectId, taskId) { return this.get(`/tasks/${taskId}/history`); } async searchTasks(projectId, query) { return this.get(`/tasks/search?projectId=${projectId}&q=${encodeURIComponent(query)}`); } // ===================== // SUBTASK ENDPOINTS // ===================== async getSubtasks(projectId, taskId) { return this.get(`/subtasks/${taskId}`); } async createSubtask(projectId, taskId, data) { return this.post('/subtasks', { ...data, taskId }); } async updateSubtask(projectId, taskId, subtaskId, data) { return this.put(`/subtasks/${subtaskId}`, data); } async deleteSubtask(projectId, taskId, subtaskId) { return this.delete(`/subtasks/${subtaskId}`); } async reorderSubtasks(projectId, taskId, subtaskId, newPosition) { return this.put(`/subtasks/${subtaskId}/position`, { newPosition }); } // ===================== // COMMENT ENDPOINTS // ===================== async getComments(projectId, taskId) { return this.get(`/comments/${taskId}`); } async createComment(projectId, taskId, data) { return this.post('/comments', { ...data, taskId }); } async updateComment(projectId, taskId, commentId, data) { return this.put(`/comments/${commentId}`, data); } async deleteComment(projectId, taskId, commentId) { return this.delete(`/comments/${commentId}`); } // ===================== // LABEL ENDPOINTS // ===================== async getLabels(projectId) { return this.get(`/labels/${projectId}`); } async createLabel(projectId, data) { return this.post('/labels', { ...data, projectId }); } async updateLabel(projectId, labelId, data) { return this.put(`/labels/${labelId}`, data); } async deleteLabel(projectId, labelId) { return this.delete(`/labels/${labelId}`); } // ===================== // FILE ENDPOINTS // ===================== async getFiles(projectId, taskId) { return this.get(`/files/${taskId}`); } async uploadTaskFile(projectId, taskId, file, onProgress) { return this.uploadFile(`/files/${taskId}`, file, onProgress); } async downloadTaskFile(projectId, taskId, fileId, filename) { return this.downloadFile(`/files/download/${fileId}`, filename); } async deleteFile(projectId, taskId, fileId) { return this.delete(`/files/${fileId}`); } getFilePreviewUrl(projectId, taskId, fileId) { const token = this.getToken(); const url = `${this.baseUrl}/files/preview/${fileId}`; // Add token as query parameter for img src authentication return token ? `${url}?token=${encodeURIComponent(token)}` : url; } // ===================== // LINK ENDPOINTS // ===================== async getLinks(projectId, taskId) { return this.get(`/links/${taskId}`); } async createLink(projectId, taskId, data) { return this.post('/links', { ...data, taskId }); } async updateLink(projectId, taskId, linkId, data) { return this.put(`/links/${linkId}`, data); } async deleteLink(projectId, taskId, linkId) { return this.delete(`/links/${linkId}`); } // ===================== // TEMPLATE ENDPOINTS // ===================== async getTemplates() { return this.get('/templates'); } async getTemplate(id) { return this.get(`/templates/${id}`); } async createTemplate(data) { return this.post('/templates', data); } async updateTemplate(id, data) { return this.put(`/templates/${id}`, data); } async deleteTemplate(id) { return this.delete(`/templates/${id}`); } async createTaskFromTemplate(projectId, templateId, columnId) { return this.post(`/templates/${templateId}/apply`, { projectId, columnId }); } // ===================== // STATS ENDPOINTS // ===================== async getStats(projectId) { const endpoint = projectId ? `/stats/dashboard?projectId=${projectId}` : '/stats/dashboard'; return this.get(endpoint); } async getCompletionStats(projectId, weeks = 8) { const endpoint = projectId ? `/stats/completed-per-week?projectId=${projectId}&weeks=${weeks}` : `/stats/completed-per-week?weeks=${weeks}`; return this.get(endpoint); } async getTimeStats(projectId) { return this.get('/stats/time-per-project'); } async getOverdueTasks(projectId) { // Placeholder - overdue data comes from dashboard stats return []; } async getDueTodayTasks(projectId) { // Placeholder - due today data comes from dashboard stats return []; } // ===================== // EXPORT/IMPORT ENDPOINTS // ===================== async exportProject(projectId, format = 'json') { const response = await this.get(`/export/project/${projectId}?format=${format}`); if (format === 'csv') { // For CSV, trigger download const blob = new Blob([response], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `project_${projectId}_export.csv`; a.click(); window.URL.revokeObjectURL(url); } return response; } async exportAll(format = 'json') { return this.get(`/export/all?format=${format}`); } async importProject(data) { return this.post('/import', data); } // ===================== // HEALTH ENDPOINTS // ===================== async healthCheck() { return this.get('/health'); } async getSystemInfo() { return this.get('/health/info'); } // ===================== // ADMIN ENDPOINTS // ===================== async getAdminUsers() { return this.get('/admin/users'); } async createAdminUser(data) { return this.post('/admin/users', data); } async updateAdminUser(userId, data) { return this.put(`/admin/users/${userId}`, data); } async deleteAdminUser(userId) { return this.delete(`/admin/users/${userId}`); } async getUploadSettings() { return this.get('/admin/upload-settings'); } async updateUploadSettings(settings) { return this.put('/admin/upload-settings', settings); } // ===================== // PROPOSALS (GENEHMIGUNGEN) ENDPOINTS // ===================== async getProposals(sort = 'date', archived = false, projectId = null) { let url = `/proposals?sort=${sort}&archived=${archived ? '1' : '0'}`; if (projectId) { url += `&projectId=${projectId}`; } return this.get(url); } async createProposal(data) { return this.post('/proposals', data); } async approveProposal(proposalId, approved) { return this.put(`/proposals/${proposalId}/approve`, { approved }); } async archiveProposal(proposalId, archived) { return this.put(`/proposals/${proposalId}/archive`, { archived }); } async deleteProposal(proposalId) { return this.delete(`/proposals/${proposalId}`); } // ===================== // TASKS FOR PROPOSALS // ===================== async getAllTasks() { return this.get('/tasks/all'); } // ===================== // NOTIFICATION ENDPOINTS // ===================== async getNotifications() { return this.get('/notifications'); } async getNotificationCount() { return this.get('/notifications/count'); } async markNotificationRead(id) { return this.put(`/notifications/${id}/read`, {}); } async markAllNotificationsRead() { return this.put('/notifications/read-all', {}); } async deleteNotification(id) { return this.delete(`/notifications/${id}`); } // ===================== // GITEA ENDPOINTS // ===================== async testGiteaConnection() { return this.get('/gitea/test'); } async getGiteaRepositories(page = 1, limit = 50) { return this.get(`/gitea/repositories?page=${page}&limit=${limit}`); } async createGiteaRepository(data) { return this.post('/gitea/repositories', data); } async getGiteaRepository(owner, repo) { return this.get(`/gitea/repositories/${owner}/${repo}`); } async deleteGiteaRepository(owner, repo) { return this.delete(`/gitea/repositories/${owner}/${repo}`); } async getGiteaBranches(owner, repo) { return this.get(`/gitea/repositories/${owner}/${repo}/branches`); } async getGiteaCommits(owner, repo, options = {}) { const params = new URLSearchParams(); if (options.page) params.append('page', options.page); if (options.limit) params.append('limit', options.limit); if (options.branch) params.append('branch', options.branch); const queryString = params.toString(); return this.get(`/gitea/repositories/${owner}/${repo}/commits${queryString ? '?' + queryString : ''}`); } async getGiteaUser() { return this.get('/gitea/user'); } // ===================== // APPLICATIONS (Projekt-Repository-Verknüpfung) ENDPOINTS // ===================== async getProjectApplication(projectId) { return this.get(`/applications/${projectId}`); } async saveProjectApplication(data) { return this.post('/applications', data); } async deleteProjectApplication(projectId) { return this.delete(`/applications/${projectId}`); } async getUserBasePath() { return this.get('/applications/user/base-path'); } async setUserBasePath(basePath) { return this.put('/applications/user/base-path', { basePath }); } async syncProjectApplication(projectId) { return this.post(`/applications/${projectId}/sync`, {}); } // ===================== // GIT OPERATIONS ENDPOINTS // ===================== async cloneRepository(data) { return this.post('/git/clone', data); } async getGitStatus(projectId) { return this.get(`/git/status/${projectId}`); } async gitPull(projectId, branch = null) { return this.post(`/git/pull/${projectId}`, { branch }); } async gitPush(projectId, branch = null) { return this.post(`/git/push/${projectId}`, { branch }); } async gitCommit(projectId, message, stageAll = true) { return this.post(`/git/commit/${projectId}`, { message, stageAll }); } async getGitCommits(projectId, limit = 20) { return this.get(`/git/commits/${projectId}?limit=${limit}`); } async getGitBranches(projectId) { return this.get(`/git/branches/${projectId}`); } async gitCheckout(projectId, branch) { return this.post(`/git/checkout/${projectId}`, { branch }); } async gitFetch(projectId) { return this.post(`/git/fetch/${projectId}`, {}); } async gitStage(projectId) { return this.post(`/git/stage/${projectId}`, {}); } async getGitRemote(projectId) { return this.get(`/git/remote/${projectId}`); } async validatePath(path) { return this.post('/git/validate-path', { path }); } async prepareRepository(projectId, repoUrl, branch = 'main') { return this.post(`/git/prepare/${projectId}`, { repoUrl, branch }); } async setGitRemote(projectId, repoUrl) { return this.post(`/git/set-remote/${projectId}`, { repoUrl }); } async gitInitPush(projectId, targetBranch = null, force = false) { return this.post(`/git/init-push/${projectId}`, { targetBranch, force }); } async gitRenameBranch(projectId, oldName, newName) { return this.post(`/git/rename-branch/${projectId}`, { oldName, newName }); } // ===================== // SERVER GIT ENDPOINTS (Server-Modus) // ===================== async getServerGitInfo() { return this.get('/git/server/info'); } async getServerGitStatus() { return this.get('/git/server/status'); } async getServerGitBranches() { return this.get('/git/server/branches'); } async getServerGitCommits(limit = 20) { return this.get(`/git/server/commits?limit=${limit}`); } async getServerGitRemote() { return this.get('/git/server/remote'); } async serverGitStage() { return this.post('/git/server/stage', {}); } async serverGitCommit(message, stageAll = true) { return this.post('/git/server/commit', { message, stageAll }); } async serverGitPush(branch = null, force = false) { return this.post('/git/server/push', { branch, force }); } async serverGitPull(branch = null) { return this.post('/git/server/pull', { branch }); } async serverGitFetch() { return this.post('/git/server/fetch', {}); } async serverGitCheckout(branch) { return this.post('/git/server/checkout', { branch }); } // ===================== // BROWSER-UPLOAD ENDPOINTS // ===================== async prepareBrowserUpload() { return this.post('/git/browser-upload-prepare', {}); } async cancelBrowserUpload(sessionId) { return this.delete(`/git/browser-upload/${sessionId}`); } /** * Lädt Dateien vom Browser hoch und pusht sie ins Gitea * @param {Object} options - Upload-Optionen * @param {File[]} options.files - Array von File-Objekten mit relativePath Property * @param {string} options.repoUrl - Gitea Repository URL * @param {string} options.branch - Ziel-Branch * @param {string} options.commitMessage - Commit-Nachricht * @param {string} options.sessionId - Session-ID vom prepare-Aufruf * @param {Function} options.onProgress - Progress-Callback (optional) */ async browserUploadAndPush(options) { const { files, repoUrl, branch, commitMessage, sessionId, onProgress } = options; const url = `${this.baseUrl}/git/browser-upload`; const formData = new FormData(); // Metadaten hinzufügen formData.append('repoUrl', repoUrl); formData.append('branch', branch || 'main'); formData.append('commitMessage', commitMessage); formData.append('sessionId', sessionId); // Dateien hinzufügen (mit relativem Pfad als Dateiname) files.forEach(fileInfo => { // Erstelle neues File-Objekt mit relativem Pfad als Namen const file = new File([fileInfo.file], fileInfo.relativePath, { type: fileInfo.file.type }); formData.append('files', file); }); const token = this.getToken(); const csrfToken = this.getCsrfToken(); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url); if (token) { xhr.setRequestHeader('Authorization', `Bearer ${token}`); } if (csrfToken) { xhr.setRequestHeader('X-CSRF-Token', csrfToken); } if (onProgress) { xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentage = Math.round((e.loaded / e.total) * 100); onProgress(percentage); } }); } xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch { resolve({ success: true }); } } else { try { const error = JSON.parse(xhr.responseText); reject(new Error(error.error || 'Upload fehlgeschlagen')); } catch { reject(new Error('Upload fehlgeschlagen')); } } }); xhr.addEventListener('error', () => { reject(new Error('Netzwerkfehler beim Upload')); }); xhr.send(formData); }); } // ===================== // KNOWLEDGE ENDPOINTS (Wissensmanagement) // ===================== // Kategorien async getKnowledgeCategories() { return this.get('/knowledge/categories'); } async createKnowledgeCategory(data) { return this.post('/knowledge/categories', data); } async updateKnowledgeCategory(id, data) { return this.put(`/knowledge/categories/${id}`, data); } async deleteKnowledgeCategory(id) { return this.delete(`/knowledge/categories/${id}`); } async updateKnowledgeCategoryPosition(id, newPosition) { return this.put(`/knowledge/categories/${id}/position`, { newPosition }); } // Einträge async getKnowledgeEntries(categoryId = null) { const params = categoryId ? `?categoryId=${categoryId}` : ''; return this.get(`/knowledge/entries${params}`); } async getKnowledgeEntry(id) { return this.get(`/knowledge/entries/${id}`); } async createKnowledgeEntry(data) { return this.post('/knowledge/entries', data); } async updateKnowledgeEntry(id, data) { return this.put(`/knowledge/entries/${id}`, data); } async deleteKnowledgeEntry(id) { return this.delete(`/knowledge/entries/${id}`); } async updateKnowledgeEntryPosition(id, newPosition, newCategoryId = null) { return this.put(`/knowledge/entries/${id}/position`, { newPosition, newCategoryId }); } // Anhänge async getKnowledgeAttachments(entryId) { return this.get(`/knowledge/attachments/${entryId}`); } async uploadKnowledgeAttachment(entryId, file, onProgress) { return this.uploadFile(`/knowledge/attachments/${entryId}`, file, onProgress); } async deleteKnowledgeAttachment(id) { return this.delete(`/knowledge/attachments/${id}`); } getKnowledgeAttachmentDownloadUrl(id) { return `${this.baseUrl}/knowledge/attachments/download/${id}`; } // Suche async searchKnowledge(query) { return this.get(`/knowledge/search?q=${encodeURIComponent(query)}`); } // ===================== // CODING // ===================== async getCodingDirectories() { return this.get('/coding/directories'); } async createCodingDirectory(data) { return this.post('/coding/directories', data); } async updateCodingDirectory(id, data) { return this.put(`/coding/directories/${id}`, data); } async deleteCodingDirectory(id) { return this.delete(`/coding/directories/${id}`); } async getCodingDirectoryStatus(id) { return this.get(`/coding/directories/${id}/status`); } async codingGitFetch(id) { return this.post(`/coding/directories/${id}/fetch`); } async codingGitPull(id) { return this.post(`/coding/directories/${id}/pull`); } async codingGitPush(id, force = false) { return this.post(`/coding/directories/${id}/push`, { force }); } async codingGitCommit(id, message) { return this.post(`/coding/directories/${id}/commit`, { message }); } async getCodingDirectoryBranches(id) { return this.get(`/coding/directories/${id}/branches`); } async codingGitCheckout(id, branch) { return this.post(`/coding/directories/${id}/checkout`, { branch }); } async getCodingDirectoryCommits(id, limit = 20) { return this.get(`/coding/directories/${id}/commits?limit=${limit}`); } async validateCodingPath(path) { return this.post('/coding/validate-path', { path }); } } // Custom API Error Class class ApiError extends Error { constructor(message, status, data = {}) { super(message); this.name = 'ApiError'; this.status = status; this.data = data; } get isOffline() { return this.data.offline === true; } get isUnauthorized() { return this.status === 401; } get isNotFound() { return this.status === 404; } get isValidationError() { return this.status === 400; } get isServerError() { return this.status >= 500; } } // Create singleton instance const api = new ApiClient(); export { api, ApiError }; export default api;