1360 Zeilen
36 KiB
JavaScript
1360 Zeilen
36 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
|
|
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;
|
|
}
|
|
|
|
// =====================
|
|
// 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;
|