/** * 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;