Initial commit
Dieser Commit ist enthalten in:
409
backend/routes/admin.js
Normale Datei
409
backend/routes/admin.js
Normale Datei
@ -0,0 +1,409 @@
|
||||
/**
|
||||
* TASKMATE - Admin Routes
|
||||
* =======================
|
||||
* API-Endpunkte für Benutzerverwaltung
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Standard-Upload-Einstellungen
|
||||
*/
|
||||
const DEFAULT_UPLOAD_SETTINGS = {
|
||||
maxFileSizeMB: 15,
|
||||
allowedTypes: {
|
||||
images: {
|
||||
enabled: true,
|
||||
types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
|
||||
},
|
||||
documents: {
|
||||
enabled: true,
|
||||
types: ['application/pdf']
|
||||
},
|
||||
office: {
|
||||
enabled: true,
|
||||
types: [
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
]
|
||||
},
|
||||
text: {
|
||||
enabled: true,
|
||||
types: ['text/plain', 'text/csv', 'text/markdown']
|
||||
},
|
||||
archives: {
|
||||
enabled: true,
|
||||
types: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed']
|
||||
},
|
||||
data: {
|
||||
enabled: true,
|
||||
types: ['application/json']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Alle Admin-Routes erfordern Authentifizierung und Admin-Rolle
|
||||
router.use(authenticateToken);
|
||||
router.use(requireAdmin);
|
||||
|
||||
/**
|
||||
* GET /api/admin/users - Alle Benutzer abrufen
|
||||
*/
|
||||
router.get('/users', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const users = db.prepare(`
|
||||
SELECT id, username, display_name, color, role, permissions, email,
|
||||
created_at, last_login, failed_attempts, locked_until
|
||||
FROM users
|
||||
ORDER BY id
|
||||
`).all();
|
||||
|
||||
// Permissions parsen
|
||||
const parsedUsers = users.map(user => ({
|
||||
...user,
|
||||
permissions: JSON.parse(user.permissions || '[]')
|
||||
}));
|
||||
|
||||
res.json(parsedUsers);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Benutzer:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Abrufen der Benutzer' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/admin/users - Neuen Benutzer anlegen
|
||||
*/
|
||||
router.post('/users', async (req, res) => {
|
||||
try {
|
||||
const { username, password, displayName, email, role, permissions } = req.body;
|
||||
|
||||
// Validierung
|
||||
if (!username || !password || !displayName || !email) {
|
||||
return res.status(400).json({ error: 'Kürzel, Passwort, Anzeigename und E-Mail erforderlich' });
|
||||
}
|
||||
|
||||
// E-Mail-Validierung
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return res.status(400).json({ error: 'Ungültige E-Mail-Adresse' });
|
||||
}
|
||||
|
||||
// Kürzel muss genau 2 Buchstaben sein
|
||||
const usernameUpper = username.toUpperCase();
|
||||
if (!/^[A-Z]{2}$/.test(usernameUpper)) {
|
||||
return res.status(400).json({ error: 'Kürzel muss genau 2 Buchstaben sein (z.B. HG)' });
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen haben' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Prüfen ob Kürzel bereits existiert
|
||||
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(usernameUpper);
|
||||
if (existing) {
|
||||
return res.status(400).json({ error: 'Kürzel bereits vergeben' });
|
||||
}
|
||||
|
||||
// Prüfen ob E-Mail bereits existiert
|
||||
const existingEmail = db.prepare('SELECT id FROM users WHERE email = ?').get(email.toLowerCase());
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({ error: 'E-Mail bereits vergeben' });
|
||||
}
|
||||
|
||||
// Passwort hashen
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
|
||||
// Standardfarbe Grau
|
||||
const defaultColor = '#808080';
|
||||
|
||||
// Benutzer erstellen
|
||||
const result = db.prepare(`
|
||||
INSERT INTO users (username, password_hash, display_name, color, role, permissions, email)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
usernameUpper,
|
||||
passwordHash,
|
||||
displayName,
|
||||
defaultColor,
|
||||
role || 'user',
|
||||
JSON.stringify(permissions || []),
|
||||
email.toLowerCase()
|
||||
);
|
||||
|
||||
logger.info(`Admin ${req.user.username} hat Benutzer ${usernameUpper} erstellt`);
|
||||
|
||||
res.status(201).json({
|
||||
id: result.lastInsertRowid,
|
||||
username: usernameUpper,
|
||||
displayName,
|
||||
email: email.toLowerCase(),
|
||||
color: defaultColor,
|
||||
role: role || 'user',
|
||||
permissions: permissions || []
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Benutzers:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Erstellen des Benutzers' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/admin/users/:id - Benutzer bearbeiten
|
||||
*/
|
||||
router.put('/users/:id', async (req, res) => {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
const { displayName, color, role, permissions, password, unlockAccount, email } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Benutzer prüfen
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||||
}
|
||||
|
||||
// Verhindern, dass der einzige Admin seine Rolle ändert
|
||||
if (user.role === 'admin' && role !== 'admin') {
|
||||
const adminCount = db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get();
|
||||
if (adminCount.count <= 1) {
|
||||
return res.status(400).json({ error: 'Mindestens ein Admin muss existieren' });
|
||||
}
|
||||
}
|
||||
|
||||
// Update-Felder sammeln
|
||||
const updates = [];
|
||||
const params = [];
|
||||
|
||||
if (displayName !== undefined) {
|
||||
updates.push('display_name = ?');
|
||||
params.push(displayName);
|
||||
}
|
||||
|
||||
if (color !== undefined) {
|
||||
updates.push('color = ?');
|
||||
params.push(color);
|
||||
}
|
||||
|
||||
if (role !== undefined) {
|
||||
updates.push('role = ?');
|
||||
params.push(role);
|
||||
}
|
||||
|
||||
if (permissions !== undefined) {
|
||||
updates.push('permissions = ?');
|
||||
params.push(JSON.stringify(permissions));
|
||||
}
|
||||
|
||||
if (password) {
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({ error: 'Passwort muss mindestens 8 Zeichen haben' });
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
updates.push('password_hash = ?');
|
||||
params.push(passwordHash);
|
||||
}
|
||||
|
||||
if (unlockAccount) {
|
||||
updates.push('failed_attempts = 0');
|
||||
updates.push('locked_until = NULL');
|
||||
}
|
||||
|
||||
if (email !== undefined) {
|
||||
// E-Mail-Validierung
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return res.status(400).json({ error: 'Ungültige E-Mail-Adresse' });
|
||||
}
|
||||
// Prüfen ob E-Mail bereits von anderem Benutzer verwendet wird
|
||||
const existingEmail = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?').get(email.toLowerCase(), userId);
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({ error: 'E-Mail bereits vergeben' });
|
||||
}
|
||||
updates.push('email = ?');
|
||||
params.push(email.toLowerCase());
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return res.status(400).json({ error: 'Keine Änderungen angegeben' });
|
||||
}
|
||||
|
||||
params.push(userId);
|
||||
db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
||||
|
||||
logger.info(`Admin ${req.user.username} hat Benutzer ${user.username} bearbeitet`);
|
||||
|
||||
// Aktualisierten Benutzer zurueckgeben
|
||||
const updatedUser = db.prepare(`
|
||||
SELECT id, username, display_name, color, role, permissions, email,
|
||||
created_at, last_login, failed_attempts, locked_until
|
||||
FROM users WHERE id = ?
|
||||
`).get(userId);
|
||||
|
||||
res.json({
|
||||
...updatedUser,
|
||||
permissions: JSON.parse(updatedUser.permissions || '[]')
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Bearbeiten des Benutzers:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Bearbeiten des Benutzers' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/users/:id - Benutzer löschen
|
||||
*/
|
||||
router.delete('/users/:id', (req, res) => {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
const db = getDb();
|
||||
|
||||
// Benutzer prüfen
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||||
}
|
||||
|
||||
// Verhindern, dass der letzte Admin gelöscht wird
|
||||
if (user.role === 'admin') {
|
||||
const adminCount = db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin'").get();
|
||||
if (adminCount.count <= 1) {
|
||||
return res.status(400).json({ error: 'Der letzte Admin kann nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
|
||||
// Verhindern, dass man sich selbst löscht
|
||||
if (userId === req.user.id) {
|
||||
return res.status(400).json({ error: 'Sie können sich nicht selbst löschen' });
|
||||
}
|
||||
|
||||
// Alle Referenzen auf den Benutzer auf NULL setzen oder löschen
|
||||
// Tasks
|
||||
db.prepare('UPDATE tasks SET assigned_to = NULL WHERE assigned_to = ?').run(userId);
|
||||
db.prepare('UPDATE tasks SET created_by = NULL WHERE created_by = ?').run(userId);
|
||||
|
||||
// Kommentare
|
||||
db.prepare('UPDATE comments SET user_id = NULL WHERE user_id = ?').run(userId);
|
||||
|
||||
// Historie
|
||||
db.prepare('UPDATE history SET user_id = NULL WHERE user_id = ?').run(userId);
|
||||
|
||||
// Vorschläge
|
||||
db.prepare('UPDATE proposals SET created_by = NULL WHERE created_by = ?').run(userId);
|
||||
db.prepare('UPDATE proposals SET approved_by = NULL WHERE approved_by = ?').run(userId);
|
||||
|
||||
// Projekte
|
||||
db.prepare('UPDATE projects SET created_by = NULL WHERE created_by = ?').run(userId);
|
||||
|
||||
// Anhänge
|
||||
db.prepare('UPDATE attachments SET uploaded_by = NULL WHERE uploaded_by = ?').run(userId);
|
||||
|
||||
// Links
|
||||
db.prepare('UPDATE links SET created_by = NULL WHERE created_by = ?').run(userId);
|
||||
|
||||
// Login-Audit (kann gelöscht werden)
|
||||
db.prepare('DELETE FROM login_audit WHERE user_id = ?').run(userId);
|
||||
|
||||
// Votes des Benutzers löschen
|
||||
db.prepare('DELETE FROM proposal_votes WHERE user_id = ?').run(userId);
|
||||
|
||||
// Benutzer löschen
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
||||
|
||||
logger.info(`Admin ${req.user.username} hat Benutzer ${user.username} gelöscht`);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen des Benutzers:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Löschen des Benutzers' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/upload-settings - Upload-Einstellungen abrufen
|
||||
*/
|
||||
router.get('/upload-settings', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('upload_settings');
|
||||
|
||||
if (setting) {
|
||||
const settings = JSON.parse(setting.value);
|
||||
res.json(settings);
|
||||
} else {
|
||||
// Standard-Einstellungen zurückgeben und speichern
|
||||
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)')
|
||||
.run('upload_settings', JSON.stringify(DEFAULT_UPLOAD_SETTINGS));
|
||||
res.json(DEFAULT_UPLOAD_SETTINGS);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Upload-Einstellungen:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Abrufen der Upload-Einstellungen' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/admin/upload-settings - Upload-Einstellungen speichern
|
||||
*/
|
||||
router.put('/upload-settings', (req, res) => {
|
||||
try {
|
||||
const { maxFileSizeMB, allowedTypes } = req.body;
|
||||
|
||||
// Validierung
|
||||
if (typeof maxFileSizeMB !== 'number' || maxFileSizeMB < 1 || maxFileSizeMB > 100) {
|
||||
return res.status(400).json({ error: 'Maximale Dateigröße muss zwischen 1 und 100 MB liegen' });
|
||||
}
|
||||
|
||||
if (!allowedTypes || typeof allowedTypes !== 'object') {
|
||||
return res.status(400).json({ error: 'Ungültige Dateityp-Konfiguration' });
|
||||
}
|
||||
|
||||
const settings = { maxFileSizeMB, allowedTypes };
|
||||
|
||||
const db = getDb();
|
||||
db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)')
|
||||
.run('upload_settings', JSON.stringify(settings));
|
||||
|
||||
logger.info(`Admin ${req.user.username} hat Upload-Einstellungen geändert`);
|
||||
|
||||
res.json(settings);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Speichern der Upload-Einstellungen:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Speichern der Upload-Einstellungen' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Hilfsfunktion zum Abrufen der aktuellen Upload-Einstellungen
|
||||
*/
|
||||
function getUploadSettings() {
|
||||
try {
|
||||
const db = getDb();
|
||||
const setting = db.prepare('SELECT value FROM settings WHERE key = ?').get('upload_settings');
|
||||
|
||||
if (setting) {
|
||||
return JSON.parse(setting.value);
|
||||
}
|
||||
return DEFAULT_UPLOAD_SETTINGS;
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Upload-Einstellungen:', error);
|
||||
return DEFAULT_UPLOAD_SETTINGS;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
module.exports.getUploadSettings = getUploadSettings;
|
||||
module.exports.DEFAULT_UPLOAD_SETTINGS = DEFAULT_UPLOAD_SETTINGS;
|
||||
212
backend/routes/applications.js
Normale Datei
212
backend/routes/applications.js
Normale Datei
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* TASKMATE - Applications Route
|
||||
* ==============================
|
||||
* API-Endpoints für Anwendungs-/Repository-Verwaltung
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const gitService = require('../services/gitService');
|
||||
|
||||
/**
|
||||
* GET /api/applications/:projectId
|
||||
* Anwendungs-Konfiguration für ein Projekt abrufen
|
||||
*/
|
||||
router.get('/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const db = getDb();
|
||||
|
||||
const application = db.prepare(`
|
||||
SELECT a.*, p.name as project_name
|
||||
FROM applications a
|
||||
JOIN projects p ON a.project_id = p.id
|
||||
WHERE a.project_id = ?
|
||||
`).get(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.json({
|
||||
configured: false,
|
||||
projectId: parseInt(projectId)
|
||||
});
|
||||
}
|
||||
|
||||
// Prüfe ob das Repository existiert und erreichbar ist
|
||||
const isRepo = gitService.isGitRepository(application.local_path);
|
||||
const isAccessible = gitService.isPathAccessible(application.local_path);
|
||||
|
||||
res.json({
|
||||
configured: true,
|
||||
...application,
|
||||
isRepository: isRepo,
|
||||
isAccessible
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Anwendung:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/applications
|
||||
* Anwendung für ein Projekt erstellen oder aktualisieren
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { projectId, localPath, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch } = req.body;
|
||||
const userId = req.user.id;
|
||||
const db = getDb();
|
||||
|
||||
if (!projectId || !localPath) {
|
||||
return res.status(400).json({ error: 'projectId und localPath sind erforderlich' });
|
||||
}
|
||||
|
||||
// Prüfe ob der Pfad erreichbar ist
|
||||
if (!gitService.isPathAccessible(localPath)) {
|
||||
return res.status(400).json({
|
||||
error: 'Pfad nicht erreichbar. Stelle sicher, dass das Laufwerk in Docker gemountet ist.',
|
||||
hint: 'Gemountete Laufwerke: C:, D:, E:'
|
||||
});
|
||||
}
|
||||
|
||||
// Prüfe ob bereits eine Anwendung für dieses Projekt existiert
|
||||
const existing = db.prepare('SELECT id FROM applications WHERE project_id = ?').get(projectId);
|
||||
|
||||
if (existing) {
|
||||
// Update
|
||||
db.prepare(`
|
||||
UPDATE applications SET
|
||||
local_path = ?,
|
||||
gitea_repo_url = ?,
|
||||
gitea_repo_owner = ?,
|
||||
gitea_repo_name = ?,
|
||||
default_branch = ?
|
||||
WHERE project_id = ?
|
||||
`).run(localPath, giteaRepoUrl || null, giteaRepoOwner || null, giteaRepoName || null, defaultBranch || 'main', projectId);
|
||||
|
||||
logger.info(`Anwendung aktualisiert für Projekt ${projectId}`);
|
||||
} else {
|
||||
// Insert
|
||||
db.prepare(`
|
||||
INSERT INTO applications (project_id, local_path, gitea_repo_url, gitea_repo_owner, gitea_repo_name, default_branch, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(projectId, localPath, giteaRepoUrl || null, giteaRepoOwner || null, giteaRepoName || null, defaultBranch || 'main', userId);
|
||||
|
||||
logger.info(`Anwendung erstellt für Projekt ${projectId}`);
|
||||
}
|
||||
|
||||
// Anwendung zurückgeben
|
||||
const application = db.prepare(`
|
||||
SELECT a.*, p.name as project_name
|
||||
FROM applications a
|
||||
JOIN projects p ON a.project_id = p.id
|
||||
WHERE a.project_id = ?
|
||||
`).get(projectId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
application
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Speichern der Anwendung:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/applications/:projectId
|
||||
* Anwendungs-Konfiguration entfernen
|
||||
*/
|
||||
router.delete('/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const db = getDb();
|
||||
|
||||
const result = db.prepare('DELETE FROM applications WHERE project_id = ?').run(projectId);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt gefunden' });
|
||||
}
|
||||
|
||||
logger.info(`Anwendung gelöscht für Projekt ${projectId}`);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen der Anwendung:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/applications/user/base-path
|
||||
* Basis-Pfad des aktuellen Benutzers abrufen
|
||||
*/
|
||||
router.get('/user/base-path', (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const db = getDb();
|
||||
|
||||
const user = db.prepare('SELECT repositories_base_path FROM users WHERE id = ?').get(userId);
|
||||
|
||||
res.json({
|
||||
basePath: user?.repositories_base_path || null,
|
||||
configured: !!user?.repositories_base_path
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen des Basis-Pfads:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/applications/user/base-path
|
||||
* Basis-Pfad des aktuellen Benutzers setzen
|
||||
*/
|
||||
router.put('/user/base-path', (req, res) => {
|
||||
try {
|
||||
const { basePath } = req.body;
|
||||
const userId = req.user.id;
|
||||
const db = getDb();
|
||||
|
||||
if (!basePath) {
|
||||
return res.status(400).json({ error: 'basePath ist erforderlich' });
|
||||
}
|
||||
|
||||
// Prüfe ob der Pfad erreichbar ist
|
||||
if (!gitService.isPathAccessible(basePath)) {
|
||||
return res.status(400).json({
|
||||
error: 'Pfad nicht erreichbar. Stelle sicher, dass das Laufwerk in Docker gemountet ist.',
|
||||
hint: 'Gemountete Laufwerke: C:, D:, E:'
|
||||
});
|
||||
}
|
||||
|
||||
db.prepare('UPDATE users SET repositories_base_path = ? WHERE id = ?').run(basePath, userId);
|
||||
|
||||
logger.info(`Basis-Pfad gesetzt für Benutzer ${userId}: ${basePath}`);
|
||||
res.json({ success: true, basePath });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Setzen des Basis-Pfads:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/applications/:projectId/sync
|
||||
* Synchronisierungszeitpunkt aktualisieren
|
||||
*/
|
||||
router.post('/:projectId/sync', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const db = getDb();
|
||||
|
||||
db.prepare('UPDATE applications SET last_sync = CURRENT_TIMESTAMP WHERE project_id = ?').run(projectId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren der Synchronisierung:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
319
backend/routes/auth.js
Normale Datei
319
backend/routes/auth.js
Normale Datei
@ -0,0 +1,319 @@
|
||||
/**
|
||||
* TASKMATE - Auth Routes
|
||||
* ======================
|
||||
* Login, Logout, Token-Refresh
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { getDb } = require('../database');
|
||||
const { generateToken, authenticateToken } = require('../middleware/auth');
|
||||
const { getTokenForUser } = require('../middleware/csrf');
|
||||
const { validatePassword } = require('../middleware/validation');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Konfiguration
|
||||
const MAX_LOGIN_ATTEMPTS = parseInt(process.env.MAX_LOGIN_ATTEMPTS) || 5;
|
||||
const LOCKOUT_DURATION = (parseInt(process.env.LOCKOUT_DURATION_MINUTES) || 15) * 60 * 1000;
|
||||
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
* Benutzer anmelden
|
||||
* - Admin-User loggt sich mit username "admin" ein
|
||||
* - Alle anderen User loggen sich mit E-Mail ein
|
||||
*/
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const userAgent = req.headers['user-agent'];
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'E-Mail/Benutzername und Passwort erforderlich' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Benutzer suchen: Zuerst nach Username "admin", dann nach E-Mail
|
||||
let user;
|
||||
if (username.toLowerCase() === 'admin') {
|
||||
// Admin-User per Username suchen
|
||||
user = db.prepare('SELECT * FROM users WHERE username = ?').get('admin');
|
||||
} else {
|
||||
// Normale User per E-Mail suchen
|
||||
user = db.prepare('SELECT * FROM users WHERE email = ?').get(username);
|
||||
}
|
||||
|
||||
// Audit-Log Eintrag vorbereiten
|
||||
const logAttempt = (userId, success) => {
|
||||
db.prepare(`
|
||||
INSERT INTO login_audit (user_id, ip_address, success, user_agent)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(userId, ip, success ? 1 : 0, userAgent);
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
logger.warn(`Login fehlgeschlagen: Benutzer nicht gefunden - ${username}`);
|
||||
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
|
||||
}
|
||||
|
||||
// Prüfen ob Account gesperrt ist
|
||||
if (user.locked_until) {
|
||||
const lockedUntil = new Date(user.locked_until).getTime();
|
||||
if (Date.now() < lockedUntil) {
|
||||
const remainingMinutes = Math.ceil((lockedUntil - Date.now()) / 60000);
|
||||
logger.warn(`Login blockiert: Account gesperrt - ${username}`);
|
||||
return res.status(423).json({
|
||||
error: `Account ist gesperrt. Versuche es in ${remainingMinutes} Minuten erneut.`
|
||||
});
|
||||
} else {
|
||||
// Sperre aufheben
|
||||
db.prepare('UPDATE users SET locked_until = NULL, failed_attempts = 0 WHERE id = ?')
|
||||
.run(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Passwort prüfen
|
||||
const validPassword = await bcrypt.compare(password, user.password_hash);
|
||||
|
||||
if (!validPassword) {
|
||||
// Fehlversuche erhöhen
|
||||
const newFailedAttempts = (user.failed_attempts || 0) + 1;
|
||||
|
||||
if (newFailedAttempts >= MAX_LOGIN_ATTEMPTS) {
|
||||
// Account sperren
|
||||
const lockUntil = new Date(Date.now() + LOCKOUT_DURATION).toISOString();
|
||||
db.prepare('UPDATE users SET failed_attempts = ?, locked_until = ? WHERE id = ?')
|
||||
.run(newFailedAttempts, lockUntil, user.id);
|
||||
logger.warn(`Account gesperrt nach ${MAX_LOGIN_ATTEMPTS} Fehlversuchen: ${username}`);
|
||||
} else {
|
||||
db.prepare('UPDATE users SET failed_attempts = ? WHERE id = ?')
|
||||
.run(newFailedAttempts, user.id);
|
||||
}
|
||||
|
||||
logAttempt(user.id, false);
|
||||
logger.warn(`Login fehlgeschlagen: Falsches Passwort - ${username} (Versuch ${newFailedAttempts})`);
|
||||
|
||||
const remainingAttempts = MAX_LOGIN_ATTEMPTS - newFailedAttempts;
|
||||
return res.status(401).json({
|
||||
error: 'Ungültige Anmeldedaten',
|
||||
remainingAttempts: remainingAttempts > 0 ? remainingAttempts : 0
|
||||
});
|
||||
}
|
||||
|
||||
// Login erfolgreich - Fehlversuche zurücksetzen
|
||||
db.prepare(`
|
||||
UPDATE users
|
||||
SET failed_attempts = 0, locked_until = NULL, last_login = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(user.id);
|
||||
|
||||
logAttempt(user.id, true);
|
||||
|
||||
// JWT-Token generieren
|
||||
const token = generateToken(user);
|
||||
|
||||
// CSRF-Token generieren
|
||||
const csrfToken = getTokenForUser(user.id);
|
||||
|
||||
logger.info(`Login erfolgreich: ${username}`);
|
||||
|
||||
// Permissions parsen
|
||||
let permissions = [];
|
||||
try {
|
||||
permissions = JSON.parse(user.permissions || '[]');
|
||||
} catch (e) {
|
||||
permissions = [];
|
||||
}
|
||||
|
||||
res.json({
|
||||
token,
|
||||
csrfToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.display_name,
|
||||
color: user.color,
|
||||
role: user.role || 'user',
|
||||
permissions: permissions
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Login-Fehler:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
* Benutzer abmelden
|
||||
*/
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
// Bei JWT gibt es serverseitig nichts zu tun
|
||||
// Client muss Token löschen
|
||||
logger.info(`Logout: ${req.user.username}`);
|
||||
res.json({ message: 'Erfolgreich abgemeldet' });
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
* Aktuellen Benutzer abrufen
|
||||
*/
|
||||
router.get('/me', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const user = db.prepare('SELECT id, username, display_name, color, role, permissions FROM users WHERE id = ?')
|
||||
.get(req.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||||
}
|
||||
|
||||
// CSRF-Token erneuern
|
||||
const csrfToken = getTokenForUser(user.id);
|
||||
|
||||
// Permissions parsen
|
||||
let permissions = [];
|
||||
try {
|
||||
permissions = JSON.parse(user.permissions || '[]');
|
||||
} catch (e) {
|
||||
permissions = [];
|
||||
}
|
||||
|
||||
res.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.display_name,
|
||||
color: user.color,
|
||||
role: user.role || 'user',
|
||||
permissions: permissions
|
||||
},
|
||||
csrfToken
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei /me:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/auth/refresh
|
||||
* Token erneuern
|
||||
*/
|
||||
router.post('/refresh', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Benutzer nicht gefunden' });
|
||||
}
|
||||
|
||||
const token = generateToken(user);
|
||||
const csrfToken = getTokenForUser(user.id);
|
||||
|
||||
res.json({ token, csrfToken });
|
||||
} catch (error) {
|
||||
logger.error('Token-Refresh Fehler:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/auth/password
|
||||
* Passwort ändern
|
||||
*/
|
||||
router.put('/password', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({ error: 'Aktuelles und neues Passwort erforderlich' });
|
||||
}
|
||||
|
||||
// Passwort-Richtlinien prüfen
|
||||
const passwordErrors = validatePassword(newPassword);
|
||||
if (passwordErrors.length > 0) {
|
||||
return res.status(400).json({ error: passwordErrors.join('. ') });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id);
|
||||
|
||||
// Aktuelles Passwort prüfen
|
||||
const validPassword = await bcrypt.compare(currentPassword, user.password_hash);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' });
|
||||
}
|
||||
|
||||
// Neues Passwort hashen und speichern
|
||||
const newHash = await bcrypt.hash(newPassword, 12);
|
||||
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(newHash, user.id);
|
||||
|
||||
logger.info(`Passwort geändert: ${user.username}`);
|
||||
res.json({ message: 'Passwort erfolgreich geändert' });
|
||||
} catch (error) {
|
||||
logger.error('Passwort-Änderung Fehler:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/auth/color
|
||||
* Benutzerfarbe ändern
|
||||
*/
|
||||
router.put('/color', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const { color } = req.body;
|
||||
|
||||
if (!color) {
|
||||
return res.status(400).json({ error: 'Farbe erforderlich' });
|
||||
}
|
||||
|
||||
// Validate hex color format
|
||||
const hexColorRegex = /^#[0-9A-Fa-f]{6}$/;
|
||||
if (!hexColorRegex.test(color)) {
|
||||
return res.status(400).json({ error: 'Ungültiges Farbformat (erwartet: #RRGGBB)' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
db.prepare('UPDATE users SET color = ? WHERE id = ?').run(color, req.user.id);
|
||||
|
||||
logger.info(`Farbe geändert: ${req.user.username} -> ${color}`);
|
||||
res.json({ message: 'Farbe erfolgreich geändert', color });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Ändern der Farbe:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/auth/users
|
||||
* Alle Benutzer abrufen (für Zuweisung)
|
||||
* Admin-Benutzer werden ausgeschlossen, da sie nur fuer die Benutzerverwaltung sind
|
||||
*/
|
||||
router.get('/users', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
// Nur regulaere Benutzer (nicht Admins) fuer Aufgaben-Zuweisung
|
||||
const users = db.prepare(`
|
||||
SELECT id, username, display_name, color
|
||||
FROM users
|
||||
WHERE role != 'admin' OR role IS NULL
|
||||
`).all();
|
||||
|
||||
res.json(users.map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
displayName: u.display_name,
|
||||
color: u.color
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Benutzer:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
302
backend/routes/columns.js
Normale Datei
302
backend/routes/columns.js
Normale Datei
@ -0,0 +1,302 @@
|
||||
/**
|
||||
* TASKMATE - Column Routes
|
||||
* ========================
|
||||
* CRUD für Board-Spalten
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* GET /api/columns/:projectId
|
||||
* Alle Spalten eines Projekts
|
||||
*/
|
||||
router.get('/:projectId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const columns = db.prepare(`
|
||||
SELECT * FROM columns WHERE project_id = ? ORDER BY position
|
||||
`).all(req.params.projectId);
|
||||
|
||||
res.json(columns.map(c => ({
|
||||
id: c.id,
|
||||
projectId: c.project_id,
|
||||
name: c.name,
|
||||
position: c.position,
|
||||
color: c.color,
|
||||
filterCategory: c.filter_category || 'in_progress'
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Spalten:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/columns
|
||||
* Neue Spalte erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { projectId, name, color, filterCategory } = req.body;
|
||||
|
||||
// Validierung
|
||||
const errors = [];
|
||||
errors.push(validators.required(projectId, 'Projekt-ID'));
|
||||
errors.push(validators.required(name, 'Name'));
|
||||
errors.push(validators.maxLength(name, 50, 'Name'));
|
||||
if (color) errors.push(validators.hexColor(color, 'Farbe'));
|
||||
|
||||
const firstError = errors.find(e => e !== null);
|
||||
if (firstError) {
|
||||
return res.status(400).json({ error: firstError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Höchste Position ermitteln
|
||||
const maxPos = db.prepare(
|
||||
'SELECT COALESCE(MAX(position), -1) as max FROM columns WHERE project_id = ?'
|
||||
).get(projectId).max;
|
||||
|
||||
// Spalte erstellen mit filter_category
|
||||
const result = db.prepare(`
|
||||
INSERT INTO columns (project_id, name, position, color, filter_category)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(projectId, name, maxPos + 1, color || null, filterCategory || 'in_progress');
|
||||
|
||||
const column = db.prepare('SELECT * FROM columns WHERE id = ?').get(result.lastInsertRowid);
|
||||
|
||||
logger.info(`Spalte erstellt: ${name} in Projekt ${projectId} (Filter: ${filterCategory || 'in_progress'})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${projectId}`).emit('column:created', {
|
||||
id: column.id,
|
||||
projectId: column.project_id,
|
||||
name: column.name,
|
||||
position: column.position,
|
||||
color: column.color,
|
||||
filterCategory: column.filter_category
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: column.id,
|
||||
projectId: column.project_id,
|
||||
name: column.name,
|
||||
position: column.position,
|
||||
color: column.color,
|
||||
filterCategory: column.filter_category
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen der Spalte:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/columns/:id
|
||||
* Spalte aktualisieren
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const columnId = req.params.id;
|
||||
const { name, color, filterCategory } = req.body;
|
||||
|
||||
// Validierung
|
||||
if (name) {
|
||||
const nameError = validators.maxLength(name, 50, 'Name');
|
||||
if (nameError) {
|
||||
return res.status(400).json({ error: nameError });
|
||||
}
|
||||
}
|
||||
if (color) {
|
||||
const colorError = validators.hexColor(color, 'Farbe');
|
||||
if (colorError) {
|
||||
return res.status(400).json({ error: colorError });
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare('SELECT * FROM columns WHERE id = ?').get(columnId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Spalte nicht gefunden' });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE columns
|
||||
SET name = COALESCE(?, name), color = ?, filter_category = COALESCE(?, filter_category)
|
||||
WHERE id = ?
|
||||
`).run(name || null, color !== undefined ? color : existing.color, filterCategory || null, columnId);
|
||||
|
||||
const column = db.prepare('SELECT * FROM columns WHERE id = ?').get(columnId);
|
||||
|
||||
logger.info(`Spalte aktualisiert: ${column.name} (ID: ${columnId})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${column.project_id}`).emit('column:updated', {
|
||||
id: column.id,
|
||||
name: column.name,
|
||||
color: column.color,
|
||||
filterCategory: column.filter_category
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: column.id,
|
||||
projectId: column.project_id,
|
||||
name: column.name,
|
||||
position: column.position,
|
||||
color: column.color,
|
||||
filterCategory: column.filter_category
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren der Spalte:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/columns/:id/position
|
||||
* Spalten-Position ändern (Reihenfolge)
|
||||
*/
|
||||
router.put('/:id/position', (req, res) => {
|
||||
try {
|
||||
const columnId = req.params.id;
|
||||
const { newPosition } = req.body;
|
||||
|
||||
if (typeof newPosition !== 'number' || newPosition < 0) {
|
||||
return res.status(400).json({ error: 'Ungültige Position' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const column = db.prepare('SELECT * FROM columns WHERE id = ?').get(columnId);
|
||||
if (!column) {
|
||||
return res.status(404).json({ error: 'Spalte nicht gefunden' });
|
||||
}
|
||||
|
||||
const oldPosition = column.position;
|
||||
const projectId = column.project_id;
|
||||
|
||||
// Positionen der anderen Spalten anpassen
|
||||
if (newPosition > oldPosition) {
|
||||
// Nach rechts verschoben: Spalten dazwischen nach links
|
||||
db.prepare(`
|
||||
UPDATE columns
|
||||
SET position = position - 1
|
||||
WHERE project_id = ? AND position > ? AND position <= ?
|
||||
`).run(projectId, oldPosition, newPosition);
|
||||
} else if (newPosition < oldPosition) {
|
||||
// Nach links verschoben: Spalten dazwischen nach rechts
|
||||
db.prepare(`
|
||||
UPDATE columns
|
||||
SET position = position + 1
|
||||
WHERE project_id = ? AND position >= ? AND position < ?
|
||||
`).run(projectId, newPosition, oldPosition);
|
||||
}
|
||||
|
||||
// Neue Position setzen
|
||||
db.prepare('UPDATE columns SET position = ? WHERE id = ?').run(newPosition, columnId);
|
||||
|
||||
// Alle Spalten des Projekts zurückgeben
|
||||
const columns = db.prepare(
|
||||
'SELECT * FROM columns WHERE project_id = ? ORDER BY position'
|
||||
).all(projectId);
|
||||
|
||||
logger.info(`Spalte ${column.name} von Position ${oldPosition} zu ${newPosition} verschoben`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${projectId}`).emit('columns:reordered', {
|
||||
columns: columns.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
position: c.position,
|
||||
color: c.color,
|
||||
filterCategory: c.filter_category
|
||||
}))
|
||||
});
|
||||
|
||||
res.json({
|
||||
columns: columns.map(c => ({
|
||||
id: c.id,
|
||||
projectId: c.project_id,
|
||||
name: c.name,
|
||||
position: c.position,
|
||||
color: c.color,
|
||||
filterCategory: c.filter_category
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Verschieben der Spalte:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/columns/:id
|
||||
* Spalte löschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const columnId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const column = db.prepare('SELECT * FROM columns WHERE id = ?').get(columnId);
|
||||
if (!column) {
|
||||
return res.status(404).json({ error: 'Spalte nicht gefunden' });
|
||||
}
|
||||
|
||||
// Prüfen ob Aufgaben in der Spalte sind
|
||||
const taskCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM tasks WHERE column_id = ?'
|
||||
).get(columnId).count;
|
||||
|
||||
if (taskCount > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Spalte enthält noch Aufgaben. Verschiebe oder lösche diese zuerst.'
|
||||
});
|
||||
}
|
||||
|
||||
// Mindestens eine Spalte muss bleiben
|
||||
const columnCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM columns WHERE project_id = ?'
|
||||
).get(column.project_id).count;
|
||||
|
||||
if (columnCount <= 1) {
|
||||
return res.status(400).json({
|
||||
error: 'Mindestens eine Spalte muss vorhanden sein.'
|
||||
});
|
||||
}
|
||||
|
||||
// Spalte löschen
|
||||
db.prepare('DELETE FROM columns WHERE id = ?').run(columnId);
|
||||
|
||||
// Positionen neu nummerieren
|
||||
const remainingColumns = db.prepare(
|
||||
'SELECT id FROM columns WHERE project_id = ? ORDER BY position'
|
||||
).all(column.project_id);
|
||||
|
||||
remainingColumns.forEach((col, index) => {
|
||||
db.prepare('UPDATE columns SET position = ? WHERE id = ?').run(index, col.id);
|
||||
});
|
||||
|
||||
logger.info(`Spalte gelöscht: ${column.name} (ID: ${columnId})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${column.project_id}`).emit('column:deleted', { id: columnId });
|
||||
|
||||
res.json({ message: 'Spalte gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen der Spalte:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
279
backend/routes/comments.js
Normale Datei
279
backend/routes/comments.js
Normale Datei
@ -0,0 +1,279 @@
|
||||
/**
|
||||
* TASKMATE - Comment Routes
|
||||
* =========================
|
||||
* CRUD für Kommentare
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators, sanitizeMarkdown } = require('../middleware/validation');
|
||||
const notificationService = require('../services/notificationService');
|
||||
|
||||
/**
|
||||
* GET /api/comments/:taskId
|
||||
* Alle Kommentare einer Aufgabe
|
||||
*/
|
||||
router.get('/:taskId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const comments = db.prepare(`
|
||||
SELECT c.*, u.display_name, u.color
|
||||
FROM comments c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.task_id = ?
|
||||
ORDER BY c.created_at ASC
|
||||
`).all(req.params.taskId);
|
||||
|
||||
res.json(comments.map(c => ({
|
||||
id: c.id,
|
||||
taskId: c.task_id,
|
||||
userId: c.user_id,
|
||||
userName: c.display_name,
|
||||
userColor: c.color,
|
||||
content: c.content,
|
||||
createdAt: c.created_at
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Kommentare:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/comments
|
||||
* Neuen Kommentar erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { taskId, content } = req.body;
|
||||
|
||||
// Validierung
|
||||
const contentError = validators.required(content, 'Inhalt') ||
|
||||
validators.maxLength(content, 5000, 'Inhalt');
|
||||
if (contentError) {
|
||||
return res.status(400).json({ error: contentError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Task prüfen
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
// Inhalt bereinigen (Markdown erlaubt)
|
||||
const sanitizedContent = sanitizeMarkdown(content);
|
||||
|
||||
// @Erwähnungen verarbeiten
|
||||
const mentions = content.match(/@(\w+)/g);
|
||||
const mentionedUsers = [];
|
||||
|
||||
if (mentions) {
|
||||
mentions.forEach(mention => {
|
||||
const username = mention.substring(1);
|
||||
const user = db.prepare('SELECT id, display_name FROM users WHERE username = ?').get(username);
|
||||
if (user) {
|
||||
mentionedUsers.push(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Kommentar erstellen
|
||||
const result = db.prepare(`
|
||||
INSERT INTO comments (task_id, user_id, content)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(taskId, req.user.id, sanitizedContent);
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(taskId);
|
||||
|
||||
// Historie
|
||||
db.prepare(`
|
||||
INSERT INTO history (task_id, user_id, action, new_value)
|
||||
VALUES (?, ?, 'commented', ?)
|
||||
`).run(taskId, req.user.id, sanitizedContent.substring(0, 100));
|
||||
|
||||
const comment = db.prepare(`
|
||||
SELECT c.*, u.display_name, u.color
|
||||
FROM comments c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
logger.info(`Kommentar erstellt in Task ${taskId} von ${req.user.username}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('comment:created', {
|
||||
taskId,
|
||||
comment: {
|
||||
id: comment.id,
|
||||
taskId: comment.task_id,
|
||||
userId: comment.user_id,
|
||||
userName: comment.display_name,
|
||||
userColor: comment.color,
|
||||
content: comment.content,
|
||||
createdAt: comment.created_at
|
||||
},
|
||||
mentionedUsers
|
||||
});
|
||||
|
||||
// Benachrichtigungen senden
|
||||
// 1. Benachrichtigung an zugewiesene Mitarbeiter der Aufgabe
|
||||
const assignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
|
||||
const mentionedUserIds = mentionedUsers.map(u => u.id);
|
||||
|
||||
assignees.forEach(a => {
|
||||
// Nicht an Kommentator und nicht an erwähnte Benutzer (die bekommen separate Benachrichtigung)
|
||||
if (a.user_id !== req.user.id && !mentionedUserIds.includes(a.user_id)) {
|
||||
notificationService.create(a.user_id, 'comment:created', {
|
||||
taskId: parseInt(taskId),
|
||||
taskTitle: task.title,
|
||||
projectId: task.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Benachrichtigung an erwähnte Benutzer
|
||||
mentionedUsers.forEach(user => {
|
||||
if (user.id !== req.user.id) {
|
||||
notificationService.create(user.id, 'comment:mention', {
|
||||
taskId: parseInt(taskId),
|
||||
taskTitle: task.title,
|
||||
projectId: task.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: comment.id,
|
||||
taskId: comment.task_id,
|
||||
userId: comment.user_id,
|
||||
userName: comment.display_name,
|
||||
userColor: comment.color,
|
||||
content: comment.content,
|
||||
createdAt: comment.created_at
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Kommentars:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/comments/:id
|
||||
* Kommentar bearbeiten (nur eigene)
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const commentId = req.params.id;
|
||||
const { content } = req.body;
|
||||
|
||||
// Validierung
|
||||
const contentError = validators.required(content, 'Inhalt') ||
|
||||
validators.maxLength(content, 5000, 'Inhalt');
|
||||
if (contentError) {
|
||||
return res.status(400).json({ error: contentError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const comment = db.prepare('SELECT * FROM comments WHERE id = ?').get(commentId);
|
||||
if (!comment) {
|
||||
return res.status(404).json({ error: 'Kommentar nicht gefunden' });
|
||||
}
|
||||
|
||||
// Nur eigene Kommentare bearbeiten
|
||||
if (comment.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Nur eigene Kommentare können bearbeitet werden' });
|
||||
}
|
||||
|
||||
const sanitizedContent = sanitizeMarkdown(content);
|
||||
|
||||
db.prepare('UPDATE comments SET content = ? WHERE id = ?')
|
||||
.run(sanitizedContent, commentId);
|
||||
|
||||
const updated = db.prepare(`
|
||||
SELECT c.*, u.display_name, u.color
|
||||
FROM comments c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.id = ?
|
||||
`).get(commentId);
|
||||
|
||||
const task = db.prepare('SELECT project_id FROM tasks WHERE id = ?').get(comment.task_id);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('comment:updated', {
|
||||
taskId: comment.task_id,
|
||||
comment: {
|
||||
id: updated.id,
|
||||
taskId: updated.task_id,
|
||||
userId: updated.user_id,
|
||||
userName: updated.display_name,
|
||||
userColor: updated.color,
|
||||
content: updated.content,
|
||||
createdAt: updated.created_at
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: updated.id,
|
||||
taskId: updated.task_id,
|
||||
userId: updated.user_id,
|
||||
userName: updated.display_name,
|
||||
userColor: updated.color,
|
||||
content: updated.content,
|
||||
createdAt: updated.created_at
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren des Kommentars:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/comments/:id
|
||||
* Kommentar löschen (nur eigene)
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const commentId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const comment = db.prepare('SELECT * FROM comments WHERE id = ?').get(commentId);
|
||||
if (!comment) {
|
||||
return res.status(404).json({ error: 'Kommentar nicht gefunden' });
|
||||
}
|
||||
|
||||
// Nur eigene Kommentare löschen
|
||||
if (comment.user_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Nur eigene Kommentare können gelöscht werden' });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM comments WHERE id = ?').run(commentId);
|
||||
|
||||
const task = db.prepare('SELECT project_id FROM tasks WHERE id = ?').get(comment.task_id);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('comment:deleted', {
|
||||
taskId: comment.task_id,
|
||||
commentId
|
||||
});
|
||||
|
||||
res.json({ message: 'Kommentar gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen des Kommentars:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
230
backend/routes/export.js
Normale Datei
230
backend/routes/export.js
Normale Datei
@ -0,0 +1,230 @@
|
||||
/**
|
||||
* TASKMATE - Export Routes
|
||||
* ========================
|
||||
* Export in JSON und CSV
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* GET /api/export/project/:id/json
|
||||
* Projekt als JSON exportieren
|
||||
*/
|
||||
router.get('/project/:id/json', (req, res) => {
|
||||
try {
|
||||
const projectId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
// Projekt
|
||||
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
||||
}
|
||||
|
||||
// Spalten
|
||||
const columns = db.prepare('SELECT * FROM columns WHERE project_id = ? ORDER BY position').all(projectId);
|
||||
|
||||
// Labels
|
||||
const labels = db.prepare('SELECT * FROM labels WHERE project_id = ?').all(projectId);
|
||||
|
||||
// Aufgaben mit allen Details
|
||||
const tasks = db.prepare('SELECT * FROM tasks WHERE project_id = ?').all(projectId);
|
||||
|
||||
const tasksWithDetails = tasks.map(task => {
|
||||
const taskLabels = db.prepare(`
|
||||
SELECT l.* FROM labels l
|
||||
JOIN task_labels tl ON l.id = tl.label_id
|
||||
WHERE tl.task_id = ?
|
||||
`).all(task.id);
|
||||
|
||||
const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(task.id);
|
||||
const comments = db.prepare(`
|
||||
SELECT c.*, u.display_name FROM comments c
|
||||
LEFT JOIN users u ON c.user_id = u.id
|
||||
WHERE c.task_id = ?
|
||||
`).all(task.id);
|
||||
const attachments = db.prepare('SELECT * FROM attachments WHERE task_id = ?').all(task.id);
|
||||
const links = db.prepare('SELECT * FROM links WHERE task_id = ?').all(task.id);
|
||||
|
||||
return {
|
||||
...task,
|
||||
labels: taskLabels,
|
||||
subtasks,
|
||||
comments,
|
||||
attachments,
|
||||
links
|
||||
};
|
||||
});
|
||||
|
||||
// Vorlagen
|
||||
const templates = db.prepare('SELECT * FROM task_templates WHERE project_id = ?').all(projectId);
|
||||
|
||||
const exportData = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
exportedBy: req.user.username,
|
||||
version: '1.0',
|
||||
project: {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
createdAt: project.created_at
|
||||
},
|
||||
columns: columns.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
position: c.position,
|
||||
color: c.color
|
||||
})),
|
||||
labels: labels.map(l => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
color: l.color
|
||||
})),
|
||||
tasks: tasksWithDetails,
|
||||
templates
|
||||
};
|
||||
|
||||
logger.info(`Projekt exportiert als JSON: ${project.name}`);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${project.name.replace(/[^a-z0-9]/gi, '_')}_export.json"`);
|
||||
res.json(exportData);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim JSON-Export:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/export/project/:id/csv
|
||||
* Aufgaben als CSV exportieren
|
||||
*/
|
||||
router.get('/project/:id/csv', (req, res) => {
|
||||
try {
|
||||
const projectId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
||||
}
|
||||
|
||||
const tasks = db.prepare(`
|
||||
SELECT
|
||||
t.*,
|
||||
c.name as column_name,
|
||||
u.display_name as assigned_name
|
||||
FROM tasks t
|
||||
LEFT JOIN columns c ON t.column_id = c.id
|
||||
LEFT JOIN users u ON t.assigned_to = u.id
|
||||
WHERE t.project_id = ?
|
||||
ORDER BY c.position, t.position
|
||||
`).all(projectId);
|
||||
|
||||
// CSV Header
|
||||
const headers = [
|
||||
'ID', 'Titel', 'Beschreibung', 'Status', 'Priorität',
|
||||
'Fälligkeitsdatum', 'Zugewiesen an', 'Zeitschätzung (Min)',
|
||||
'Erstellt am', 'Archiviert'
|
||||
];
|
||||
|
||||
// CSV Zeilen
|
||||
const rows = tasks.map(task => [
|
||||
task.id,
|
||||
escapeCsvField(task.title),
|
||||
escapeCsvField(task.description || ''),
|
||||
task.column_name,
|
||||
task.priority,
|
||||
task.due_date || '',
|
||||
task.assigned_name || '',
|
||||
task.time_estimate_min || '',
|
||||
task.created_at,
|
||||
task.archived ? 'Ja' : 'Nein'
|
||||
]);
|
||||
|
||||
// CSV zusammenbauen
|
||||
const csv = [
|
||||
headers.join(';'),
|
||||
...rows.map(row => row.join(';'))
|
||||
].join('\n');
|
||||
|
||||
logger.info(`Projekt exportiert als CSV: ${project.name}`);
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${project.name.replace(/[^a-z0-9]/gi, '_')}_export.csv"`);
|
||||
// BOM für Excel UTF-8 Erkennung
|
||||
res.send('\ufeff' + csv);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim CSV-Export:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/export/all/json
|
||||
* Alle Daten exportieren (Backup)
|
||||
*/
|
||||
router.get('/all/json', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
const projects = db.prepare('SELECT * FROM projects').all();
|
||||
const columns = db.prepare('SELECT * FROM columns').all();
|
||||
const labels = db.prepare('SELECT * FROM labels').all();
|
||||
const tasks = db.prepare('SELECT * FROM tasks').all();
|
||||
const subtasks = db.prepare('SELECT * FROM subtasks').all();
|
||||
const comments = db.prepare('SELECT * FROM comments').all();
|
||||
const taskLabels = db.prepare('SELECT * FROM task_labels').all();
|
||||
const attachments = db.prepare('SELECT * FROM attachments').all();
|
||||
const links = db.prepare('SELECT * FROM links').all();
|
||||
const templates = db.prepare('SELECT * FROM task_templates').all();
|
||||
const history = db.prepare('SELECT * FROM history').all();
|
||||
|
||||
const exportData = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
exportedBy: req.user.username,
|
||||
version: '1.0',
|
||||
data: {
|
||||
projects,
|
||||
columns,
|
||||
labels,
|
||||
tasks,
|
||||
subtasks,
|
||||
comments,
|
||||
taskLabels,
|
||||
attachments,
|
||||
links,
|
||||
templates,
|
||||
history
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('Vollständiger Export durchgeführt');
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="taskmate_backup_${Date.now()}.json"`);
|
||||
res.json(exportData);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Voll-Export:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: CSV-Feld escapen
|
||||
*/
|
||||
function escapeCsvField(field) {
|
||||
if (typeof field !== 'string') return field;
|
||||
|
||||
// Wenn Feld Semikolon, Anführungszeichen oder Zeilenumbruch enthält
|
||||
if (field.includes(';') || field.includes('"') || field.includes('\n')) {
|
||||
// Anführungszeichen verdoppeln und in Anführungszeichen setzen
|
||||
return '"' + field.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
238
backend/routes/files.js
Normale Datei
238
backend/routes/files.js
Normale Datei
@ -0,0 +1,238 @@
|
||||
/**
|
||||
* TASKMATE - File Routes
|
||||
* ======================
|
||||
* Upload, Download, Löschen von Dateien
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { upload, deleteFile, formatFileSize, isImage, getFileIcon, UPLOAD_DIR } = require('../middleware/upload');
|
||||
const csrfProtection = require('../middleware/csrf');
|
||||
|
||||
/**
|
||||
* GET /api/files/:taskId
|
||||
* Alle Dateien einer Aufgabe
|
||||
*/
|
||||
router.get('/:taskId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const attachments = db.prepare(`
|
||||
SELECT a.*, u.display_name as uploader_name
|
||||
FROM attachments a
|
||||
LEFT JOIN users u ON a.uploaded_by = u.id
|
||||
WHERE a.task_id = ?
|
||||
ORDER BY a.uploaded_at DESC
|
||||
`).all(req.params.taskId);
|
||||
|
||||
res.json(attachments.map(a => ({
|
||||
id: a.id,
|
||||
taskId: a.task_id,
|
||||
filename: a.filename,
|
||||
originalName: a.original_name,
|
||||
mimeType: a.mime_type,
|
||||
sizeBytes: a.size_bytes,
|
||||
sizeFormatted: formatFileSize(a.size_bytes),
|
||||
isImage: isImage(a.mime_type),
|
||||
icon: getFileIcon(a.mime_type),
|
||||
uploadedBy: a.uploaded_by,
|
||||
uploaderName: a.uploader_name,
|
||||
uploadedAt: a.uploaded_at
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Dateien:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/files/:taskId
|
||||
* Datei(en) hochladen
|
||||
*/
|
||||
router.post('/:taskId', csrfProtection, upload.array('files', 10), (req, res) => {
|
||||
try {
|
||||
const taskId = req.params.taskId;
|
||||
const db = getDb();
|
||||
|
||||
// Task prüfen
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
// Hochgeladene Dateien löschen
|
||||
req.files?.forEach(f => fs.unlinkSync(f.path));
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({ error: 'Keine Dateien hochgeladen' });
|
||||
}
|
||||
|
||||
const insertAttachment = db.prepare(`
|
||||
INSERT INTO attachments (task_id, filename, original_name, mime_type, size_bytes, uploaded_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const attachments = [];
|
||||
|
||||
req.files.forEach(file => {
|
||||
const result = insertAttachment.run(
|
||||
taskId,
|
||||
`task_${taskId}/${file.filename}`,
|
||||
file.originalname,
|
||||
file.mimetype,
|
||||
file.size,
|
||||
req.user.id
|
||||
);
|
||||
|
||||
attachments.push({
|
||||
id: result.lastInsertRowid,
|
||||
taskId: parseInt(taskId),
|
||||
filename: `task_${taskId}/${file.filename}`,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
sizeBytes: file.size,
|
||||
sizeFormatted: formatFileSize(file.size),
|
||||
isImage: isImage(file.mimetype),
|
||||
icon: getFileIcon(file.mimetype),
|
||||
uploadedBy: req.user.id,
|
||||
uploaderName: req.user.displayName,
|
||||
uploadedAt: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(taskId);
|
||||
|
||||
// Historie
|
||||
db.prepare(`
|
||||
INSERT INTO history (task_id, user_id, action, new_value)
|
||||
VALUES (?, ?, 'attachment_added', ?)
|
||||
`).run(taskId, req.user.id, attachments.map(a => a.originalName).join(', '));
|
||||
|
||||
logger.info(`${attachments.length} Datei(en) hochgeladen für Task ${taskId}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('files:uploaded', {
|
||||
taskId,
|
||||
attachments
|
||||
});
|
||||
|
||||
res.status(201).json({ attachments });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Hochladen:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/files/download/:id
|
||||
* Datei herunterladen
|
||||
*/
|
||||
router.get('/download/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const attachment = db.prepare('SELECT * FROM attachments WHERE id = ?').get(req.params.id);
|
||||
|
||||
if (!attachment) {
|
||||
return res.status(404).json({ error: 'Datei nicht gefunden' });
|
||||
}
|
||||
|
||||
const filePath = path.join(UPLOAD_DIR, attachment.filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
logger.error(`Datei existiert nicht: ${filePath}`);
|
||||
return res.status(404).json({ error: 'Datei nicht gefunden' });
|
||||
}
|
||||
|
||||
res.download(filePath, attachment.original_name);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Download:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/files/preview/:id
|
||||
* Bild-Vorschau
|
||||
*/
|
||||
router.get('/preview/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const attachment = db.prepare('SELECT * FROM attachments WHERE id = ?').get(req.params.id);
|
||||
|
||||
if (!attachment) {
|
||||
return res.status(404).json({ error: 'Datei nicht gefunden' });
|
||||
}
|
||||
|
||||
if (!isImage(attachment.mime_type)) {
|
||||
return res.status(400).json({ error: 'Keine Bilddatei' });
|
||||
}
|
||||
|
||||
const filePath = path.join(UPLOAD_DIR, attachment.filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({ error: 'Datei nicht gefunden' });
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', attachment.mime_type);
|
||||
res.sendFile(filePath);
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei Vorschau:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/files/:id
|
||||
* Datei löschen
|
||||
*/
|
||||
router.delete('/:id', csrfProtection, (req, res) => {
|
||||
try {
|
||||
const attachmentId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const attachment = db.prepare('SELECT * FROM attachments WHERE id = ?').get(attachmentId);
|
||||
if (!attachment) {
|
||||
return res.status(404).json({ error: 'Datei nicht gefunden' });
|
||||
}
|
||||
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(attachment.task_id);
|
||||
|
||||
// Datei vom Dateisystem löschen
|
||||
const filePath = path.join(UPLOAD_DIR, attachment.filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// Aus Datenbank löschen
|
||||
db.prepare('DELETE FROM attachments WHERE id = ?').run(attachmentId);
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(attachment.task_id);
|
||||
|
||||
// Historie
|
||||
db.prepare(`
|
||||
INSERT INTO history (task_id, user_id, action, old_value)
|
||||
VALUES (?, ?, 'attachment_removed', ?)
|
||||
`).run(attachment.task_id, req.user.id, attachment.original_name);
|
||||
|
||||
logger.info(`Datei gelöscht: ${attachment.original_name}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('file:deleted', {
|
||||
taskId: attachment.task_id,
|
||||
attachmentId
|
||||
});
|
||||
|
||||
res.json({ message: 'Datei gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen der Datei:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
444
backend/routes/git.js
Normale Datei
444
backend/routes/git.js
Normale Datei
@ -0,0 +1,444 @@
|
||||
/**
|
||||
* TASKMATE - Git Route
|
||||
* =====================
|
||||
* API-Endpoints für Git-Operationen
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const gitService = require('../services/gitService');
|
||||
const giteaService = require('../services/giteaService');
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Anwendung für Projekt abrufen
|
||||
*/
|
||||
function getApplicationForProject(projectId) {
|
||||
const db = getDb();
|
||||
return db.prepare('SELECT * FROM applications WHERE project_id = ?').get(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/git/clone
|
||||
* Repository klonen
|
||||
*/
|
||||
router.post('/clone', async (req, res) => {
|
||||
try {
|
||||
const { projectId, repoUrl, localPath, branch } = req.body;
|
||||
|
||||
if (!localPath) {
|
||||
return res.status(400).json({ error: 'localPath ist erforderlich' });
|
||||
}
|
||||
|
||||
if (!repoUrl) {
|
||||
return res.status(400).json({ error: 'repoUrl ist erforderlich' });
|
||||
}
|
||||
|
||||
// Clone ausführen
|
||||
const result = await gitService.cloneRepository(repoUrl, localPath, { branch });
|
||||
|
||||
if (result.success && projectId) {
|
||||
// Anwendung aktualisieren
|
||||
const db = getDb();
|
||||
db.prepare('UPDATE applications SET last_sync = CURRENT_TIMESTAMP WHERE project_id = ?').run(projectId);
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Klonen:', error);
|
||||
res.status(500).json({ error: 'Serverfehler', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/git/status/:projectId
|
||||
* Git-Status für ein Projekt abrufen
|
||||
*/
|
||||
router.get('/status/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
const result = gitService.getStatus(application.local_path);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen des Status:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/pull/:projectId
|
||||
* Pull für ein Projekt ausführen
|
||||
*/
|
||||
router.post('/pull/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { branch } = req.body;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
// Fetch zuerst
|
||||
gitService.fetchRemote(application.local_path);
|
||||
|
||||
// Dann Pull
|
||||
const result = gitService.pullChanges(application.local_path, { branch });
|
||||
|
||||
if (result.success) {
|
||||
// Sync-Zeitpunkt aktualisieren
|
||||
const db = getDb();
|
||||
db.prepare('UPDATE applications SET last_sync = CURRENT_TIMESTAMP WHERE project_id = ?').run(projectId);
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Pull:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/push/:projectId
|
||||
* Push für ein Projekt ausführen
|
||||
*/
|
||||
router.post('/push/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { branch } = req.body;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
// Prüfe ob Remote existiert
|
||||
if (!gitService.hasRemote(application.local_path)) {
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'Kein Remote konfiguriert. Bitte Repository zuerst vorbereiten.'
|
||||
});
|
||||
}
|
||||
|
||||
// Versuche normalen Push, falls das fehlschlägt wegen fehlendem Upstream, push mit -u
|
||||
let result = gitService.pushChanges(application.local_path, { branch });
|
||||
|
||||
// Falls Push wegen fehlendem Upstream fehlschlägt, versuche mit -u
|
||||
if (!result.success && result.error && result.error.includes('no upstream')) {
|
||||
const currentBranch = branch || 'main';
|
||||
result = gitService.pushWithUpstream(application.local_path, currentBranch);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Sync-Zeitpunkt aktualisieren
|
||||
const db = getDb();
|
||||
db.prepare('UPDATE applications SET last_sync = CURRENT_TIMESTAMP WHERE project_id = ?').run(projectId);
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Push:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/commit/:projectId
|
||||
* Commit für ein Projekt erstellen
|
||||
*/
|
||||
router.post('/commit/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { message, stageAll } = req.body;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Commit-Nachricht ist erforderlich' });
|
||||
}
|
||||
|
||||
// Optional: Alle Änderungen stagen
|
||||
if (stageAll !== false) {
|
||||
const stageResult = gitService.stageAll(application.local_path);
|
||||
if (!stageResult.success) {
|
||||
return res.json(stageResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit erstellen
|
||||
const result = gitService.commit(application.local_path, message);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Commit:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/git/commits/:projectId
|
||||
* Commit-Historie für ein Projekt abrufen
|
||||
*/
|
||||
router.get('/commits/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
const result = gitService.getCommitHistory(application.local_path, limit);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Commits:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/git/branches/:projectId
|
||||
* Branches für ein Projekt abrufen
|
||||
*/
|
||||
router.get('/branches/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
const result = gitService.getBranches(application.local_path);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Branches:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/checkout/:projectId
|
||||
* Branch wechseln
|
||||
*/
|
||||
router.post('/checkout/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { branch } = req.body;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
if (!branch) {
|
||||
return res.status(400).json({ error: 'Branch ist erforderlich' });
|
||||
}
|
||||
|
||||
const result = gitService.checkoutBranch(application.local_path, branch);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Branch-Wechsel:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/fetch/:projectId
|
||||
* Fetch von Remote ausführen
|
||||
*/
|
||||
router.post('/fetch/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
const result = gitService.fetchRemote(application.local_path);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Fetch:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/stage/:projectId
|
||||
* Alle Änderungen stagen
|
||||
*/
|
||||
router.post('/stage/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
const result = gitService.stageAll(application.local_path);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Stagen:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/git/remote/:projectId
|
||||
* Remote-URL abrufen
|
||||
*/
|
||||
router.get('/remote/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
const result = gitService.getRemoteUrl(application.local_path);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Remote-URL:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/validate-path
|
||||
* Pfad validieren
|
||||
*/
|
||||
router.post('/validate-path', (req, res) => {
|
||||
try {
|
||||
const { path } = req.body;
|
||||
|
||||
if (!path) {
|
||||
return res.status(400).json({ error: 'Pfad ist erforderlich' });
|
||||
}
|
||||
|
||||
const isAccessible = gitService.isPathAccessible(path);
|
||||
const isRepo = isAccessible ? gitService.isGitRepository(path) : false;
|
||||
const hasRemote = isRepo ? gitService.hasRemote(path) : false;
|
||||
|
||||
res.json({
|
||||
valid: isAccessible,
|
||||
isRepository: isRepo,
|
||||
hasRemote: hasRemote,
|
||||
containerPath: gitService.windowsToContainerPath(path)
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei der Pfad-Validierung:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/prepare/:projectId
|
||||
* Repository für Gitea vorbereiten (init, remote hinzufügen)
|
||||
*/
|
||||
router.post('/prepare/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { repoUrl, branch } = req.body;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
if (!repoUrl) {
|
||||
return res.status(400).json({ error: 'repoUrl ist erforderlich' });
|
||||
}
|
||||
|
||||
const result = gitService.prepareForGitea(application.local_path, repoUrl, { branch });
|
||||
|
||||
if (result.success) {
|
||||
logger.info(`Repository vorbereitet für Projekt ${projectId}: ${repoUrl}`);
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Vorbereiten des Repositories:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/set-remote/:projectId
|
||||
* Remote für ein Projekt setzen/aktualisieren
|
||||
*/
|
||||
router.post('/set-remote/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { repoUrl } = req.body;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
if (!repoUrl) {
|
||||
return res.status(400).json({ error: 'repoUrl ist erforderlich' });
|
||||
}
|
||||
|
||||
// Prüfe ob Git-Repo existiert
|
||||
if (!gitService.isGitRepository(application.local_path)) {
|
||||
// Initialisiere Repository
|
||||
const initResult = gitService.initRepository(application.local_path);
|
||||
if (!initResult.success) {
|
||||
return res.json(initResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Remote setzen
|
||||
const result = gitService.setRemote(application.local_path, repoUrl);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Setzen des Remotes:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/git/init-push/:projectId
|
||||
* Initialen Push mit Upstream-Tracking
|
||||
*/
|
||||
router.post('/init-push/:projectId', (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.params;
|
||||
const { branch } = req.body;
|
||||
const application = getApplicationForProject(projectId);
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json({ error: 'Keine Anwendung für dieses Projekt konfiguriert' });
|
||||
}
|
||||
|
||||
const currentBranch = branch || 'main';
|
||||
const result = gitService.pushWithUpstream(application.local_path, currentBranch);
|
||||
|
||||
if (result.success) {
|
||||
// Sync-Zeitpunkt aktualisieren
|
||||
const db = getDb();
|
||||
db.prepare('UPDATE applications SET last_sync = CURRENT_TIMESTAMP WHERE project_id = ?').run(projectId);
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim initialen Push:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
160
backend/routes/gitea.js
Normale Datei
160
backend/routes/gitea.js
Normale Datei
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* TASKMATE - Gitea Route
|
||||
* ======================
|
||||
* API-Endpoints für Gitea-Integration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const giteaService = require('../services/giteaService');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* GET /api/gitea/test
|
||||
* Gitea-Verbindung testen
|
||||
*/
|
||||
router.get('/test', async (req, res) => {
|
||||
try {
|
||||
const result = await giteaService.testConnection();
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Testen der Gitea-Verbindung:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
connected: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/gitea/repositories
|
||||
* Alle verfügbaren Repositories auflisten
|
||||
*/
|
||||
router.get('/repositories', async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
|
||||
const result = await giteaService.listRepositories({ page, limit });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Auflisten der Repositories:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/gitea/repositories
|
||||
* Neues Repository erstellen
|
||||
*/
|
||||
router.post('/repositories', async (req, res) => {
|
||||
try {
|
||||
const { name, description, private: isPrivate, autoInit, defaultBranch } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Repository-Name ist erforderlich' });
|
||||
}
|
||||
|
||||
const result = await giteaService.createRepository(name, {
|
||||
description,
|
||||
private: isPrivate !== false,
|
||||
autoInit: autoInit !== false,
|
||||
defaultBranch: defaultBranch || 'main'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
logger.info(`Gitea-Repository erstellt: ${result.repository.fullName}`);
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Repositories:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/gitea/repositories/:owner/:repo
|
||||
* Repository-Details abrufen
|
||||
*/
|
||||
router.get('/repositories/:owner/:repo', async (req, res) => {
|
||||
try {
|
||||
const { owner, repo } = req.params;
|
||||
const result = await giteaService.getRepository(owner, repo);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error(`Fehler beim Abrufen des Repositories ${req.params.owner}/${req.params.repo}:`, error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/gitea/repositories/:owner/:repo
|
||||
* Repository löschen
|
||||
*/
|
||||
router.delete('/repositories/:owner/:repo', async (req, res) => {
|
||||
try {
|
||||
const { owner, repo } = req.params;
|
||||
const result = await giteaService.deleteRepository(owner, repo);
|
||||
|
||||
if (result.success) {
|
||||
logger.info(`Gitea-Repository gelöscht: ${owner}/${repo}`);
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error(`Fehler beim Löschen des Repositories ${req.params.owner}/${req.params.repo}:`, error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/gitea/repositories/:owner/:repo/branches
|
||||
* Branches eines Repositories abrufen
|
||||
*/
|
||||
router.get('/repositories/:owner/:repo/branches', async (req, res) => {
|
||||
try {
|
||||
const { owner, repo } = req.params;
|
||||
const result = await giteaService.getRepositoryBranches(owner, repo);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error(`Fehler beim Abrufen der Branches für ${req.params.owner}/${req.params.repo}:`, error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/gitea/repositories/:owner/:repo/commits
|
||||
* Commits eines Repositories abrufen
|
||||
*/
|
||||
router.get('/repositories/:owner/:repo/commits', async (req, res) => {
|
||||
try {
|
||||
const { owner, repo } = req.params;
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const branch = req.query.branch || '';
|
||||
|
||||
const result = await giteaService.getRepositoryCommits(owner, repo, { page, limit, branch });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error(`Fehler beim Abrufen der Commits für ${req.params.owner}/${req.params.repo}:`, error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/gitea/user
|
||||
* Aktuellen Gitea-Benutzer abrufen
|
||||
*/
|
||||
router.get('/user', async (req, res) => {
|
||||
try {
|
||||
const result = await giteaService.getCurrentUser();
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen des Gitea-Benutzers:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
158
backend/routes/health.js
Normale Datei
158
backend/routes/health.js
Normale Datei
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* TASKMATE - Health Check Routes
|
||||
* ==============================
|
||||
* Server-Status und Health-Check Endpoints
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getDb } = require('../database');
|
||||
const backup = require('../utils/backup');
|
||||
|
||||
/**
|
||||
* GET /api/health
|
||||
* Einfacher Health-Check
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
// Datenbank-Check
|
||||
const db = getDb();
|
||||
db.prepare('SELECT 1').get();
|
||||
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: 'unhealthy',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/health/detailed
|
||||
* Detaillierter Health-Check (mit Auth)
|
||||
*/
|
||||
router.get('/detailed', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
// Datenbank-Statistiken
|
||||
const userCount = db.prepare('SELECT COUNT(*) as count FROM users').get().count;
|
||||
const projectCount = db.prepare('SELECT COUNT(*) as count FROM projects WHERE archived = 0').get().count;
|
||||
const taskCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE archived = 0').get().count;
|
||||
|
||||
// Disk-Space für Uploads
|
||||
const uploadsDir = process.env.UPLOAD_DIR || path.join(__dirname, '..', 'uploads');
|
||||
let uploadsSize = 0;
|
||||
let uploadCount = 0;
|
||||
|
||||
if (fs.existsSync(uploadsDir)) {
|
||||
const getDirectorySize = (dir) => {
|
||||
let size = 0;
|
||||
let count = 0;
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
const subResult = getDirectorySize(filePath);
|
||||
size += subResult.size;
|
||||
count += subResult.count;
|
||||
} else {
|
||||
size += stats.size;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return { size, count };
|
||||
};
|
||||
const result = getDirectorySize(uploadsDir);
|
||||
uploadsSize = result.size;
|
||||
uploadCount = result.count;
|
||||
}
|
||||
|
||||
// Letzte Backups
|
||||
const backups = backup.listBackups().slice(0, 5);
|
||||
|
||||
// Memory Usage
|
||||
const memUsage = process.memoryUsage();
|
||||
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: Math.floor(process.uptime()),
|
||||
database: {
|
||||
users: userCount,
|
||||
projects: projectCount,
|
||||
tasks: taskCount
|
||||
},
|
||||
storage: {
|
||||
uploadCount,
|
||||
uploadsSizeMB: Math.round(uploadsSize / 1024 / 1024 * 100) / 100
|
||||
},
|
||||
backups: backups.map(b => ({
|
||||
name: b.name,
|
||||
sizeMB: Math.round(b.size / 1024 / 1024 * 100) / 100,
|
||||
created: b.created
|
||||
})),
|
||||
memory: {
|
||||
heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
|
||||
heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
|
||||
rssMB: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100
|
||||
},
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: 'unhealthy',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/health/backup
|
||||
* Manuelles Backup auslösen
|
||||
*/
|
||||
router.post('/backup', (req, res) => {
|
||||
try {
|
||||
const backupPath = backup.createBackup();
|
||||
|
||||
if (backupPath) {
|
||||
res.json({
|
||||
message: 'Backup erfolgreich erstellt',
|
||||
path: path.basename(backupPath)
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: 'Backup fehlgeschlagen' });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/health/backups
|
||||
* Liste aller Backups
|
||||
*/
|
||||
router.get('/backups', (req, res) => {
|
||||
try {
|
||||
const backups = backup.listBackups();
|
||||
|
||||
res.json(backups.map(b => ({
|
||||
name: b.name,
|
||||
sizeMB: Math.round(b.size / 1024 / 1024 * 100) / 100,
|
||||
created: b.created
|
||||
})));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
269
backend/routes/import.js
Normale Datei
269
backend/routes/import.js
Normale Datei
@ -0,0 +1,269 @@
|
||||
/**
|
||||
* TASKMATE - Import Routes
|
||||
* ========================
|
||||
* Import von JSON-Backups
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* POST /api/import/project
|
||||
* Projekt aus JSON importieren
|
||||
*/
|
||||
router.post('/project', (req, res) => {
|
||||
try {
|
||||
const { data, overwrite = false } = req.body;
|
||||
|
||||
if (!data || !data.project) {
|
||||
return res.status(400).json({ error: 'Ungültiges Import-Format' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Transaktion starten
|
||||
const importProject = db.transaction(() => {
|
||||
const importData = data;
|
||||
|
||||
// Projekt erstellen
|
||||
const projectResult = db.prepare(`
|
||||
INSERT INTO projects (name, description, created_by)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(
|
||||
importData.project.name + (overwrite ? '' : ' (Import)'),
|
||||
importData.project.description,
|
||||
req.user.id
|
||||
);
|
||||
|
||||
const newProjectId = projectResult.lastInsertRowid;
|
||||
|
||||
// Mapping für alte -> neue IDs
|
||||
const columnMap = new Map();
|
||||
const labelMap = new Map();
|
||||
const taskMap = new Map();
|
||||
|
||||
// Spalten importieren
|
||||
if (importData.columns) {
|
||||
const insertColumn = db.prepare(`
|
||||
INSERT INTO columns (project_id, name, position, color)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
importData.columns.forEach(col => {
|
||||
const result = insertColumn.run(newProjectId, col.name, col.position, col.color);
|
||||
columnMap.set(col.id, result.lastInsertRowid);
|
||||
});
|
||||
}
|
||||
|
||||
// Labels importieren
|
||||
if (importData.labels) {
|
||||
const insertLabel = db.prepare(`
|
||||
INSERT INTO labels (project_id, name, color)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
importData.labels.forEach(label => {
|
||||
const result = insertLabel.run(newProjectId, label.name, label.color);
|
||||
labelMap.set(label.id, result.lastInsertRowid);
|
||||
});
|
||||
}
|
||||
|
||||
// Aufgaben importieren
|
||||
if (importData.tasks) {
|
||||
const insertTask = db.prepare(`
|
||||
INSERT INTO tasks (
|
||||
project_id, column_id, title, description, priority,
|
||||
due_date, time_estimate_min, position, created_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
importData.tasks.forEach(task => {
|
||||
const newColumnId = columnMap.get(task.column_id);
|
||||
if (!newColumnId) return;
|
||||
|
||||
const result = insertTask.run(
|
||||
newProjectId,
|
||||
newColumnId,
|
||||
task.title,
|
||||
task.description,
|
||||
task.priority || 'medium',
|
||||
task.due_date,
|
||||
task.time_estimate_min,
|
||||
task.position,
|
||||
req.user.id
|
||||
);
|
||||
|
||||
taskMap.set(task.id, result.lastInsertRowid);
|
||||
|
||||
// Task-Labels
|
||||
if (task.labels) {
|
||||
const insertTaskLabel = db.prepare(
|
||||
'INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)'
|
||||
);
|
||||
task.labels.forEach(label => {
|
||||
const newLabelId = labelMap.get(label.id);
|
||||
if (newLabelId) {
|
||||
try {
|
||||
insertTaskLabel.run(result.lastInsertRowid, newLabelId);
|
||||
} catch (e) { /* Ignorieren */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Subtasks
|
||||
if (task.subtasks) {
|
||||
const insertSubtask = db.prepare(
|
||||
'INSERT INTO subtasks (task_id, title, completed, position) VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
task.subtasks.forEach(st => {
|
||||
insertSubtask.run(
|
||||
result.lastInsertRowid,
|
||||
st.title,
|
||||
st.completed ? 1 : 0,
|
||||
st.position
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Links
|
||||
if (task.links) {
|
||||
const insertLink = db.prepare(
|
||||
'INSERT INTO links (task_id, title, url, created_by) VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
task.links.forEach(link => {
|
||||
insertLink.run(result.lastInsertRowid, link.title, link.url, req.user.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Vorlagen importieren
|
||||
if (importData.templates) {
|
||||
const insertTemplate = db.prepare(`
|
||||
INSERT INTO task_templates (
|
||||
project_id, name, title_template, description,
|
||||
priority, labels, subtasks, time_estimate_min
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
importData.templates.forEach(tmpl => {
|
||||
// Labels-IDs mappen
|
||||
let newLabels = null;
|
||||
if (tmpl.labels) {
|
||||
const oldLabels = typeof tmpl.labels === 'string' ? JSON.parse(tmpl.labels) : tmpl.labels;
|
||||
const mappedLabels = oldLabels.map(id => labelMap.get(id)).filter(id => id);
|
||||
newLabels = JSON.stringify(mappedLabels);
|
||||
}
|
||||
|
||||
insertTemplate.run(
|
||||
newProjectId,
|
||||
tmpl.name,
|
||||
tmpl.title_template,
|
||||
tmpl.description,
|
||||
tmpl.priority,
|
||||
newLabels,
|
||||
tmpl.subtasks,
|
||||
tmpl.time_estimate_min
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
projectId: newProjectId,
|
||||
columnsImported: columnMap.size,
|
||||
labelsImported: labelMap.size,
|
||||
tasksImported: taskMap.size
|
||||
};
|
||||
});
|
||||
|
||||
const result = importProject();
|
||||
|
||||
logger.info(`Projekt importiert: ID ${result.projectId} (${result.tasksImported} Aufgaben)`);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Import erfolgreich',
|
||||
...result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Import:', { error: error.message });
|
||||
res.status(500).json({ error: 'Import fehlgeschlagen: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/import/validate
|
||||
* Import-Datei validieren
|
||||
*/
|
||||
router.post('/validate', (req, res) => {
|
||||
try {
|
||||
const { data } = req.body;
|
||||
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
if (!data) {
|
||||
errors.push('Keine Daten vorhanden');
|
||||
return res.json({ valid: false, errors, warnings });
|
||||
}
|
||||
|
||||
// Version prüfen
|
||||
if (!data.version) {
|
||||
warnings.push('Keine Versionsangabe gefunden');
|
||||
}
|
||||
|
||||
// Projekt prüfen
|
||||
if (!data.project) {
|
||||
errors.push('Kein Projekt in den Daten gefunden');
|
||||
} else {
|
||||
if (!data.project.name) {
|
||||
errors.push('Projektname fehlt');
|
||||
}
|
||||
}
|
||||
|
||||
// Spalten prüfen
|
||||
if (!data.columns || data.columns.length === 0) {
|
||||
errors.push('Keine Spalten in den Daten gefunden');
|
||||
}
|
||||
|
||||
// Aufgaben prüfen
|
||||
if (data.tasks) {
|
||||
data.tasks.forEach((task, idx) => {
|
||||
if (!task.title) {
|
||||
warnings.push(`Aufgabe ${idx + 1} hat keinen Titel`);
|
||||
}
|
||||
if (!task.column_id) {
|
||||
warnings.push(`Aufgabe "${task.title || idx + 1}" hat keine Spalten-ID`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Statistiken
|
||||
const stats = {
|
||||
projectName: data.project?.name || 'Unbekannt',
|
||||
columns: data.columns?.length || 0,
|
||||
labels: data.labels?.length || 0,
|
||||
tasks: data.tasks?.length || 0,
|
||||
templates: data.templates?.length || 0
|
||||
};
|
||||
|
||||
res.json({
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
stats
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei Import-Validierung:', { error: error.message });
|
||||
res.status(400).json({
|
||||
valid: false,
|
||||
errors: ['Ungültiges JSON-Format'],
|
||||
warnings: []
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
202
backend/routes/labels.js
Normale Datei
202
backend/routes/labels.js
Normale Datei
@ -0,0 +1,202 @@
|
||||
/**
|
||||
* TASKMATE - Label Routes
|
||||
* =======================
|
||||
* CRUD für Labels/Tags
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* GET /api/labels/:projectId
|
||||
* Alle Labels eines Projekts
|
||||
*/
|
||||
router.get('/:projectId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const labels = db.prepare(`
|
||||
SELECT l.*,
|
||||
(SELECT COUNT(*) FROM task_labels tl WHERE tl.label_id = l.id) as task_count
|
||||
FROM labels l
|
||||
WHERE l.project_id = ?
|
||||
ORDER BY l.name
|
||||
`).all(req.params.projectId);
|
||||
|
||||
res.json(labels.map(l => ({
|
||||
id: l.id,
|
||||
projectId: l.project_id,
|
||||
name: l.name,
|
||||
color: l.color,
|
||||
taskCount: l.task_count
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Labels:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/labels
|
||||
* Neues Label erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { projectId, name, color } = req.body;
|
||||
|
||||
// Validierung
|
||||
const errors = [];
|
||||
errors.push(validators.required(projectId, 'Projekt-ID'));
|
||||
errors.push(validators.required(name, 'Name'));
|
||||
errors.push(validators.maxLength(name, 30, 'Name'));
|
||||
errors.push(validators.required(color, 'Farbe'));
|
||||
errors.push(validators.hexColor(color, 'Farbe'));
|
||||
|
||||
const firstError = errors.find(e => e !== null);
|
||||
if (firstError) {
|
||||
return res.status(400).json({ error: firstError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Prüfen ob Label-Name bereits existiert
|
||||
const existing = db.prepare(
|
||||
'SELECT id FROM labels WHERE project_id = ? AND LOWER(name) = LOWER(?)'
|
||||
).get(projectId, name);
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ error: 'Ein Label mit diesem Namen existiert bereits' });
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO labels (project_id, name, color)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(projectId, name, color);
|
||||
|
||||
const label = db.prepare('SELECT * FROM labels WHERE id = ?').get(result.lastInsertRowid);
|
||||
|
||||
logger.info(`Label erstellt: ${name} in Projekt ${projectId}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${projectId}`).emit('label:created', {
|
||||
id: label.id,
|
||||
projectId: label.project_id,
|
||||
name: label.name,
|
||||
color: label.color
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: label.id,
|
||||
projectId: label.project_id,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
taskCount: 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Labels:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/labels/:id
|
||||
* Label aktualisieren
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const labelId = req.params.id;
|
||||
const { name, color } = req.body;
|
||||
|
||||
// Validierung
|
||||
if (name) {
|
||||
const nameError = validators.maxLength(name, 30, 'Name');
|
||||
if (nameError) return res.status(400).json({ error: nameError });
|
||||
}
|
||||
if (color) {
|
||||
const colorError = validators.hexColor(color, 'Farbe');
|
||||
if (colorError) return res.status(400).json({ error: colorError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare('SELECT * FROM labels WHERE id = ?').get(labelId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Label nicht gefunden' });
|
||||
}
|
||||
|
||||
// Prüfen ob neuer Name bereits existiert
|
||||
if (name && name.toLowerCase() !== existing.name.toLowerCase()) {
|
||||
const duplicate = db.prepare(
|
||||
'SELECT id FROM labels WHERE project_id = ? AND LOWER(name) = LOWER(?) AND id != ?'
|
||||
).get(existing.project_id, name, labelId);
|
||||
|
||||
if (duplicate) {
|
||||
return res.status(400).json({ error: 'Ein Label mit diesem Namen existiert bereits' });
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE labels SET
|
||||
name = COALESCE(?, name),
|
||||
color = COALESCE(?, color)
|
||||
WHERE id = ?
|
||||
`).run(name || null, color || null, labelId);
|
||||
|
||||
const label = db.prepare('SELECT * FROM labels WHERE id = ?').get(labelId);
|
||||
|
||||
logger.info(`Label aktualisiert: ${label.name} (ID: ${labelId})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${label.project_id}`).emit('label:updated', {
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: label.color
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: label.id,
|
||||
projectId: label.project_id,
|
||||
name: label.name,
|
||||
color: label.color
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren des Labels:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/labels/:id
|
||||
* Label löschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const labelId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const label = db.prepare('SELECT * FROM labels WHERE id = ?').get(labelId);
|
||||
if (!label) {
|
||||
return res.status(404).json({ error: 'Label nicht gefunden' });
|
||||
}
|
||||
|
||||
// Label wird von task_labels durch CASCADE gelöscht
|
||||
db.prepare('DELETE FROM labels WHERE id = ?').run(labelId);
|
||||
|
||||
logger.info(`Label gelöscht: ${label.name} (ID: ${labelId})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${label.project_id}`).emit('label:deleted', { id: labelId });
|
||||
|
||||
res.json({ message: 'Label gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen des Labels:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
253
backend/routes/links.js
Normale Datei
253
backend/routes/links.js
Normale Datei
@ -0,0 +1,253 @@
|
||||
/**
|
||||
* TASKMATE - Link Routes
|
||||
* ======================
|
||||
* CRUD für Links/URLs
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators, stripHtml } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Link-Icon basierend auf URL
|
||||
*/
|
||||
function getLinkIcon(url) {
|
||||
try {
|
||||
const hostname = new URL(url).hostname.toLowerCase();
|
||||
if (hostname.includes('youtube') || hostname.includes('youtu.be')) return 'youtube';
|
||||
if (hostname.includes('github')) return 'github';
|
||||
if (hostname.includes('gitlab')) return 'gitlab';
|
||||
if (hostname.includes('figma')) return 'figma';
|
||||
if (hostname.includes('drive.google')) return 'google-drive';
|
||||
if (hostname.includes('docs.google')) return 'google-docs';
|
||||
if (hostname.includes('notion')) return 'notion';
|
||||
if (hostname.includes('trello')) return 'trello';
|
||||
if (hostname.includes('slack')) return 'slack';
|
||||
if (hostname.includes('jira') || hostname.includes('atlassian')) return 'jira';
|
||||
return 'link';
|
||||
} catch {
|
||||
return 'link';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/links/:taskId
|
||||
* Alle Links einer Aufgabe
|
||||
*/
|
||||
router.get('/:taskId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const links = db.prepare(`
|
||||
SELECT l.*, u.display_name as creator_name
|
||||
FROM links l
|
||||
LEFT JOIN users u ON l.created_by = u.id
|
||||
WHERE l.task_id = ?
|
||||
ORDER BY l.created_at DESC
|
||||
`).all(req.params.taskId);
|
||||
|
||||
res.json(links.map(l => ({
|
||||
id: l.id,
|
||||
taskId: l.task_id,
|
||||
title: l.title,
|
||||
url: l.url,
|
||||
icon: getLinkIcon(l.url),
|
||||
createdBy: l.created_by,
|
||||
creatorName: l.creator_name,
|
||||
createdAt: l.created_at
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Links:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/links
|
||||
* Neuen Link erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { taskId, title, url } = req.body;
|
||||
|
||||
// Validierung
|
||||
const urlError = validators.required(url, 'URL') || validators.url(url, 'URL');
|
||||
if (urlError) {
|
||||
return res.status(400).json({ error: urlError });
|
||||
}
|
||||
|
||||
if (title) {
|
||||
const titleError = validators.maxLength(title, 100, 'Titel');
|
||||
if (titleError) {
|
||||
return res.status(400).json({ error: titleError });
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Task prüfen
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
const sanitizedTitle = title ? stripHtml(title) : null;
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO links (task_id, title, url, created_by)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(taskId, sanitizedTitle, url, req.user.id);
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(taskId);
|
||||
|
||||
const link = db.prepare(`
|
||||
SELECT l.*, u.display_name as creator_name
|
||||
FROM links l
|
||||
LEFT JOIN users u ON l.created_by = u.id
|
||||
WHERE l.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
logger.info(`Link erstellt: ${url} für Task ${taskId}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('link:created', {
|
||||
taskId,
|
||||
link: {
|
||||
id: link.id,
|
||||
taskId: link.task_id,
|
||||
title: link.title,
|
||||
url: link.url,
|
||||
icon: getLinkIcon(link.url),
|
||||
createdBy: link.created_by,
|
||||
creatorName: link.creator_name,
|
||||
createdAt: link.created_at
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: link.id,
|
||||
taskId: link.task_id,
|
||||
title: link.title,
|
||||
url: link.url,
|
||||
icon: getLinkIcon(link.url),
|
||||
createdBy: link.created_by,
|
||||
creatorName: link.creator_name,
|
||||
createdAt: link.created_at
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Links:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/links/:id
|
||||
* Link aktualisieren
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const linkId = req.params.id;
|
||||
const { title, url } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare('SELECT * FROM links WHERE id = ?').get(linkId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Link nicht gefunden' });
|
||||
}
|
||||
|
||||
// Validierung
|
||||
if (url) {
|
||||
const urlError = validators.url(url, 'URL');
|
||||
if (urlError) return res.status(400).json({ error: urlError });
|
||||
}
|
||||
if (title) {
|
||||
const titleError = validators.maxLength(title, 100, 'Titel');
|
||||
if (titleError) return res.status(400).json({ error: titleError });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE links SET
|
||||
title = ?,
|
||||
url = COALESCE(?, url)
|
||||
WHERE id = ?
|
||||
`).run(title !== undefined ? stripHtml(title) : existing.title, url || null, linkId);
|
||||
|
||||
const link = db.prepare(`
|
||||
SELECT l.*, u.display_name as creator_name
|
||||
FROM links l
|
||||
LEFT JOIN users u ON l.created_by = u.id
|
||||
WHERE l.id = ?
|
||||
`).get(linkId);
|
||||
|
||||
const task = db.prepare('SELECT project_id FROM tasks WHERE id = ?').get(link.task_id);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('link:updated', {
|
||||
taskId: link.task_id,
|
||||
link: {
|
||||
id: link.id,
|
||||
title: link.title,
|
||||
url: link.url,
|
||||
icon: getLinkIcon(link.url)
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: link.id,
|
||||
taskId: link.task_id,
|
||||
title: link.title,
|
||||
url: link.url,
|
||||
icon: getLinkIcon(link.url),
|
||||
createdBy: link.created_by,
|
||||
creatorName: link.creator_name,
|
||||
createdAt: link.created_at
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren des Links:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/links/:id
|
||||
* Link löschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const linkId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const link = db.prepare('SELECT * FROM links WHERE id = ?').get(linkId);
|
||||
if (!link) {
|
||||
return res.status(404).json({ error: 'Link nicht gefunden' });
|
||||
}
|
||||
|
||||
const task = db.prepare('SELECT project_id FROM tasks WHERE id = ?').get(link.task_id);
|
||||
|
||||
db.prepare('DELETE FROM links WHERE id = ?').run(linkId);
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(link.task_id);
|
||||
|
||||
logger.info(`Link gelöscht: ${link.url}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('link:deleted', {
|
||||
taskId: link.task_id,
|
||||
linkId
|
||||
});
|
||||
|
||||
res.json({ message: 'Link gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen des Links:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
134
backend/routes/notifications.js
Normale Datei
134
backend/routes/notifications.js
Normale Datei
@ -0,0 +1,134 @@
|
||||
/**
|
||||
* TASKMATE - Notifications Routes
|
||||
* ================================
|
||||
* API-Endpunkte für das Benachrichtigungssystem
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const notificationService = require('../services/notificationService');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* GET /api/notifications
|
||||
* Alle Benachrichtigungen des Users abrufen
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
|
||||
const notifications = notificationService.getForUser(userId, limit);
|
||||
const unreadCount = notificationService.getUnreadCount(userId);
|
||||
|
||||
res.json({
|
||||
notifications,
|
||||
unreadCount
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Benachrichtigungen:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Abrufen der Benachrichtigungen' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/notifications/count
|
||||
* Ungelesene Anzahl ermitteln
|
||||
*/
|
||||
router.get('/count', (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const count = notificationService.getUnreadCount(userId);
|
||||
|
||||
res.json({ count });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Ermitteln der Anzahl:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Ermitteln der Anzahl' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/notifications/:id/read
|
||||
* Als gelesen markieren
|
||||
*/
|
||||
router.put('/:id/read', (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const notificationId = parseInt(req.params.id);
|
||||
|
||||
const success = notificationService.markAsRead(notificationId, userId);
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Benachrichtigung nicht gefunden' });
|
||||
}
|
||||
|
||||
// Aktualisierte Zählung senden
|
||||
const io = req.app.get('io');
|
||||
const count = notificationService.getUnreadCount(userId);
|
||||
if (io) {
|
||||
io.to(`user:${userId}`).emit('notification:count', { count });
|
||||
}
|
||||
|
||||
res.json({ success: true, unreadCount: count });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Markieren als gelesen:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Markieren als gelesen' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/notifications/read-all
|
||||
* Alle als gelesen markieren
|
||||
*/
|
||||
router.put('/read-all', (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
const count = notificationService.markAllAsRead(userId);
|
||||
|
||||
// Aktualisierte Zählung senden
|
||||
const io = req.app.get('io');
|
||||
if (io) {
|
||||
io.to(`user:${userId}`).emit('notification:count', { count: 0 });
|
||||
}
|
||||
|
||||
res.json({ success: true, markedCount: count, unreadCount: 0 });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Markieren aller als gelesen:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Markieren aller als gelesen' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/notifications/:id
|
||||
* Benachrichtigung löschen (nur nicht-persistente)
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const notificationId = parseInt(req.params.id);
|
||||
|
||||
const success = notificationService.delete(notificationId, userId);
|
||||
|
||||
if (!success) {
|
||||
return res.status(400).json({
|
||||
error: 'Benachrichtigung nicht gefunden oder kann nicht gelöscht werden'
|
||||
});
|
||||
}
|
||||
|
||||
// Aktualisierte Zählung senden
|
||||
const io = req.app.get('io');
|
||||
const count = notificationService.getUnreadCount(userId);
|
||||
if (io) {
|
||||
io.to(`user:${userId}`).emit('notification:count', { count });
|
||||
io.to(`user:${userId}`).emit('notification:deleted', { notificationId });
|
||||
}
|
||||
|
||||
res.json({ success: true, unreadCount: count });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen der Benachrichtigung:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Löschen der Benachrichtigung' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
359
backend/routes/projects.js
Normale Datei
359
backend/routes/projects.js
Normale Datei
@ -0,0 +1,359 @@
|
||||
/**
|
||||
* TASKMATE - Project Routes
|
||||
* =========================
|
||||
* CRUD für Projekte
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* GET /api/projects
|
||||
* Alle Projekte abrufen
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const includeArchived = req.query.archived === 'true';
|
||||
|
||||
let query = `
|
||||
SELECT p.*, u.display_name as creator_name,
|
||||
(SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.archived = 0) as task_count,
|
||||
(SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.archived = 0 AND t.column_id IN
|
||||
(SELECT c.id FROM columns c WHERE c.project_id = p.id ORDER BY c.position DESC LIMIT 1)) as completed_count
|
||||
FROM projects p
|
||||
LEFT JOIN users u ON p.created_by = u.id
|
||||
`;
|
||||
|
||||
if (!includeArchived) {
|
||||
query += ' WHERE p.archived = 0';
|
||||
}
|
||||
|
||||
query += ' ORDER BY p.created_at DESC';
|
||||
|
||||
const projects = db.prepare(query).all();
|
||||
|
||||
res.json(projects.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description,
|
||||
archived: !!p.archived,
|
||||
createdAt: p.created_at,
|
||||
createdBy: p.created_by,
|
||||
creatorName: p.creator_name,
|
||||
taskCount: p.task_count,
|
||||
completedCount: p.completed_count
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Projekte:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/projects/:id
|
||||
* Einzelnes Projekt mit Spalten und Aufgaben
|
||||
*/
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const projectId = req.params.id;
|
||||
|
||||
// Projekt abrufen
|
||||
const project = db.prepare(`
|
||||
SELECT p.*, u.display_name as creator_name
|
||||
FROM projects p
|
||||
LEFT JOIN users u ON p.created_by = u.id
|
||||
WHERE p.id = ?
|
||||
`).get(projectId);
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
||||
}
|
||||
|
||||
// Spalten abrufen
|
||||
const columns = db.prepare(`
|
||||
SELECT * FROM columns WHERE project_id = ? ORDER BY position
|
||||
`).all(projectId);
|
||||
|
||||
// Labels abrufen
|
||||
const labels = db.prepare(`
|
||||
SELECT * FROM labels WHERE project_id = ?
|
||||
`).all(projectId);
|
||||
|
||||
res.json({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
archived: !!project.archived,
|
||||
createdAt: project.created_at,
|
||||
createdBy: project.created_by,
|
||||
creatorName: project.creator_name,
|
||||
columns: columns.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
position: c.position,
|
||||
color: c.color
|
||||
})),
|
||||
labels: labels.map(l => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
color: l.color
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen des Projekts:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/projects
|
||||
* Neues Projekt erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
// Validierung
|
||||
const nameError = validators.required(name, 'Name') ||
|
||||
validators.maxLength(name, 100, 'Name');
|
||||
if (nameError) {
|
||||
return res.status(400).json({ error: nameError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Projekt erstellen
|
||||
const result = db.prepare(`
|
||||
INSERT INTO projects (name, description, created_by)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(name, description || null, req.user.id);
|
||||
|
||||
const projectId = result.lastInsertRowid;
|
||||
|
||||
// Standard-Spalten erstellen
|
||||
const insertColumn = db.prepare(`
|
||||
INSERT INTO columns (project_id, name, position) VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
insertColumn.run(projectId, 'Offen', 0);
|
||||
insertColumn.run(projectId, 'In Arbeit', 1);
|
||||
insertColumn.run(projectId, 'Erledigt', 2);
|
||||
|
||||
// Standard-Labels erstellen
|
||||
const insertLabel = db.prepare(`
|
||||
INSERT INTO labels (project_id, name, color) VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
insertLabel.run(projectId, 'Bug', '#DC2626');
|
||||
insertLabel.run(projectId, 'Feature', '#059669');
|
||||
insertLabel.run(projectId, 'Dokumentation', '#3182CE');
|
||||
|
||||
// Projekt mit Spalten und Labels zurückgeben
|
||||
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
||||
const columns = db.prepare('SELECT * FROM columns WHERE project_id = ? ORDER BY position').all(projectId);
|
||||
const labels = db.prepare('SELECT * FROM labels WHERE project_id = ?').all(projectId);
|
||||
|
||||
logger.info(`Projekt erstellt: ${name} (ID: ${projectId}) von ${req.user.username}`);
|
||||
|
||||
// WebSocket: Andere Clients benachrichtigen
|
||||
const io = req.app.get('io');
|
||||
io.emit('project:created', {
|
||||
id: projectId,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
createdBy: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
archived: false,
|
||||
createdAt: project.created_at,
|
||||
createdBy: project.created_by,
|
||||
columns: columns.map(c => ({ id: c.id, name: c.name, position: c.position, color: c.color })),
|
||||
labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color }))
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Projekts:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/projects/:id
|
||||
* Projekt aktualisieren
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const projectId = req.params.id;
|
||||
const { name, description } = req.body;
|
||||
|
||||
// Validierung
|
||||
if (name) {
|
||||
const nameError = validators.maxLength(name, 100, 'Name');
|
||||
if (nameError) {
|
||||
return res.status(400).json({ error: nameError });
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Prüfen ob Projekt existiert
|
||||
const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
||||
}
|
||||
|
||||
// Aktualisieren
|
||||
db.prepare(`
|
||||
UPDATE projects
|
||||
SET name = COALESCE(?, name), description = COALESCE(?, description)
|
||||
WHERE id = ?
|
||||
`).run(name || null, description !== undefined ? description : null, projectId);
|
||||
|
||||
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
||||
|
||||
logger.info(`Projekt aktualisiert: ${project.name} (ID: ${projectId})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.emit('project:updated', {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
description: project.description
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
archived: !!project.archived,
|
||||
createdAt: project.created_at
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren des Projekts:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/projects/:id/archive
|
||||
* Projekt archivieren/wiederherstellen
|
||||
*/
|
||||
router.put('/:id/archive', (req, res) => {
|
||||
try {
|
||||
const projectId = req.params.id;
|
||||
const { archived } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
||||
}
|
||||
|
||||
db.prepare('UPDATE projects SET archived = ? WHERE id = ?')
|
||||
.run(archived ? 1 : 0, projectId);
|
||||
|
||||
logger.info(`Projekt ${archived ? 'archiviert' : 'wiederhergestellt'}: ${existing.name}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.emit('project:archived', { id: projectId, archived: !!archived });
|
||||
|
||||
res.json({ message: archived ? 'Projekt archiviert' : 'Projekt wiederhergestellt' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Archivieren:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/projects/:id
|
||||
* Projekt löschen
|
||||
* Query param: force=true um alle zugehörigen Aufgaben mitzulöschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const projectId = req.params.id;
|
||||
const forceDelete = req.query.force === 'true';
|
||||
const db = getDb();
|
||||
|
||||
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Projekt nicht gefunden' });
|
||||
}
|
||||
|
||||
// Anzahl der Aufgaben ermitteln
|
||||
const taskCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM tasks WHERE project_id = ?'
|
||||
).get(projectId).count;
|
||||
|
||||
// Ohne force: Prüfen ob noch aktive Aufgaben existieren
|
||||
if (!forceDelete && taskCount > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Projekt enthält noch Aufgaben. Verwende force=true um alles zu löschen.',
|
||||
taskCount: taskCount
|
||||
});
|
||||
}
|
||||
|
||||
// Bei force=true: Explizit alle zugehörigen Daten löschen
|
||||
if (forceDelete && taskCount > 0) {
|
||||
// Alle Task-IDs für das Projekt holen
|
||||
const taskIds = db.prepare('SELECT id FROM tasks WHERE project_id = ?')
|
||||
.all(projectId)
|
||||
.map(t => t.id);
|
||||
|
||||
if (taskIds.length > 0) {
|
||||
const placeholders = taskIds.map(() => '?').join(',');
|
||||
|
||||
// Anhänge löschen
|
||||
db.prepare(`DELETE FROM attachments WHERE task_id IN (${placeholders})`).run(...taskIds);
|
||||
// Kommentare löschen
|
||||
db.prepare(`DELETE FROM comments WHERE task_id IN (${placeholders})`).run(...taskIds);
|
||||
// Task-Labels löschen
|
||||
db.prepare(`DELETE FROM task_labels WHERE task_id IN (${placeholders})`).run(...taskIds);
|
||||
// Task-Assignees löschen (Mehrfachzuweisung)
|
||||
db.prepare(`DELETE FROM task_assignees WHERE task_id IN (${placeholders})`).run(...taskIds);
|
||||
// Unteraufgaben löschen
|
||||
db.prepare(`DELETE FROM subtasks WHERE task_id IN (${placeholders})`).run(...taskIds);
|
||||
// Links löschen
|
||||
db.prepare(`DELETE FROM links WHERE task_id IN (${placeholders})`).run(...taskIds);
|
||||
// Historie löschen
|
||||
db.prepare(`DELETE FROM history WHERE task_id IN (${placeholders})`).run(...taskIds);
|
||||
// Tasks löschen
|
||||
db.prepare(`DELETE FROM tasks WHERE project_id = ?`).run(projectId);
|
||||
}
|
||||
|
||||
logger.info(`${taskCount} Aufgaben gelöscht für Projekt: ${project.name}`);
|
||||
}
|
||||
|
||||
// Labels des Projekts löschen
|
||||
db.prepare('DELETE FROM labels WHERE project_id = ?').run(projectId);
|
||||
|
||||
// Spalten löschen
|
||||
db.prepare('DELETE FROM columns WHERE project_id = ?').run(projectId);
|
||||
|
||||
// Projekt löschen
|
||||
db.prepare('DELETE FROM projects WHERE id = ?').run(projectId);
|
||||
|
||||
logger.info(`Projekt gelöscht: ${project.name} (ID: ${projectId}), ${taskCount} Aufgaben entfernt`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.emit('project:deleted', { id: projectId });
|
||||
|
||||
res.json({ message: 'Projekt gelöscht', deletedTasks: taskCount });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen des Projekts:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
299
backend/routes/proposals.js
Normale Datei
299
backend/routes/proposals.js
Normale Datei
@ -0,0 +1,299 @@
|
||||
/**
|
||||
* TASKMATE - Proposals Routes
|
||||
* ===========================
|
||||
* API-Endpunkte fuer Vorschlaege und Genehmigungen
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const { authenticateToken, requireRegularUser, checkPermission } = require('../middleware/auth');
|
||||
const logger = require('../utils/logger');
|
||||
const notificationService = require('../services/notificationService');
|
||||
|
||||
// Alle Proposals-Routes erfordern Authentifizierung und regulaeren User (kein Admin)
|
||||
router.use(authenticateToken);
|
||||
router.use(requireRegularUser);
|
||||
|
||||
/**
|
||||
* GET /api/proposals - Alle Genehmigungen abrufen (projektbezogen)
|
||||
* Query-Parameter: sort = 'date' | 'alpha', archived = '0' | '1', projectId = number
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const sort = req.query.sort || 'date';
|
||||
const archived = req.query.archived === '1' ? 1 : 0;
|
||||
const projectId = req.query.projectId ? parseInt(req.query.projectId) : null;
|
||||
|
||||
let orderBy;
|
||||
switch (sort) {
|
||||
case 'alpha':
|
||||
orderBy = 'p.title ASC';
|
||||
break;
|
||||
case 'date':
|
||||
default:
|
||||
orderBy = 'p.created_at DESC';
|
||||
break;
|
||||
}
|
||||
|
||||
// Nur Genehmigungen des aktuellen Projekts laden
|
||||
let whereClause = 'p.archived = ?';
|
||||
const params = [archived];
|
||||
|
||||
if (projectId) {
|
||||
whereClause += ' AND p.project_id = ?';
|
||||
params.push(projectId);
|
||||
}
|
||||
|
||||
const proposals = db.prepare(`
|
||||
SELECT
|
||||
p.*,
|
||||
u.display_name as created_by_name,
|
||||
u.color as created_by_color,
|
||||
ua.display_name as approved_by_name,
|
||||
t.title as task_title,
|
||||
t.id as linked_task_id
|
||||
FROM proposals p
|
||||
LEFT JOIN users u ON p.created_by = u.id
|
||||
LEFT JOIN users ua ON p.approved_by = ua.id
|
||||
LEFT JOIN tasks t ON p.task_id = t.id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ${orderBy}
|
||||
`).all(...params);
|
||||
|
||||
res.json(proposals);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Genehmigungen:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Abrufen der Genehmigungen' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/proposals - Neue Genehmigung erstellen (projektbezogen)
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { title, description, taskId, projectId } = req.body;
|
||||
|
||||
if (!title || title.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Titel erforderlich' });
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
return res.status(400).json({ error: 'Projekt erforderlich' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO proposals (title, description, created_by, task_id, project_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(title.trim(), description?.trim() || null, req.user.id, taskId || null, projectId);
|
||||
|
||||
const proposal = db.prepare(`
|
||||
SELECT
|
||||
p.*,
|
||||
u.display_name as created_by_name,
|
||||
u.color as created_by_color,
|
||||
t.title as task_title,
|
||||
t.id as linked_task_id
|
||||
FROM proposals p
|
||||
LEFT JOIN users u ON p.created_by = u.id
|
||||
LEFT JOIN tasks t ON p.task_id = t.id
|
||||
WHERE p.id = ?
|
||||
`).get(result.lastInsertRowid);
|
||||
|
||||
logger.info(`Benutzer ${req.user.username} hat Genehmigung "${title}" erstellt`);
|
||||
|
||||
// Benachrichtigungen an User mit 'genehmigung'-Berechtigung senden (persistent)
|
||||
const io = req.app.get('io');
|
||||
const usersWithPermission = db.prepare(`
|
||||
SELECT id FROM users
|
||||
WHERE role = 'user'
|
||||
AND permissions LIKE '%genehmigung%'
|
||||
AND id != ?
|
||||
`).all(req.user.id);
|
||||
|
||||
usersWithPermission.forEach(user => {
|
||||
notificationService.create(user.id, 'approval:pending', {
|
||||
proposalId: proposal.id,
|
||||
proposalTitle: title.trim(),
|
||||
projectId: projectId,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io, true); // persistent = true
|
||||
});
|
||||
|
||||
res.status(201).json(proposal);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen der Genehmigung:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Erstellen der Genehmigung' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/proposals/:id/approve - Genehmigung erteilen (nur mit Berechtigung)
|
||||
*/
|
||||
router.put('/:id/approve', checkPermission('genehmigung'), (req, res) => {
|
||||
try {
|
||||
const proposalId = parseInt(req.params.id);
|
||||
const { approved } = req.body;
|
||||
const db = getDb();
|
||||
|
||||
// Genehmigung pruefen
|
||||
const proposal = db.prepare('SELECT * FROM proposals WHERE id = ?').get(proposalId);
|
||||
if (!proposal) {
|
||||
return res.status(404).json({ error: 'Genehmigung nicht gefunden' });
|
||||
}
|
||||
|
||||
if (approved) {
|
||||
// Genehmigen
|
||||
db.prepare(`
|
||||
UPDATE proposals
|
||||
SET approved = 1, approved_by = ?, approved_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(req.user.id, proposalId);
|
||||
logger.info(`Benutzer ${req.user.username} hat Genehmigung ${proposalId} erteilt`);
|
||||
} else {
|
||||
// Genehmigung zurueckziehen
|
||||
db.prepare(`
|
||||
UPDATE proposals
|
||||
SET approved = 0, approved_by = NULL, approved_at = NULL
|
||||
WHERE id = ?
|
||||
`).run(proposalId);
|
||||
logger.info(`Benutzer ${req.user.username} hat Genehmigung ${proposalId} zurueckgezogen`);
|
||||
}
|
||||
|
||||
// Aktualisierte Genehmigung zurueckgeben
|
||||
const updatedProposal = db.prepare(`
|
||||
SELECT
|
||||
p.*,
|
||||
u.display_name as created_by_name,
|
||||
u.color as created_by_color,
|
||||
ua.display_name as approved_by_name,
|
||||
t.title as task_title,
|
||||
t.id as linked_task_id
|
||||
FROM proposals p
|
||||
LEFT JOIN users u ON p.created_by = u.id
|
||||
LEFT JOIN users ua ON p.approved_by = ua.id
|
||||
LEFT JOIN tasks t ON p.task_id = t.id
|
||||
WHERE p.id = ?
|
||||
`).get(proposalId);
|
||||
|
||||
// Benachrichtigungen senden
|
||||
const io = req.app.get('io');
|
||||
|
||||
if (approved) {
|
||||
// Ersteller benachrichtigen dass genehmigt wurde
|
||||
if (proposal.created_by !== req.user.id) {
|
||||
notificationService.create(proposal.created_by, 'approval:granted', {
|
||||
proposalId: proposalId,
|
||||
proposalTitle: proposal.title,
|
||||
projectId: proposal.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
|
||||
// Persistente Benachrichtigungen auflösen
|
||||
notificationService.resolvePersistent(proposalId);
|
||||
|
||||
// Aktualisierte Zählung an alle User mit Berechtigung senden
|
||||
const usersWithPermission = db.prepare(`
|
||||
SELECT id FROM users
|
||||
WHERE role = 'user'
|
||||
AND permissions LIKE '%genehmigung%'
|
||||
`).all();
|
||||
|
||||
usersWithPermission.forEach(user => {
|
||||
const count = notificationService.getUnreadCount(user.id);
|
||||
io.to(`user:${user.id}`).emit('notification:count', { count });
|
||||
});
|
||||
}
|
||||
|
||||
res.json(updatedProposal);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Genehmigen:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Genehmigen' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/proposals/:id/archive - Genehmigung archivieren/wiederherstellen (nur mit Berechtigung)
|
||||
*/
|
||||
router.put('/:id/archive', checkPermission('genehmigung'), (req, res) => {
|
||||
try {
|
||||
const proposalId = parseInt(req.params.id);
|
||||
const { archived } = req.body;
|
||||
const db = getDb();
|
||||
|
||||
// Genehmigung pruefen
|
||||
const proposal = db.prepare('SELECT * FROM proposals WHERE id = ?').get(proposalId);
|
||||
if (!proposal) {
|
||||
return res.status(404).json({ error: 'Genehmigung nicht gefunden' });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE proposals
|
||||
SET archived = ?
|
||||
WHERE id = ?
|
||||
`).run(archived ? 1 : 0, proposalId);
|
||||
|
||||
logger.info(`Benutzer ${req.user.username} hat Genehmigung ${proposalId} ${archived ? 'archiviert' : 'wiederhergestellt'}`);
|
||||
|
||||
// Aktualisierte Genehmigung zurueckgeben
|
||||
const updatedProposal = db.prepare(`
|
||||
SELECT
|
||||
p.*,
|
||||
u.display_name as created_by_name,
|
||||
u.color as created_by_color,
|
||||
ua.display_name as approved_by_name,
|
||||
t.title as task_title,
|
||||
t.id as linked_task_id
|
||||
FROM proposals p
|
||||
LEFT JOIN users u ON p.created_by = u.id
|
||||
LEFT JOIN users ua ON p.approved_by = ua.id
|
||||
LEFT JOIN tasks t ON p.task_id = t.id
|
||||
WHERE p.id = ?
|
||||
`).get(proposalId);
|
||||
|
||||
res.json(updatedProposal);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Archivieren:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Archivieren' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/proposals/:id - Eigene Genehmigung loeschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const proposalId = parseInt(req.params.id);
|
||||
const db = getDb();
|
||||
|
||||
// Genehmigung pruefen
|
||||
const proposal = db.prepare('SELECT * FROM proposals WHERE id = ?').get(proposalId);
|
||||
if (!proposal) {
|
||||
return res.status(404).json({ error: 'Genehmigung nicht gefunden' });
|
||||
}
|
||||
|
||||
// Nur eigene Genehmigungen loeschen (oder mit genehmigung-Berechtigung)
|
||||
const permissions = req.user.permissions || [];
|
||||
if (proposal.created_by !== req.user.id && !permissions.includes('genehmigung')) {
|
||||
return res.status(403).json({ error: 'Nur eigene Genehmigungen koennen geloescht werden' });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM proposals WHERE id = ?').run(proposalId);
|
||||
|
||||
logger.info(`Benutzer ${req.user.username} hat Genehmigung ${proposalId} geloescht`);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Loeschen der Genehmigung:', error);
|
||||
res.status(500).json({ error: 'Fehler beim Loeschen der Genehmigung' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
310
backend/routes/stats.js
Normale Datei
310
backend/routes/stats.js
Normale Datei
@ -0,0 +1,310 @@
|
||||
/**
|
||||
* TASKMATE - Stats Routes
|
||||
* =======================
|
||||
* Dashboard-Statistiken
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* GET /api/stats/dashboard
|
||||
* Haupt-Dashboard Statistiken
|
||||
*/
|
||||
router.get('/dashboard', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const { projectId } = req.query;
|
||||
|
||||
let projectFilter = '';
|
||||
const params = [];
|
||||
|
||||
if (projectId) {
|
||||
projectFilter = ' AND t.project_id = ?';
|
||||
params.push(projectId);
|
||||
}
|
||||
|
||||
// Gesamtzahlen
|
||||
const total = db.prepare(`
|
||||
SELECT COUNT(*) as count FROM tasks t
|
||||
WHERE t.archived = 0 ${projectFilter}
|
||||
`).get(...params).count;
|
||||
|
||||
// Offene Aufgaben (erste Spalte jedes Projekts)
|
||||
const open = db.prepare(`
|
||||
SELECT COUNT(*) as count FROM tasks t
|
||||
JOIN columns c ON t.column_id = c.id
|
||||
WHERE t.archived = 0 AND c.position = 0 ${projectFilter}
|
||||
`).get(...params).count;
|
||||
|
||||
// In Arbeit (mittlere Spalten)
|
||||
const inProgress = db.prepare(`
|
||||
SELECT COUNT(*) as count FROM tasks t
|
||||
JOIN columns c ON t.column_id = c.id
|
||||
WHERE t.archived = 0 AND c.position > 0
|
||||
AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id)
|
||||
${projectFilter}
|
||||
`).get(...params).count;
|
||||
|
||||
// Erledigt (letzte Spalte)
|
||||
const completed = db.prepare(`
|
||||
SELECT COUNT(*) as count FROM tasks t
|
||||
JOIN columns c ON t.column_id = c.id
|
||||
WHERE t.archived = 0
|
||||
AND c.position = (SELECT MAX(position) FROM columns WHERE project_id = c.project_id)
|
||||
${projectFilter}
|
||||
`).get(...params).count;
|
||||
|
||||
// Überfällig
|
||||
const overdue = db.prepare(`
|
||||
SELECT COUNT(*) as count FROM tasks t
|
||||
JOIN columns c ON t.column_id = c.id
|
||||
WHERE t.archived = 0
|
||||
AND t.due_date < date('now')
|
||||
AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id)
|
||||
${projectFilter}
|
||||
`).get(...params).count;
|
||||
|
||||
// Heute fällig
|
||||
const dueToday = db.prepare(`
|
||||
SELECT t.id, t.title, t.priority, t.assigned_to,
|
||||
u.display_name as assigned_name, u.color as assigned_color
|
||||
FROM tasks t
|
||||
LEFT JOIN users u ON t.assigned_to = u.id
|
||||
JOIN columns c ON t.column_id = c.id
|
||||
WHERE t.archived = 0
|
||||
AND t.due_date = date('now')
|
||||
AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id)
|
||||
${projectFilter}
|
||||
ORDER BY t.priority DESC
|
||||
LIMIT 10
|
||||
`).all(...params);
|
||||
|
||||
// Bald fällig (nächste 7 Tage)
|
||||
const dueSoon = db.prepare(`
|
||||
SELECT COUNT(*) as count FROM tasks t
|
||||
JOIN columns c ON t.column_id = c.id
|
||||
WHERE t.archived = 0
|
||||
AND t.due_date BETWEEN date('now', '+1 day') AND date('now', '+7 days')
|
||||
AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id)
|
||||
${projectFilter}
|
||||
`).get(...params).count;
|
||||
|
||||
res.json({
|
||||
total,
|
||||
open,
|
||||
inProgress,
|
||||
completed,
|
||||
overdue,
|
||||
dueSoon,
|
||||
dueToday: dueToday.map(t => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
priority: t.priority,
|
||||
assignedTo: t.assigned_to,
|
||||
assignedName: t.assigned_name,
|
||||
assignedColor: t.assigned_color
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei Dashboard-Stats:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/stats/completed-per-week
|
||||
* Erledigte Aufgaben pro Woche
|
||||
*/
|
||||
router.get('/completed-per-week', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const { projectId, weeks = 8 } = req.query;
|
||||
|
||||
let projectFilter = '';
|
||||
const params = [parseInt(weeks)];
|
||||
|
||||
if (projectId) {
|
||||
projectFilter = ' AND h.task_id IN (SELECT id FROM tasks WHERE project_id = ?)';
|
||||
params.push(projectId);
|
||||
}
|
||||
|
||||
// Erledigte Aufgaben pro Kalenderwoche
|
||||
const stats = db.prepare(`
|
||||
SELECT
|
||||
strftime('%Y-%W', h.timestamp) as week,
|
||||
COUNT(DISTINCT h.task_id) as count
|
||||
FROM history h
|
||||
WHERE h.action = 'moved'
|
||||
AND h.new_value IN (
|
||||
SELECT name FROM columns c
|
||||
WHERE c.position = (SELECT MAX(position) FROM columns WHERE project_id = c.project_id)
|
||||
)
|
||||
AND h.timestamp >= date('now', '-' || ? || ' weeks')
|
||||
${projectFilter}
|
||||
GROUP BY week
|
||||
ORDER BY week DESC
|
||||
`).all(...params);
|
||||
|
||||
// Letzten X Wochen mit 0 auffüllen
|
||||
const result = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let i = 0; i < parseInt(weeks); i++) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - (i * 7));
|
||||
const year = date.getFullYear();
|
||||
const week = getWeekNumber(date);
|
||||
const weekKey = `${year}-${week.toString().padStart(2, '0')}`;
|
||||
|
||||
const found = stats.find(s => s.week === weekKey);
|
||||
result.unshift({
|
||||
week: weekKey,
|
||||
label: `KW${week}`,
|
||||
count: found ? found.count : 0
|
||||
});
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei Completed-per-Week Stats:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/stats/time-per-project
|
||||
* Geschätzte Zeit pro Projekt
|
||||
*/
|
||||
router.get('/time-per-project', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
const stats = db.prepare(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
COALESCE(SUM(t.time_estimate_min), 0) as total_minutes,
|
||||
COUNT(t.id) as task_count
|
||||
FROM projects p
|
||||
LEFT JOIN tasks t ON p.id = t.project_id AND t.archived = 0
|
||||
WHERE p.archived = 0
|
||||
GROUP BY p.id
|
||||
ORDER BY total_minutes DESC
|
||||
`).all();
|
||||
|
||||
res.json(stats.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
totalMinutes: s.total_minutes,
|
||||
totalHours: Math.round(s.total_minutes / 60 * 10) / 10,
|
||||
taskCount: s.task_count
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei Time-per-Project Stats:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/stats/user-activity
|
||||
* Aktivität pro Benutzer
|
||||
*/
|
||||
router.get('/user-activity', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const { days = 30 } = req.query;
|
||||
|
||||
const stats = db.prepare(`
|
||||
SELECT
|
||||
u.id,
|
||||
u.display_name,
|
||||
u.color,
|
||||
COUNT(DISTINCT CASE WHEN h.action = 'created' THEN h.task_id END) as tasks_created,
|
||||
COUNT(DISTINCT CASE WHEN h.action = 'moved' THEN h.task_id END) as tasks_moved,
|
||||
COUNT(DISTINCT CASE WHEN h.action = 'commented' THEN h.id END) as comments,
|
||||
COUNT(h.id) as total_actions
|
||||
FROM users u
|
||||
LEFT JOIN history h ON u.id = h.user_id AND h.timestamp >= date('now', '-' || ? || ' days')
|
||||
GROUP BY u.id
|
||||
ORDER BY total_actions DESC
|
||||
`).all(parseInt(days));
|
||||
|
||||
res.json(stats.map(s => ({
|
||||
id: s.id,
|
||||
displayName: s.display_name,
|
||||
color: s.color,
|
||||
tasksCreated: s.tasks_created,
|
||||
tasksMoved: s.tasks_moved,
|
||||
comments: s.comments,
|
||||
totalActions: s.total_actions
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei User-Activity Stats:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/stats/calendar
|
||||
* Aufgaben nach Datum (für Kalender)
|
||||
*/
|
||||
router.get('/calendar', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const { projectId, month, year } = req.query;
|
||||
|
||||
const currentYear = year || new Date().getFullYear();
|
||||
const currentMonth = month || (new Date().getMonth() + 1);
|
||||
|
||||
// Start und Ende des Monats
|
||||
const startDate = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-01`;
|
||||
const endDate = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-31`;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
t.due_date,
|
||||
COUNT(*) as count,
|
||||
SUM(CASE WHEN t.due_date < date('now') AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) THEN 1 ELSE 0 END) as overdue_count
|
||||
FROM tasks t
|
||||
JOIN columns c ON t.column_id = c.id
|
||||
WHERE t.archived = 0
|
||||
AND t.due_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const params = [startDate, endDate];
|
||||
|
||||
if (projectId) {
|
||||
query += ' AND t.project_id = ?';
|
||||
params.push(projectId);
|
||||
}
|
||||
|
||||
query += ' GROUP BY t.due_date ORDER BY t.due_date';
|
||||
|
||||
const stats = db.prepare(query).all(...params);
|
||||
|
||||
res.json(stats.map(s => ({
|
||||
date: s.due_date,
|
||||
count: s.count,
|
||||
overdueCount: s.overdue_count
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei Calendar Stats:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Kalenderwoche berechnen
|
||||
*/
|
||||
function getWeekNumber(date) {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
279
backend/routes/subtasks.js
Normale Datei
279
backend/routes/subtasks.js
Normale Datei
@ -0,0 +1,279 @@
|
||||
/**
|
||||
* TASKMATE - Subtask Routes
|
||||
* =========================
|
||||
* CRUD für Unteraufgaben/Checkliste
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* GET /api/subtasks/:taskId
|
||||
* Alle Unteraufgaben einer Aufgabe
|
||||
*/
|
||||
router.get('/:taskId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const subtasks = db.prepare(`
|
||||
SELECT * FROM subtasks WHERE task_id = ? ORDER BY position
|
||||
`).all(req.params.taskId);
|
||||
|
||||
res.json(subtasks.map(s => ({
|
||||
id: s.id,
|
||||
taskId: s.task_id,
|
||||
title: s.title,
|
||||
completed: !!s.completed,
|
||||
position: s.position
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Subtasks:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/subtasks
|
||||
* Neue Unteraufgabe erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const { taskId, title } = req.body;
|
||||
|
||||
// Validierung
|
||||
const titleError = validators.required(title, 'Titel') ||
|
||||
validators.maxLength(title, 200, 'Titel');
|
||||
if (titleError) {
|
||||
return res.status(400).json({ error: titleError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Task prüfen
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
// Höchste Position ermitteln
|
||||
const maxPos = db.prepare(
|
||||
'SELECT COALESCE(MAX(position), -1) as max FROM subtasks WHERE task_id = ?'
|
||||
).get(taskId).max;
|
||||
|
||||
// Subtask erstellen
|
||||
const result = db.prepare(`
|
||||
INSERT INTO subtasks (task_id, title, position)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(taskId, title, maxPos + 1);
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(taskId);
|
||||
|
||||
const subtask = db.prepare('SELECT * FROM subtasks WHERE id = ?').get(result.lastInsertRowid);
|
||||
|
||||
logger.info(`Subtask erstellt: ${title} in Task ${taskId}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('subtask:created', {
|
||||
taskId,
|
||||
subtask: {
|
||||
id: subtask.id,
|
||||
taskId: subtask.task_id,
|
||||
title: subtask.title,
|
||||
completed: false,
|
||||
position: subtask.position
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: subtask.id,
|
||||
taskId: subtask.task_id,
|
||||
title: subtask.title,
|
||||
completed: false,
|
||||
position: subtask.position
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen des Subtasks:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/subtasks/:id
|
||||
* Unteraufgabe aktualisieren
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const subtaskId = req.params.id;
|
||||
const { title, completed } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const subtask = db.prepare('SELECT * FROM subtasks WHERE id = ?').get(subtaskId);
|
||||
if (!subtask) {
|
||||
return res.status(404).json({ error: 'Unteraufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
// Validierung
|
||||
if (title) {
|
||||
const titleError = validators.maxLength(title, 200, 'Titel');
|
||||
if (titleError) {
|
||||
return res.status(400).json({ error: titleError });
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE subtasks SET
|
||||
title = COALESCE(?, title),
|
||||
completed = COALESCE(?, completed)
|
||||
WHERE id = ?
|
||||
`).run(title || null, completed !== undefined ? (completed ? 1 : 0) : null, subtaskId);
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(subtask.task_id);
|
||||
|
||||
const updated = db.prepare('SELECT * FROM subtasks WHERE id = ?').get(subtaskId);
|
||||
const task = db.prepare('SELECT project_id FROM tasks WHERE id = ?').get(subtask.task_id);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('subtask:updated', {
|
||||
taskId: subtask.task_id,
|
||||
subtask: {
|
||||
id: updated.id,
|
||||
taskId: updated.task_id,
|
||||
title: updated.title,
|
||||
completed: !!updated.completed,
|
||||
position: updated.position
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: updated.id,
|
||||
taskId: updated.task_id,
|
||||
title: updated.title,
|
||||
completed: !!updated.completed,
|
||||
position: updated.position
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren des Subtasks:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/subtasks/:id/position
|
||||
* Unteraufgabe-Position ändern
|
||||
*/
|
||||
router.put('/:id/position', (req, res) => {
|
||||
try {
|
||||
const subtaskId = req.params.id;
|
||||
const { newPosition } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const subtask = db.prepare('SELECT * FROM subtasks WHERE id = ?').get(subtaskId);
|
||||
if (!subtask) {
|
||||
return res.status(404).json({ error: 'Unteraufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
const oldPosition = subtask.position;
|
||||
const taskId = subtask.task_id;
|
||||
|
||||
if (newPosition > oldPosition) {
|
||||
db.prepare(`
|
||||
UPDATE subtasks SET position = position - 1
|
||||
WHERE task_id = ? AND position > ? AND position <= ?
|
||||
`).run(taskId, oldPosition, newPosition);
|
||||
} else if (newPosition < oldPosition) {
|
||||
db.prepare(`
|
||||
UPDATE subtasks SET position = position + 1
|
||||
WHERE task_id = ? AND position >= ? AND position < ?
|
||||
`).run(taskId, newPosition, oldPosition);
|
||||
}
|
||||
|
||||
db.prepare('UPDATE subtasks SET position = ? WHERE id = ?').run(newPosition, subtaskId);
|
||||
|
||||
const subtasks = db.prepare(
|
||||
'SELECT * FROM subtasks WHERE task_id = ? ORDER BY position'
|
||||
).all(taskId);
|
||||
|
||||
const task = db.prepare('SELECT project_id FROM tasks WHERE id = ?').get(taskId);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('subtasks:reordered', {
|
||||
taskId,
|
||||
subtasks: subtasks.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
completed: !!s.completed,
|
||||
position: s.position
|
||||
}))
|
||||
});
|
||||
|
||||
res.json({
|
||||
subtasks: subtasks.map(s => ({
|
||||
id: s.id,
|
||||
taskId: s.task_id,
|
||||
title: s.title,
|
||||
completed: !!s.completed,
|
||||
position: s.position
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Verschieben des Subtasks:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/subtasks/:id
|
||||
* Unteraufgabe löschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const subtaskId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const subtask = db.prepare('SELECT * FROM subtasks WHERE id = ?').get(subtaskId);
|
||||
if (!subtask) {
|
||||
return res.status(404).json({ error: 'Unteraufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
const taskId = subtask.task_id;
|
||||
|
||||
db.prepare('DELETE FROM subtasks WHERE id = ?').run(subtaskId);
|
||||
|
||||
// Positionen neu nummerieren
|
||||
const remaining = db.prepare(
|
||||
'SELECT id FROM subtasks WHERE task_id = ? ORDER BY position'
|
||||
).all(taskId);
|
||||
|
||||
remaining.forEach((s, idx) => {
|
||||
db.prepare('UPDATE subtasks SET position = ? WHERE id = ?').run(idx, s.id);
|
||||
});
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(taskId);
|
||||
|
||||
const task = db.prepare('SELECT project_id FROM tasks WHERE id = ?').get(taskId);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('subtask:deleted', {
|
||||
taskId,
|
||||
subtaskId
|
||||
});
|
||||
|
||||
res.json({ message: 'Unteraufgabe gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen des Subtasks:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
899
backend/routes/tasks.js
Normale Datei
899
backend/routes/tasks.js
Normale Datei
@ -0,0 +1,899 @@
|
||||
/**
|
||||
* TASKMATE - Task Routes
|
||||
* ======================
|
||||
* CRUD für Aufgaben
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
const notificationService = require('../services/notificationService');
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Historie-Eintrag erstellen
|
||||
*/
|
||||
function addHistory(db, taskId, userId, action, fieldChanged = null, oldValue = null, newValue = null) {
|
||||
db.prepare(`
|
||||
INSERT INTO history (task_id, user_id, action, field_changed, old_value, new_value)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(taskId, userId, action, fieldChanged, oldValue, newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Vollständige Task-Daten laden
|
||||
*/
|
||||
function getFullTask(db, taskId) {
|
||||
const task = db.prepare(`
|
||||
SELECT t.*,
|
||||
c.username as creator_name
|
||||
FROM tasks t
|
||||
LEFT JOIN users c ON t.created_by = c.id
|
||||
WHERE t.id = ?
|
||||
`).get(taskId);
|
||||
|
||||
if (!task) return null;
|
||||
|
||||
// Zugewiesene Mitarbeiter laden (Mehrfachzuweisung)
|
||||
const assignees = db.prepare(`
|
||||
SELECT u.id, u.username, u.display_name, u.color
|
||||
FROM task_assignees ta
|
||||
JOIN users u ON ta.user_id = u.id
|
||||
WHERE ta.task_id = ?
|
||||
ORDER BY u.username
|
||||
`).all(taskId);
|
||||
|
||||
// Labels laden
|
||||
const labels = db.prepare(`
|
||||
SELECT l.* FROM labels l
|
||||
JOIN task_labels tl ON l.id = tl.label_id
|
||||
WHERE tl.task_id = ?
|
||||
`).all(taskId);
|
||||
|
||||
// Subtasks laden
|
||||
const subtasks = db.prepare(`
|
||||
SELECT * FROM subtasks WHERE task_id = ? ORDER BY position
|
||||
`).all(taskId);
|
||||
|
||||
// Anhänge zählen
|
||||
const attachmentCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM attachments WHERE task_id = ?'
|
||||
).get(taskId).count;
|
||||
|
||||
// Links zählen
|
||||
const linkCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM links WHERE task_id = ?'
|
||||
).get(taskId).count;
|
||||
|
||||
// Kommentare zählen
|
||||
const commentCount = db.prepare(
|
||||
'SELECT COUNT(*) as count FROM comments WHERE task_id = ?'
|
||||
).get(taskId).count;
|
||||
|
||||
// Verknüpfte Genehmigungen laden
|
||||
const proposals = db.prepare(`
|
||||
SELECT p.id, p.title, p.approved, p.approved_by,
|
||||
u.display_name as approved_by_name
|
||||
FROM proposals p
|
||||
LEFT JOIN users u ON p.approved_by = u.id
|
||||
WHERE p.task_id = ? AND p.archived = 0
|
||||
ORDER BY p.created_at DESC
|
||||
`).all(taskId);
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
projectId: task.project_id,
|
||||
columnId: task.column_id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
priority: task.priority,
|
||||
startDate: task.start_date,
|
||||
dueDate: task.due_date,
|
||||
// Neues Format: Array von Mitarbeitern
|
||||
assignees: assignees.map(a => ({
|
||||
id: a.id,
|
||||
username: a.username,
|
||||
display_name: a.display_name,
|
||||
color: a.color
|
||||
})),
|
||||
// Rückwärtskompatibilität: assignedTo als erster Mitarbeiter (falls vorhanden)
|
||||
assignedTo: assignees.length > 0 ? assignees[0].id : null,
|
||||
assignedName: assignees.length > 0 ? assignees[0].username : null,
|
||||
assignedColor: assignees.length > 0 ? assignees[0].color : null,
|
||||
timeEstimateMin: task.time_estimate_min,
|
||||
dependsOn: task.depends_on,
|
||||
position: task.position,
|
||||
archived: !!task.archived,
|
||||
createdAt: task.created_at,
|
||||
createdBy: task.created_by,
|
||||
creatorName: task.creator_name,
|
||||
updatedAt: task.updated_at,
|
||||
labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color })),
|
||||
subtasks: subtasks.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
completed: !!s.completed,
|
||||
position: s.position
|
||||
})),
|
||||
subtaskProgress: {
|
||||
total: subtasks.length,
|
||||
completed: subtasks.filter(s => s.completed).length
|
||||
},
|
||||
attachmentCount,
|
||||
linkCount,
|
||||
commentCount,
|
||||
proposals: proposals.map(p => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
approved: !!p.approved,
|
||||
approvedByName: p.approved_by_name
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/tasks/all
|
||||
* Alle aktiven Aufgaben (nicht archiviert) fuer Auswahl in Vorschlaegen
|
||||
*/
|
||||
router.get('/all', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
const tasks = db.prepare(`
|
||||
SELECT t.id, t.title, t.project_id, p.name as project_name
|
||||
FROM tasks t
|
||||
LEFT JOIN projects p ON t.project_id = p.id
|
||||
WHERE t.archived = 0
|
||||
ORDER BY p.name, t.title
|
||||
`).all();
|
||||
|
||||
res.json(tasks);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen aller Aufgaben:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/tasks/project/:projectId
|
||||
* Alle Aufgaben eines Projekts
|
||||
*/
|
||||
router.get('/project/:projectId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const projectId = req.params.projectId;
|
||||
const includeArchived = req.query.archived === 'true';
|
||||
|
||||
let query = `
|
||||
SELECT t.id FROM tasks t
|
||||
WHERE t.project_id = ?
|
||||
`;
|
||||
|
||||
if (!includeArchived) {
|
||||
query += ' AND t.archived = 0';
|
||||
}
|
||||
|
||||
query += ' ORDER BY t.column_id, t.position';
|
||||
|
||||
const taskIds = db.prepare(query).all(projectId);
|
||||
const tasks = taskIds.map(t => getFullTask(db, t.id));
|
||||
|
||||
res.json(tasks);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Aufgaben:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/tasks/search
|
||||
* Aufgaben suchen - durchsucht auch Subtasks, Links, Anhänge und Kommentare
|
||||
* WICHTIG: Diese Route MUSS vor /:id definiert werden!
|
||||
*/
|
||||
router.get('/search', (req, res) => {
|
||||
try {
|
||||
const { q, projectId, assignedTo, priority, dueBefore, dueAfter, labels, archived } = req.query;
|
||||
|
||||
const db = getDb();
|
||||
const params = [];
|
||||
|
||||
// Basis-Query mit LEFT JOINs für tiefe Suche
|
||||
let query = `
|
||||
SELECT DISTINCT t.id FROM tasks t
|
||||
LEFT JOIN subtasks s ON t.id = s.task_id
|
||||
LEFT JOIN links l ON t.id = l.task_id
|
||||
LEFT JOIN attachments a ON t.id = a.task_id
|
||||
LEFT JOIN comments c ON t.id = c.task_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
if (projectId) {
|
||||
query += ' AND t.project_id = ?';
|
||||
params.push(projectId);
|
||||
}
|
||||
|
||||
// Erweiterte Textsuche: Titel, Beschreibung, Subtasks, Links, Anhänge, Kommentare
|
||||
if (q) {
|
||||
const searchTerm = `%${q}%`;
|
||||
query += ` AND (
|
||||
t.title LIKE ?
|
||||
OR t.description LIKE ?
|
||||
OR s.title LIKE ?
|
||||
OR l.url LIKE ?
|
||||
OR l.title LIKE ?
|
||||
OR a.original_name LIKE ?
|
||||
OR c.content LIKE ?
|
||||
)`;
|
||||
params.push(searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm);
|
||||
}
|
||||
|
||||
if (assignedTo) {
|
||||
query += ' AND t.assigned_to = ?';
|
||||
params.push(assignedTo);
|
||||
}
|
||||
|
||||
if (priority) {
|
||||
query += ' AND t.priority = ?';
|
||||
params.push(priority);
|
||||
}
|
||||
|
||||
if (dueBefore) {
|
||||
query += ' AND t.due_date <= ?';
|
||||
params.push(dueBefore);
|
||||
}
|
||||
|
||||
if (dueAfter) {
|
||||
query += ' AND t.due_date >= ?';
|
||||
params.push(dueAfter);
|
||||
}
|
||||
|
||||
if (archived !== 'true') {
|
||||
query += ' AND t.archived = 0';
|
||||
}
|
||||
|
||||
if (labels) {
|
||||
const labelIds = labels.split(',').map(id => parseInt(id)).filter(id => !isNaN(id));
|
||||
if (labelIds.length > 0) {
|
||||
query += ` AND t.id IN (
|
||||
SELECT task_id FROM task_labels WHERE label_id IN (${labelIds.map(() => '?').join(',')})
|
||||
)`;
|
||||
params.push(...labelIds);
|
||||
}
|
||||
}
|
||||
|
||||
query += ' ORDER BY t.due_date ASC, t.priority DESC, t.updated_at DESC LIMIT 100';
|
||||
|
||||
const taskIds = db.prepare(query).all(...params);
|
||||
const tasks = taskIds.map(t => getFullTask(db, t.id));
|
||||
|
||||
logger.info(`Suche nach "${q}" in Projekt ${projectId}: ${tasks.length} Treffer`);
|
||||
|
||||
res.json(tasks);
|
||||
} catch (error) {
|
||||
logger.error('Fehler bei der Suche:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/tasks/:id
|
||||
* Einzelne Aufgabe mit allen Details
|
||||
*/
|
||||
router.get('/:id', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const task = getFullTask(db, req.params.id);
|
||||
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
// Historie laden
|
||||
const history = db.prepare(`
|
||||
SELECT h.*, u.display_name, u.color
|
||||
FROM history h
|
||||
JOIN users u ON h.user_id = u.id
|
||||
WHERE h.task_id = ?
|
||||
ORDER BY h.timestamp DESC
|
||||
LIMIT 50
|
||||
`).all(req.params.id);
|
||||
|
||||
task.history = history.map(h => ({
|
||||
id: h.id,
|
||||
action: h.action,
|
||||
fieldChanged: h.field_changed,
|
||||
oldValue: h.old_value,
|
||||
newValue: h.new_value,
|
||||
timestamp: h.timestamp,
|
||||
userName: h.display_name,
|
||||
userColor: h.color
|
||||
}));
|
||||
|
||||
res.json(task);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Aufgabe:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/tasks
|
||||
* Neue Aufgabe erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
projectId, columnId, title, description, priority,
|
||||
startDate, dueDate, assignees, assignedTo, timeEstimateMin, dependsOn, labels
|
||||
} = req.body;
|
||||
|
||||
// Validierung
|
||||
const errors = [];
|
||||
errors.push(validators.required(projectId, 'Projekt-ID'));
|
||||
errors.push(validators.required(columnId, 'Spalten-ID'));
|
||||
errors.push(validators.required(title, 'Titel'));
|
||||
errors.push(validators.maxLength(title, 200, 'Titel'));
|
||||
if (priority) errors.push(validators.enum(priority, ['low', 'medium', 'high'], 'Priorität'));
|
||||
if (startDate) errors.push(validators.date(startDate, 'Startdatum'));
|
||||
if (dueDate) errors.push(validators.date(dueDate, 'Fälligkeitsdatum'));
|
||||
if (timeEstimateMin) errors.push(validators.positiveInteger(timeEstimateMin, 'Zeitschätzung'));
|
||||
|
||||
const firstError = errors.find(e => e !== null);
|
||||
if (firstError) {
|
||||
return res.status(400).json({ error: firstError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
// Höchste Position in der Spalte ermitteln
|
||||
const maxPos = db.prepare(
|
||||
'SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE column_id = ?'
|
||||
).get(columnId).max;
|
||||
|
||||
// Aufgabe erstellen (ohne assigned_to, wird über task_assignees gemacht)
|
||||
const result = db.prepare(`
|
||||
INSERT INTO tasks (
|
||||
project_id, column_id, title, description, priority,
|
||||
start_date, due_date, time_estimate_min, depends_on, position, created_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
projectId, columnId, title, description || null, priority || 'medium',
|
||||
startDate || null, dueDate || null, timeEstimateMin || null,
|
||||
dependsOn || null, maxPos + 1, req.user.id
|
||||
);
|
||||
|
||||
const taskId = result.lastInsertRowid;
|
||||
|
||||
// Mitarbeiter zuweisen (Mehrfachzuweisung)
|
||||
const assigneeIds = assignees && Array.isArray(assignees) ? assignees :
|
||||
(assignedTo ? [assignedTo] : []);
|
||||
if (assigneeIds.length > 0) {
|
||||
const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)');
|
||||
assigneeIds.forEach(userId => {
|
||||
try {
|
||||
insertAssignee.run(taskId, userId);
|
||||
} catch (e) {
|
||||
// User existiert nicht oder bereits zugewiesen
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Labels zuweisen
|
||||
if (labels && Array.isArray(labels)) {
|
||||
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
|
||||
labels.forEach(labelId => {
|
||||
try {
|
||||
insertLabel.run(taskId, labelId);
|
||||
} catch (e) {
|
||||
// Label existiert nicht, ignorieren
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Historie
|
||||
addHistory(db, taskId, req.user.id, 'created');
|
||||
|
||||
const task = getFullTask(db, taskId);
|
||||
|
||||
logger.info(`Aufgabe erstellt: ${title} (ID: ${taskId}) von ${req.user.username}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${projectId}`).emit('task:created', task);
|
||||
|
||||
// Benachrichtigungen an zugewiesene Mitarbeiter (außer Ersteller)
|
||||
if (assigneeIds.length > 0) {
|
||||
assigneeIds.forEach(assigneeId => {
|
||||
if (assigneeId !== req.user.id) {
|
||||
notificationService.create(assigneeId, 'task:assigned', {
|
||||
taskId: taskId,
|
||||
taskTitle: title,
|
||||
projectId: projectId,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json(task);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen der Aufgabe:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/tasks/:id
|
||||
* Aufgabe aktualisieren
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const taskId = req.params.id;
|
||||
const {
|
||||
title, description, priority, columnId, startDate, dueDate, assignees, assignedTo,
|
||||
timeEstimateMin, dependsOn, labels
|
||||
} = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
// Validierung
|
||||
if (title) {
|
||||
const titleError = validators.maxLength(title, 200, 'Titel');
|
||||
if (titleError) return res.status(400).json({ error: titleError });
|
||||
}
|
||||
if (priority) {
|
||||
const prioError = validators.enum(priority, ['low', 'medium', 'high'], 'Priorität');
|
||||
if (prioError) return res.status(400).json({ error: prioError });
|
||||
}
|
||||
if (startDate) {
|
||||
const startDateError = validators.date(startDate, 'Startdatum');
|
||||
if (startDateError) return res.status(400).json({ error: startDateError });
|
||||
}
|
||||
if (dueDate) {
|
||||
const dateError = validators.date(dueDate, 'Fälligkeitsdatum');
|
||||
if (dateError) return res.status(400).json({ error: dateError });
|
||||
}
|
||||
|
||||
// Änderungen tracken für Historie
|
||||
const changes = [];
|
||||
if (title !== undefined && title !== existing.title) {
|
||||
changes.push({ field: 'title', old: existing.title, new: title });
|
||||
}
|
||||
if (description !== undefined && description !== existing.description) {
|
||||
changes.push({ field: 'description', old: existing.description, new: description });
|
||||
}
|
||||
if (priority !== undefined && priority !== existing.priority) {
|
||||
changes.push({ field: 'priority', old: existing.priority, new: priority });
|
||||
}
|
||||
if (columnId !== undefined && columnId !== existing.column_id) {
|
||||
const oldColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(existing.column_id);
|
||||
const newColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(columnId);
|
||||
changes.push({ field: 'column', old: oldColumn?.name, new: newColumn?.name });
|
||||
}
|
||||
if (startDate !== undefined && startDate !== existing.start_date) {
|
||||
changes.push({ field: 'start_date', old: existing.start_date, new: startDate });
|
||||
}
|
||||
if (dueDate !== undefined && dueDate !== existing.due_date) {
|
||||
changes.push({ field: 'due_date', old: existing.due_date, new: dueDate });
|
||||
}
|
||||
if (timeEstimateMin !== undefined && timeEstimateMin !== existing.time_estimate_min) {
|
||||
changes.push({ field: 'time_estimate', old: String(existing.time_estimate_min), new: String(timeEstimateMin) });
|
||||
}
|
||||
if (dependsOn !== undefined && dependsOn !== existing.depends_on) {
|
||||
changes.push({ field: 'depends_on', old: String(existing.depends_on), new: String(dependsOn) });
|
||||
}
|
||||
|
||||
// Aufgabe aktualisieren (ohne assigned_to)
|
||||
db.prepare(`
|
||||
UPDATE tasks SET
|
||||
title = COALESCE(?, title),
|
||||
description = ?,
|
||||
priority = COALESCE(?, priority),
|
||||
column_id = ?,
|
||||
start_date = ?,
|
||||
due_date = ?,
|
||||
time_estimate_min = ?,
|
||||
depends_on = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
title || null,
|
||||
description !== undefined ? description : existing.description,
|
||||
priority || null,
|
||||
columnId !== undefined ? columnId : existing.column_id,
|
||||
startDate !== undefined ? startDate : existing.start_date,
|
||||
dueDate !== undefined ? dueDate : existing.due_date,
|
||||
timeEstimateMin !== undefined ? timeEstimateMin : existing.time_estimate_min,
|
||||
dependsOn !== undefined ? dependsOn : existing.depends_on,
|
||||
taskId
|
||||
);
|
||||
|
||||
// Mitarbeiter aktualisieren (Mehrfachzuweisung)
|
||||
if (assignees !== undefined && Array.isArray(assignees)) {
|
||||
// Alte Zuweisungen entfernen
|
||||
db.prepare('DELETE FROM task_assignees WHERE task_id = ?').run(taskId);
|
||||
// Neue Zuweisungen hinzufügen
|
||||
const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)');
|
||||
assignees.forEach(userId => {
|
||||
try {
|
||||
insertAssignee.run(taskId, userId);
|
||||
} catch (e) {
|
||||
// User existiert nicht oder bereits zugewiesen
|
||||
}
|
||||
});
|
||||
changes.push({ field: 'assignees', old: 'changed', new: 'changed' });
|
||||
} else if (assignedTo !== undefined) {
|
||||
// Rückwärtskompatibilität: einzelne Zuweisung
|
||||
db.prepare('DELETE FROM task_assignees WHERE task_id = ?').run(taskId);
|
||||
if (assignedTo) {
|
||||
try {
|
||||
db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)').run(taskId, assignedTo);
|
||||
} catch (e) {
|
||||
// Ignorieren
|
||||
}
|
||||
}
|
||||
changes.push({ field: 'assignees', old: 'changed', new: 'changed' });
|
||||
}
|
||||
|
||||
// Labels aktualisieren
|
||||
if (labels !== undefined && Array.isArray(labels)) {
|
||||
// Alte Labels entfernen
|
||||
db.prepare('DELETE FROM task_labels WHERE task_id = ?').run(taskId);
|
||||
// Neue Labels zuweisen
|
||||
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
|
||||
labels.forEach(labelId => {
|
||||
try {
|
||||
insertLabel.run(taskId, labelId);
|
||||
} catch (e) {
|
||||
// Label existiert nicht
|
||||
}
|
||||
});
|
||||
changes.push({ field: 'labels', old: 'changed', new: 'changed' });
|
||||
}
|
||||
|
||||
// Historie-Einträge
|
||||
changes.forEach(change => {
|
||||
addHistory(db, taskId, req.user.id, 'updated', change.field, change.old, change.new);
|
||||
});
|
||||
|
||||
const task = getFullTask(db, taskId);
|
||||
|
||||
logger.info(`Aufgabe aktualisiert: ${task.title} (ID: ${taskId})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${existing.project_id}`).emit('task:updated', task);
|
||||
|
||||
// Benachrichtigungen für Änderungen senden
|
||||
const taskTitle = title || existing.title;
|
||||
|
||||
// Zuweisung geändert - neue Mitarbeiter benachrichtigen
|
||||
if (assignees !== undefined && Array.isArray(assignees)) {
|
||||
// Alte Mitarbeiter ermitteln
|
||||
const oldAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
|
||||
const oldAssigneeIds = oldAssignees.map(a => a.user_id);
|
||||
|
||||
// Neue Mitarbeiter (die vorher nicht zugewiesen waren)
|
||||
const newAssigneeIds = assignees.filter(id => !oldAssigneeIds.includes(id));
|
||||
newAssigneeIds.forEach(assigneeId => {
|
||||
if (assigneeId !== req.user.id) {
|
||||
notificationService.create(assigneeId, 'task:assigned', {
|
||||
taskId: parseInt(taskId),
|
||||
taskTitle: taskTitle,
|
||||
projectId: existing.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
|
||||
// Entfernte Mitarbeiter
|
||||
const removedAssigneeIds = oldAssigneeIds.filter(id => !assignees.includes(id));
|
||||
removedAssigneeIds.forEach(assigneeId => {
|
||||
if (assigneeId !== req.user.id) {
|
||||
notificationService.create(assigneeId, 'task:unassigned', {
|
||||
taskId: parseInt(taskId),
|
||||
taskTitle: taskTitle,
|
||||
projectId: existing.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Priorität auf hoch gesetzt
|
||||
if (priority === 'high' && existing.priority !== 'high') {
|
||||
// Alle Assignees benachrichtigen
|
||||
const currentAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
|
||||
currentAssignees.forEach(a => {
|
||||
if (a.user_id !== req.user.id) {
|
||||
notificationService.create(a.user_id, 'task:priority_up', {
|
||||
taskId: parseInt(taskId),
|
||||
taskTitle: taskTitle,
|
||||
projectId: existing.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fälligkeitsdatum geändert
|
||||
if (dueDate !== undefined && dueDate !== existing.due_date) {
|
||||
const currentAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
|
||||
currentAssignees.forEach(a => {
|
||||
if (a.user_id !== req.user.id) {
|
||||
notificationService.create(a.user_id, 'task:due_changed', {
|
||||
taskId: parseInt(taskId),
|
||||
taskTitle: taskTitle,
|
||||
projectId: existing.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json(task);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren der Aufgabe:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/tasks/:id/move
|
||||
* Aufgabe verschieben (Spalte/Position)
|
||||
*/
|
||||
router.put('/:id/move', (req, res) => {
|
||||
try {
|
||||
const taskId = req.params.id;
|
||||
const { columnId, position } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
const oldColumnId = task.column_id;
|
||||
const oldPosition = task.position;
|
||||
const newColumnId = columnId || oldColumnId;
|
||||
const newPosition = position !== undefined ? position : oldPosition;
|
||||
|
||||
// Spaltenname für Historie
|
||||
const oldColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(oldColumnId);
|
||||
const newColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(newColumnId);
|
||||
|
||||
if (oldColumnId !== newColumnId) {
|
||||
// In andere Spalte verschoben
|
||||
// Positionen in alter Spalte anpassen
|
||||
db.prepare(`
|
||||
UPDATE tasks SET position = position - 1
|
||||
WHERE column_id = ? AND position > ?
|
||||
`).run(oldColumnId, oldPosition);
|
||||
|
||||
// Positionen in neuer Spalte anpassen
|
||||
db.prepare(`
|
||||
UPDATE tasks SET position = position + 1
|
||||
WHERE column_id = ? AND position >= ?
|
||||
`).run(newColumnId, newPosition);
|
||||
|
||||
// Task verschieben
|
||||
db.prepare(`
|
||||
UPDATE tasks SET column_id = ?, position = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(newColumnId, newPosition, taskId);
|
||||
|
||||
addHistory(db, taskId, req.user.id, 'moved', 'column', oldColumn?.name, newColumn?.name);
|
||||
} else if (oldPosition !== newPosition) {
|
||||
// Innerhalb der Spalte verschoben
|
||||
if (newPosition > oldPosition) {
|
||||
db.prepare(`
|
||||
UPDATE tasks SET position = position - 1
|
||||
WHERE column_id = ? AND position > ? AND position <= ?
|
||||
`).run(newColumnId, oldPosition, newPosition);
|
||||
} else {
|
||||
db.prepare(`
|
||||
UPDATE tasks SET position = position + 1
|
||||
WHERE column_id = ? AND position >= ? AND position < ?
|
||||
`).run(newColumnId, newPosition, oldPosition);
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE tasks SET position = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(newPosition, taskId);
|
||||
}
|
||||
|
||||
const updatedTask = getFullTask(db, taskId);
|
||||
|
||||
logger.info(`Aufgabe verschoben: ${task.title} -> ${newColumn?.name || 'Position ' + newPosition}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('task:moved', {
|
||||
task: updatedTask,
|
||||
oldColumnId,
|
||||
newColumnId,
|
||||
oldPosition,
|
||||
newPosition
|
||||
});
|
||||
|
||||
// Benachrichtigung wenn in Erledigt-Spalte verschoben
|
||||
if (oldColumnId !== newColumnId) {
|
||||
const newColumnFull = db.prepare('SELECT filter_category FROM columns WHERE id = ?').get(newColumnId);
|
||||
const oldColumnFull = db.prepare('SELECT filter_category FROM columns WHERE id = ?').get(oldColumnId);
|
||||
|
||||
// Prüfen ob in Erledigt-Spalte verschoben (und vorher nicht dort war)
|
||||
if (newColumnFull?.filter_category === 'completed' && oldColumnFull?.filter_category !== 'completed') {
|
||||
// Alle Assignees benachrichtigen
|
||||
const assignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
|
||||
assignees.forEach(a => {
|
||||
if (a.user_id !== req.user.id) {
|
||||
notificationService.create(a.user_id, 'task:completed', {
|
||||
taskId: parseInt(taskId),
|
||||
taskTitle: task.title,
|
||||
projectId: task.project_id,
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.display_name || req.user.username
|
||||
}, io);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json(updatedTask);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Verschieben der Aufgabe:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/tasks/:id/duplicate
|
||||
* Aufgabe duplizieren
|
||||
*/
|
||||
router.post('/:id/duplicate', (req, res) => {
|
||||
try {
|
||||
const taskId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
// Höchste Position ermitteln
|
||||
const maxPos = db.prepare(
|
||||
'SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE column_id = ?'
|
||||
).get(task.column_id).max;
|
||||
|
||||
// Aufgabe duplizieren
|
||||
const result = db.prepare(`
|
||||
INSERT INTO tasks (
|
||||
project_id, column_id, title, description, priority,
|
||||
due_date, assigned_to, time_estimate_min, position, created_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
task.project_id, task.column_id, task.title + ' (Kopie)', task.description,
|
||||
task.priority, task.due_date, task.assigned_to, task.time_estimate_min,
|
||||
maxPos + 1, req.user.id
|
||||
);
|
||||
|
||||
const newTaskId = result.lastInsertRowid;
|
||||
|
||||
// Labels kopieren
|
||||
const taskLabels = db.prepare('SELECT label_id FROM task_labels WHERE task_id = ?').all(taskId);
|
||||
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
|
||||
taskLabels.forEach(tl => insertLabel.run(newTaskId, tl.label_id));
|
||||
|
||||
// Subtasks kopieren
|
||||
const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(taskId);
|
||||
const insertSubtask = db.prepare('INSERT INTO subtasks (task_id, title, position) VALUES (?, ?, ?)');
|
||||
subtasks.forEach((st, idx) => insertSubtask.run(newTaskId, st.title, idx));
|
||||
|
||||
addHistory(db, newTaskId, req.user.id, 'created', null, null, `Kopie von #${taskId}`);
|
||||
|
||||
const newTask = getFullTask(db, newTaskId);
|
||||
|
||||
logger.info(`Aufgabe dupliziert: ${task.title} -> ${newTask.title}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('task:created', newTask);
|
||||
|
||||
res.status(201).json(newTask);
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Duplizieren:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/tasks/:id/archive
|
||||
* Aufgabe archivieren
|
||||
*/
|
||||
router.put('/:id/archive', (req, res) => {
|
||||
try {
|
||||
const taskId = req.params.id;
|
||||
const { archived } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.run(archived ? 1 : 0, taskId);
|
||||
|
||||
addHistory(db, taskId, req.user.id, archived ? 'archived' : 'restored');
|
||||
|
||||
logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('task:archived', { id: taskId, archived: !!archived });
|
||||
|
||||
res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Archivieren:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/tasks/:id
|
||||
* Aufgabe löschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const taskId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
if (!task) {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
|
||||
|
||||
// Positionen neu nummerieren
|
||||
const remainingTasks = db.prepare(
|
||||
'SELECT id FROM tasks WHERE column_id = ? ORDER BY position'
|
||||
).all(task.column_id);
|
||||
|
||||
remainingTasks.forEach((t, idx) => {
|
||||
db.prepare('UPDATE tasks SET position = ? WHERE id = ?').run(idx, t.id);
|
||||
});
|
||||
|
||||
logger.info(`Aufgabe gelöscht: ${task.title} (ID: ${taskId})`);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${task.project_id}`).emit('task:deleted', {
|
||||
id: taskId,
|
||||
columnId: task.column_id
|
||||
});
|
||||
|
||||
res.json({ message: 'Aufgabe gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
336
backend/routes/templates.js
Normale Datei
336
backend/routes/templates.js
Normale Datei
@ -0,0 +1,336 @@
|
||||
/**
|
||||
* TASKMATE - Template Routes
|
||||
* ==========================
|
||||
* CRUD für Aufgaben-Vorlagen
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../database');
|
||||
const logger = require('../utils/logger');
|
||||
const { validators } = require('../middleware/validation');
|
||||
|
||||
/**
|
||||
* GET /api/templates/:projectId
|
||||
* Alle Vorlagen eines Projekts
|
||||
*/
|
||||
router.get('/:projectId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const templates = db.prepare(`
|
||||
SELECT * FROM task_templates WHERE project_id = ? ORDER BY name
|
||||
`).all(req.params.projectId);
|
||||
|
||||
res.json(templates.map(t => ({
|
||||
id: t.id,
|
||||
projectId: t.project_id,
|
||||
name: t.name,
|
||||
titleTemplate: t.title_template,
|
||||
description: t.description,
|
||||
priority: t.priority,
|
||||
labels: t.labels ? JSON.parse(t.labels) : [],
|
||||
subtasks: t.subtasks ? JSON.parse(t.subtasks) : [],
|
||||
timeEstimateMin: t.time_estimate_min
|
||||
})));
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Abrufen der Vorlagen:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/templates
|
||||
* Neue Vorlage erstellen
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
projectId, name, titleTemplate, description,
|
||||
priority, labels, subtasks, timeEstimateMin
|
||||
} = req.body;
|
||||
|
||||
// Validierung
|
||||
const errors = [];
|
||||
errors.push(validators.required(projectId, 'Projekt-ID'));
|
||||
errors.push(validators.required(name, 'Name'));
|
||||
errors.push(validators.maxLength(name, 50, 'Name'));
|
||||
if (titleTemplate) errors.push(validators.maxLength(titleTemplate, 200, 'Titel-Vorlage'));
|
||||
if (priority) errors.push(validators.enum(priority, ['low', 'medium', 'high'], 'Priorität'));
|
||||
|
||||
const firstError = errors.find(e => e !== null);
|
||||
if (firstError) {
|
||||
return res.status(400).json({ error: firstError });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO task_templates (
|
||||
project_id, name, title_template, description,
|
||||
priority, labels, subtasks, time_estimate_min
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
projectId,
|
||||
name,
|
||||
titleTemplate || null,
|
||||
description || null,
|
||||
priority || 'medium',
|
||||
labels ? JSON.stringify(labels) : null,
|
||||
subtasks ? JSON.stringify(subtasks) : null,
|
||||
timeEstimateMin || null
|
||||
);
|
||||
|
||||
const template = db.prepare('SELECT * FROM task_templates WHERE id = ?').get(result.lastInsertRowid);
|
||||
|
||||
logger.info(`Vorlage erstellt: ${name} in Projekt ${projectId}`);
|
||||
|
||||
res.status(201).json({
|
||||
id: template.id,
|
||||
projectId: template.project_id,
|
||||
name: template.name,
|
||||
titleTemplate: template.title_template,
|
||||
description: template.description,
|
||||
priority: template.priority,
|
||||
labels: template.labels ? JSON.parse(template.labels) : [],
|
||||
subtasks: template.subtasks ? JSON.parse(template.subtasks) : [],
|
||||
timeEstimateMin: template.time_estimate_min
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen der Vorlage:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/templates/:id
|
||||
* Vorlage aktualisieren
|
||||
*/
|
||||
router.put('/:id', (req, res) => {
|
||||
try {
|
||||
const templateId = req.params.id;
|
||||
const {
|
||||
name, titleTemplate, description,
|
||||
priority, labels, subtasks, timeEstimateMin
|
||||
} = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const existing = db.prepare('SELECT * FROM task_templates WHERE id = ?').get(templateId);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Vorlage nicht gefunden' });
|
||||
}
|
||||
|
||||
// Validierung
|
||||
if (name) {
|
||||
const nameError = validators.maxLength(name, 50, 'Name');
|
||||
if (nameError) return res.status(400).json({ error: nameError });
|
||||
}
|
||||
if (priority) {
|
||||
const prioError = validators.enum(priority, ['low', 'medium', 'high'], 'Priorität');
|
||||
if (prioError) return res.status(400).json({ error: prioError });
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
UPDATE task_templates SET
|
||||
name = COALESCE(?, name),
|
||||
title_template = ?,
|
||||
description = ?,
|
||||
priority = COALESCE(?, priority),
|
||||
labels = ?,
|
||||
subtasks = ?,
|
||||
time_estimate_min = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
name || null,
|
||||
titleTemplate !== undefined ? titleTemplate : existing.title_template,
|
||||
description !== undefined ? description : existing.description,
|
||||
priority || null,
|
||||
labels !== undefined ? JSON.stringify(labels) : existing.labels,
|
||||
subtasks !== undefined ? JSON.stringify(subtasks) : existing.subtasks,
|
||||
timeEstimateMin !== undefined ? timeEstimateMin : existing.time_estimate_min,
|
||||
templateId
|
||||
);
|
||||
|
||||
const template = db.prepare('SELECT * FROM task_templates WHERE id = ?').get(templateId);
|
||||
|
||||
logger.info(`Vorlage aktualisiert: ${template.name} (ID: ${templateId})`);
|
||||
|
||||
res.json({
|
||||
id: template.id,
|
||||
projectId: template.project_id,
|
||||
name: template.name,
|
||||
titleTemplate: template.title_template,
|
||||
description: template.description,
|
||||
priority: template.priority,
|
||||
labels: template.labels ? JSON.parse(template.labels) : [],
|
||||
subtasks: template.subtasks ? JSON.parse(template.subtasks) : [],
|
||||
timeEstimateMin: template.time_estimate_min
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aktualisieren der Vorlage:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/templates/:id/create-task
|
||||
* Aufgabe aus Vorlage erstellen
|
||||
*/
|
||||
router.post('/:id/create-task', (req, res) => {
|
||||
try {
|
||||
const templateId = req.params.id;
|
||||
const { columnId, title, assignedTo, dueDate } = req.body;
|
||||
|
||||
const db = getDb();
|
||||
|
||||
const template = db.prepare('SELECT * FROM task_templates WHERE id = ?').get(templateId);
|
||||
if (!template) {
|
||||
return res.status(404).json({ error: 'Vorlage nicht gefunden' });
|
||||
}
|
||||
|
||||
// columnId ist erforderlich
|
||||
if (!columnId) {
|
||||
return res.status(400).json({ error: 'Spalten-ID erforderlich' });
|
||||
}
|
||||
|
||||
// Höchste Position ermitteln
|
||||
const maxPos = db.prepare(
|
||||
'SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE column_id = ?'
|
||||
).get(columnId).max;
|
||||
|
||||
// Aufgabe erstellen
|
||||
const taskTitle = title || template.title_template || 'Neue Aufgabe';
|
||||
const result = db.prepare(`
|
||||
INSERT INTO tasks (
|
||||
project_id, column_id, title, description, priority,
|
||||
due_date, assigned_to, time_estimate_min, position, created_by
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
template.project_id,
|
||||
columnId,
|
||||
taskTitle,
|
||||
template.description,
|
||||
template.priority || 'medium',
|
||||
dueDate || null,
|
||||
assignedTo || null,
|
||||
template.time_estimate_min,
|
||||
maxPos + 1,
|
||||
req.user.id
|
||||
);
|
||||
|
||||
const taskId = result.lastInsertRowid;
|
||||
|
||||
// Labels zuweisen
|
||||
if (template.labels) {
|
||||
const labelIds = JSON.parse(template.labels);
|
||||
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
|
||||
labelIds.forEach(labelId => {
|
||||
try {
|
||||
insertLabel.run(taskId, labelId);
|
||||
} catch (e) { /* Label existiert nicht mehr */ }
|
||||
});
|
||||
}
|
||||
|
||||
// Subtasks erstellen
|
||||
if (template.subtasks) {
|
||||
const subtaskTitles = JSON.parse(template.subtasks);
|
||||
const insertSubtask = db.prepare(
|
||||
'INSERT INTO subtasks (task_id, title, position) VALUES (?, ?, ?)'
|
||||
);
|
||||
subtaskTitles.forEach((st, idx) => {
|
||||
insertSubtask.run(taskId, st, idx);
|
||||
});
|
||||
}
|
||||
|
||||
// Historie
|
||||
db.prepare(`
|
||||
INSERT INTO history (task_id, user_id, action, new_value)
|
||||
VALUES (?, ?, 'created', ?)
|
||||
`).run(taskId, req.user.id, `Aus Vorlage: ${template.name}`);
|
||||
|
||||
logger.info(`Aufgabe aus Vorlage erstellt: ${taskTitle} (Vorlage: ${template.name})`);
|
||||
|
||||
// Vollständige Task-Daten laden (vereinfacht)
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
|
||||
const labels = db.prepare(`
|
||||
SELECT l.* FROM labels l
|
||||
JOIN task_labels tl ON l.id = tl.label_id
|
||||
WHERE tl.task_id = ?
|
||||
`).all(taskId);
|
||||
const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(taskId);
|
||||
|
||||
// WebSocket
|
||||
const io = req.app.get('io');
|
||||
io.to(`project:${template.project_id}`).emit('task:created', {
|
||||
id: task.id,
|
||||
projectId: task.project_id,
|
||||
columnId: task.column_id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
priority: task.priority,
|
||||
dueDate: task.due_date,
|
||||
assignedTo: task.assigned_to,
|
||||
timeEstimateMin: task.time_estimate_min,
|
||||
position: task.position,
|
||||
labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color })),
|
||||
subtasks: subtasks.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
completed: !!s.completed,
|
||||
position: s.position
|
||||
}))
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: task.id,
|
||||
projectId: task.project_id,
|
||||
columnId: task.column_id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
priority: task.priority,
|
||||
dueDate: task.due_date,
|
||||
assignedTo: task.assigned_to,
|
||||
timeEstimateMin: task.time_estimate_min,
|
||||
position: task.position,
|
||||
labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color })),
|
||||
subtasks: subtasks.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
completed: !!s.completed,
|
||||
position: s.position
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Erstellen aus Vorlage:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/templates/:id
|
||||
* Vorlage löschen
|
||||
*/
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
const templateId = req.params.id;
|
||||
const db = getDb();
|
||||
|
||||
const template = db.prepare('SELECT * FROM task_templates WHERE id = ?').get(templateId);
|
||||
if (!template) {
|
||||
return res.status(404).json({ error: 'Vorlage nicht gefunden' });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM task_templates WHERE id = ?').run(templateId);
|
||||
|
||||
logger.info(`Vorlage gelöscht: ${template.name} (ID: ${templateId})`);
|
||||
|
||||
res.json({ message: 'Vorlage gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Löschen der Vorlage:', { error: error.message });
|
||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren