/** * TASKMATE - Hauptserver * ====================== * Node.js/Express Backend mit Socket.io für Echtzeit-Sync */ const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const path = require('path'); const helmet = require('helmet'); const cors = require('cors'); const cookieParser = require('cookie-parser'); // Lokale Module const database = require('./database'); const logger = require('./utils/logger'); const backup = require('./utils/backup'); const { authenticateToken, authenticateSocket } = require('./middleware/auth'); const csrfProtection = require('./middleware/csrf'); // Routes const authRoutes = require('./routes/auth'); const projectRoutes = require('./routes/projects'); const columnRoutes = require('./routes/columns'); const taskRoutes = require('./routes/tasks'); const subtaskRoutes = require('./routes/subtasks'); const commentRoutes = require('./routes/comments'); const labelRoutes = require('./routes/labels'); const fileRoutes = require('./routes/files'); const linkRoutes = require('./routes/links'); const templateRoutes = require('./routes/templates'); const statsRoutes = require('./routes/stats'); const exportRoutes = require('./routes/export'); const importRoutes = require('./routes/import'); const healthRoutes = require('./routes/health'); const adminRoutes = require('./routes/admin'); const proposalRoutes = require('./routes/proposals'); const notificationRoutes = require('./routes/notifications'); const notificationService = require('./services/notificationService'); const gitRoutes = require('./routes/git'); const applicationsRoutes = require('./routes/applications'); const giteaRoutes = require('./routes/gitea'); // Express App erstellen const app = express(); const server = http.createServer(app); // Socket.io Setup const io = new Server(server, { cors: { origin: true, credentials: true } }); // ============================================================================= // MIDDLEWARE // ============================================================================= // Sicherheits-Header app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], fontSrc: ["'self'", "https://fonts.gstatic.com"], imgSrc: ["'self'", "data:", "blob:"], scriptSrc: ["'self'"], connectSrc: ["'self'", "ws:", "wss:"] } } })); // CORS app.use(cors({ origin: true, credentials: true })); // Body Parser app.use(express.json({ limit: '1mb' })); app.use(express.urlencoded({ extended: true, limit: '1mb' })); // Cookie Parser app.use(cookieParser()); // Request Logging app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; // Use originalUrl to see the full path including /api prefix logger.info(`${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`); }); next(); }); // Statische Dateien (Frontend) app.use(express.static(path.join(__dirname, 'public'))); // Uploads-Ordner app.use('/uploads', authenticateToken, express.static(process.env.UPLOAD_DIR || path.join(__dirname, 'uploads'))); // ============================================================================= // API ROUTES // ============================================================================= // Health Check (ohne Auth) app.use('/api/health', healthRoutes); // Auth Routes (Login/Logout - teilweise ohne Auth) app.use('/api/auth', authRoutes); // Geschützte Routes app.use('/api/projects', authenticateToken, csrfProtection, projectRoutes); app.use('/api/columns', authenticateToken, csrfProtection, columnRoutes); app.use('/api/tasks', authenticateToken, csrfProtection, taskRoutes); app.use('/api/subtasks', authenticateToken, csrfProtection, subtaskRoutes); app.use('/api/comments', authenticateToken, csrfProtection, commentRoutes); app.use('/api/labels', authenticateToken, csrfProtection, labelRoutes); app.use('/api/files', authenticateToken, fileRoutes); app.use('/api/links', authenticateToken, csrfProtection, linkRoutes); app.use('/api/templates', authenticateToken, csrfProtection, templateRoutes); app.use('/api/stats', authenticateToken, statsRoutes); app.use('/api/export', authenticateToken, exportRoutes); app.use('/api/import', authenticateToken, csrfProtection, importRoutes); // Admin-Routes (eigene Auth-Middleware) app.use('/api/admin', csrfProtection, adminRoutes); // Proposals-Routes (eigene Auth-Middleware) app.use('/api/proposals', csrfProtection, proposalRoutes); // Notifications-Routes app.use('/api/notifications', authenticateToken, csrfProtection, notificationRoutes); // Git-Routes (lokale Git-Operationen) app.use('/api/git', authenticateToken, csrfProtection, gitRoutes); // Applications-Routes (Projekt-Repository-Verknüpfung) app.use('/api/applications', authenticateToken, csrfProtection, applicationsRoutes); // Gitea-Routes (Gitea API Integration) app.use('/api/gitea', authenticateToken, csrfProtection, giteaRoutes); // ============================================================================= // SOCKET.IO // ============================================================================= // Socket.io Middleware für Authentifizierung io.use(authenticateSocket); // Verbundene Clients speichern const connectedClients = new Map(); io.on('connection', (socket) => { const userId = socket.user.id; const username = socket.user.username; logger.info(`Socket connected: ${username} (${socket.id})`); // Client registrieren connectedClients.set(socket.id, { userId, username, connectedAt: new Date() }); // User-spezifischen Raum beitreten (für Benachrichtigungen) socket.join(`user:${userId}`); // Allen mitteilen, dass jemand online ist io.emit('user:online', { userId, username, onlineUsers: Array.from(connectedClients.values()).map(c => ({ userId: c.userId, username: c.username })) }); // Projekt-Raum beitreten socket.on('project:join', (projectId) => { socket.join(`project:${projectId}`); logger.info(`${username} joined project:${projectId}`); }); // Projekt-Raum verlassen socket.on('project:leave', (projectId) => { socket.leave(`project:${projectId}`); logger.info(`${username} left project:${projectId}`); }); // Disconnect socket.on('disconnect', () => { logger.info(`Socket disconnected: ${username} (${socket.id})`); connectedClients.delete(socket.id); // Allen mitteilen, dass jemand offline ist io.emit('user:offline', { userId, username, onlineUsers: Array.from(connectedClients.values()).map(c => ({ userId: c.userId, username: c.username })) }); }); }); // Socket.io Instance global verfügbar machen für Routes app.set('io', io); // ============================================================================= // FEHLERBEHANDLUNG // ============================================================================= // 404 Handler app.use((req, res, next) => { // API-Anfragen: JSON-Fehler if (req.path.startsWith('/api/')) { return res.status(404).json({ error: 'Endpoint nicht gefunden' }); } // Andere Anfragen: index.html (SPA) res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Globaler Error Handler app.use((err, req, res, next) => { logger.error(`Error: ${err.message}`, { stack: err.stack }); // CSRF-Fehler if (err.code === 'CSRF_ERROR') { return res.status(403).json({ error: 'Ungültiges CSRF-Token' }); } // Multer-Fehler (Datei-Upload) if (err.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ error: `Datei zu groß. Maximum: ${process.env.MAX_FILE_SIZE_MB || 15} MB` }); } // Allgemeiner Fehler res.status(err.status || 500).json({ error: process.env.NODE_ENV === 'production' ? 'Ein Fehler ist aufgetreten' : err.message }); }); // ============================================================================= // SERVER STARTEN // ============================================================================= const PORT = process.env.PORT || 3000; // Datenbank initialisieren database.initialize() .then(() => { // Server starten server.listen(PORT, () => { logger.info(`Server läuft auf Port ${PORT}`); logger.info(`Umgebung: ${process.env.NODE_ENV || 'development'}`); // Backup-System starten if (process.env.BACKUP_ENABLED !== 'false') { backup.startScheduler(); logger.info('Automatische Backups aktiviert'); } // Fälligkeits-Benachrichtigungen Scheduler (alle 6 Stunden) setInterval(() => { notificationService.checkDueTasks(io); }, 6 * 60 * 60 * 1000); // Erste Prüfung nach 1 Minute setTimeout(() => { notificationService.checkDueTasks(io); logger.info('Fälligkeits-Check für Benachrichtigungen gestartet'); }, 60 * 1000); }); }) .catch((err) => { logger.error('Fehler beim Starten:', err); process.exit(1); }); // Graceful Shutdown process.on('SIGTERM', () => { logger.info('SIGTERM empfangen, fahre herunter...'); server.close(() => { database.close(); logger.info('Server beendet'); process.exit(0); }); }); process.on('SIGINT', () => { logger.info('SIGINT empfangen, fahre herunter...'); server.close(() => { database.close(); logger.info('Server beendet'); process.exit(0); }); });