Files
TaskMate/frontend/js/api.js
Claude Project Manager ab1e5be9a9 Initial commit
2025-12-28 21:36:45 +00:00

863 Zeilen
21 KiB
JavaScript

/**
* TASKMATE - API Client
* =====================
*/
class ApiClient {
constructor() {
this.baseUrl = '/api';
this.token = null;
this.csrfToken = null;
this.refreshingToken = false;
this.requestQueue = [];
}
// Token Management
setToken(token) {
this.token = token;
if (token) {
localStorage.setItem('auth_token', token);
} else {
localStorage.removeItem('auth_token');
}
}
getToken() {
if (!this.token) {
this.token = localStorage.getItem('auth_token');
}
return this.token;
}
setCsrfToken(token) {
this.csrfToken = token;
if (token) {
sessionStorage.setItem('csrf_token', token);
} else {
sessionStorage.removeItem('csrf_token');
}
}
getCsrfToken() {
if (!this.csrfToken) {
this.csrfToken = sessionStorage.getItem('csrf_token');
}
return this.csrfToken;
}
// Base Request Method
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};
// Add auth token
const token = this.getToken();
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);
}
// Handle 401 Unauthorized
if (response.status === 401) {
this.setToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
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) {
const response = await this.post('/auth/login', { username, password });
this.setToken(response.token);
// 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.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, branch = 'main') {
return this.post(`/git/init-push/${projectId}`, { branch });
}
}
// 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;