Dieser Commit ist enthalten in:
Claude Project Manager
2025-12-28 21:36:45 +00:00
Commit ab1e5be9a9
146 geänderte Dateien mit 65525 neuen und 0 gelöschten Zeilen

409
backend/routes/admin.js Normale Datei
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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;

Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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
Datei anzeigen

@ -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;