355 Zeilen
11 KiB
JavaScript
355 Zeilen
11 KiB
JavaScript
/**
|
|
* TASKMATE - Hauptserver
|
|
* ======================
|
|
* Node.js/Express Backend mit Socket.io für Echtzeit-Sync
|
|
*/
|
|
|
|
// Umgebungsvariablen laden (muss ganz oben stehen!)
|
|
require('dotenv').config();
|
|
|
|
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 reminderService = require('./services/reminderService');
|
|
const gitRoutes = require('./routes/git');
|
|
const applicationsRoutes = require('./routes/applications');
|
|
const giteaRoutes = require('./routes/gitea');
|
|
const knowledgeRoutes = require('./routes/knowledge');
|
|
const codingRoutes = require('./routes/coding');
|
|
const reminderRoutes = require('./routes/reminders');
|
|
|
|
// 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
|
|
// =============================================================================
|
|
|
|
// Erweiterte Sicherheits-Header (CSP temporär deaktiviert für Login-Fix)
|
|
app.use(helmet({
|
|
contentSecurityPolicy: false, // Temporär deaktiviert
|
|
hsts: {
|
|
maxAge: 31536000, // 1 Jahr
|
|
includeSubDomains: true,
|
|
preload: true
|
|
},
|
|
noSniff: true,
|
|
xssFilter: true,
|
|
referrerPolicy: {
|
|
policy: "strict-origin-when-cross-origin"
|
|
}
|
|
}));
|
|
|
|
// 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());
|
|
|
|
// Input Sanitization (vor allen anderen Middlewares)
|
|
const { sanitizeMiddleware } = require('./middleware/validation');
|
|
app.use(sanitizeMiddleware);
|
|
|
|
// 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) - ohne Caching für Development
|
|
app.use(express.static(path.join(__dirname, 'public'), {
|
|
etag: false,
|
|
lastModified: false,
|
|
cacheControl: false,
|
|
setHeaders: (res, path) => {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
}
|
|
}));
|
|
|
|
// 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);
|
|
|
|
// Coding-Routes (Entwicklungsverzeichnisse mit Claude/Codex)
|
|
app.use('/api/coding', authenticateToken, csrfProtection, codingRoutes);
|
|
|
|
// Reminder-Routes (Erinnerungen)
|
|
app.use('/api/reminders', authenticateToken, csrfProtection, reminderRoutes);
|
|
|
|
// Contacts-Routes (Kontakte)
|
|
app.use('/api/contacts', authenticateToken, csrfProtection, require('./routes/contacts'));
|
|
|
|
// =============================================================================
|
|
// 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);
|
|
|
|
// Reminder Service starten
|
|
const reminderServiceInstance = reminderService.getInstance(io);
|
|
reminderServiceInstance.start();
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
logger.error('Fehler beim Starten:', err);
|
|
process.exit(1);
|
|
});
|
|
|
|
// Graceful Shutdown
|
|
process.on('SIGTERM', () => {
|
|
logger.info('SIGTERM empfangen, fahre herunter...');
|
|
|
|
// Reminder Service stoppen
|
|
const reminderServiceInstance = reminderService.getInstance();
|
|
reminderServiceInstance.stop();
|
|
|
|
server.close(() => {
|
|
database.close();
|
|
logger.info('Server beendet');
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
logger.info('SIGINT empfangen, fahre herunter...');
|
|
|
|
// Reminder Service stoppen
|
|
const reminderServiceInstance = reminderService.getInstance();
|
|
reminderServiceInstance.stop();
|
|
|
|
server.close(() => {
|
|
database.close();
|
|
logger.info('Server beendet');
|
|
process.exit(0);
|
|
});
|
|
});
|