Initial commit
Dieser Commit ist enthalten in:
307
backend/server.js
Normale Datei
307
backend/server.js
Normale Datei
@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren