Files
TaskMate/backend/server.js
2025-12-30 22:49:56 +00:00

312 Zeilen
9.5 KiB
JavaScript

/**
* 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');
const knowledgeRoutes = require('./routes/knowledge');
// 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);
// Knowledge-Routes (Wissensmanagement)
app.use('/api/knowledge', authenticateToken, csrfProtection, knowledgeRoutes);
// =============================================================================
// 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);
});
});