Files
TaskMate/frontend/js/api.js
2026-01-10 16:47:02 +00:00

1388 Zeilen
37 KiB
JavaScript

/**
* 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
// requestToken: Der Token, der bei der fehlgeschlagenen Anfrage verwendet wurde
handleAuthFailure(requestToken = null) {
// Race-Condition Check: Wenn ein neuer Token existiert, der NACH dem
// fehlgeschlagenen Request gesetzt wurde, nicht ausloggen
const currentToken = localStorage.getItem('auth_token');
if (requestToken && currentToken && requestToken !== currentToken) {
console.log('[API] 401 ignored - new token exists (login occurred after request)');
return false; // Nicht ausloggen
}
console.log('[API] Authentication failed - clearing tokens');
this.setToken(null);
this.setRefreshToken(null);
this.setCsrfToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
return true;
}
// 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) {
const currentTokenNow = localStorage.getItem('auth_token');
console.log('[API] 401 received for:', endpoint);
console.log('[API] Token used in request:', token ? token.substring(0, 20) + '...' : 'NULL');
console.log('[API] Current token in storage:', currentTokenNow ? currentTokenNow.substring(0, 20) + '...' : 'NULL');
console.log('[API] Tokens match:', token === currentTokenNow);
// 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 - aber nur wenn kein neuer Login stattfand
if (this.handleAuthFailure(token)) {
throw new ApiError('Sitzung abgelaufen', 401);
}
// Neuer Token existiert - Request mit neuem Token wiederholen (max 1x)
if (!options._newTokenRetried) {
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true, _newTokenRetried: true });
}
throw new ApiError('Sitzung abgelaufen', 401);
}
}
// Kein Refresh-Token oder Refresh bereits versucht
// Nur ausloggen wenn kein neuer Token existiert
if (this.handleAuthFailure(token)) {
throw new ApiError('Sitzung abgelaufen', 401);
}
// Neuer Token existiert - Request mit neuem Token wiederholen (max 1x)
if (!options._newTokenRetried) {
return this.request(endpoint, { ...options, _newTokenRetried: true });
}
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;
}
// =====================
// REMINDER ENDPOINTS
// =====================
async getReminders(projectId) {
const response = await this.get(`/reminders?project_id=${projectId}`);
return response.data || response; // Extract data property or fallback to response
}
async createReminder(data) {
return this.post('/reminders', data);
}
async updateReminder(reminderId, data) {
return this.put(`/reminders/${reminderId}`, data);
}
async deleteReminder(reminderId) {
return this.delete(`/reminders/${reminderId}`);
}
async getDueReminders() {
return this.get('/reminders/due/check');
}
// =====================
// 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 });
}
async getCodingDirectoryUsage(id) {
return this.get(`/coding/directories/${id}/usage`);
}
async getCodingDirectoryUsageHistory(id, hours = 24) {
return this.get(`/coding/directories/${id}/usage/history?hours=${hours}`);
}
// =============================================================================
// CONTACTS
// =============================================================================
async getContacts(params = {}) {
const queryString = new URLSearchParams(params).toString();
return this.get(`/contacts${queryString ? '?' + queryString : ''}`);
}
async getContact(id) {
return this.get(`/contacts/${id}`);
}
async createContact(data) {
return this.post('/contacts', data);
}
async updateContact(id, data) {
return this.put(`/contacts/${id}`, data);
}
async deleteContact(id) {
return this.delete(`/contacts/${id}`);
}
async getContactTags() {
return this.get('/contacts/tags/all');
}
}
// 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;