const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const cookieParser = require('cookie-parser'); const mongoose = require('mongoose'); const config = require('./config/env'); const connectDB = require('./config/database'); const errorHandler = require('./middleware/errorHandler'); const requestLogger = require('./middleware/requestLogger'); const { apiLimiter, authLimiter } = require('./middleware/rateLimiter'); const logger = require('./utils/logger'); const { version } = require('./package.json'); // Connect to database with retry logic const connectWithRetry = async () => { try { await connectDB(); // Seeding legt Admin-Konto und Grundkonfiguration an. Die Bedingung darf sich // NICHT an der User-Zahl orientieren: die Seed-Liste ist bewusst leer, dadurch // lief das Seeding bei jedem Start erneut. const Admin = require('./models/Admin'); const Config = require('./models/Config'); const [adminCount, configCount] = await Promise.all([ Admin.countDocuments(), Config.countDocuments() ]); if (adminCount === 0 || configCount === 0) { logger.info('Admin oder Konfiguration fehlt, starte Seeding...'); try { const seed = require('./seed'); await seed(); } catch (seedError) { logger.error('Fehler beim Seeding:', seedError.message); } } } catch (error) { logger.error('Verbindungsversuch fehlgeschlagen, erneuter Versuch in 5 Sekunden...'); setTimeout(connectWithRetry, 5000); } }; connectWithRetry(); const app = express(); // Trust first proxy (nginx reverse proxy sets X-Forwarded-For) app.set('trust proxy', 1); // Security headers app.use(helmet({ crossOriginResourcePolicy: { policy: 'same-site' }, contentSecurityPolicy: false // Managed by nginx/frontend })); // Middleware app.use(cors({ origin: config.corsOrigin, credentials: true })); app.use(express.json({ limit: '2mb' })); app.use(express.urlencoded({ extended: true, limit: '2mb' })); app.use(cookieParser()); // Parse cookies app.use(requestLogger); // Rate limiting app.use('/api/', apiLimiter); // Global API rate limiter // Routes app.use('/api/auth', authLimiter, require('./routes/authRoutes')); // Strict limiter for auth app.use('/api', require('./routes/userRoutes')); app.use('/api', require('./routes/auditRoutes')); app.use('/api/config', require('./routes/configRoutes')); app.use('/api/stoeberhundefuehrer', require('./routes/stoeberhundefuehrerRoutes')); // Health check with basic system info app.get('/health', async (req, res) => { const dbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected'; const isProd = config.nodeEnv === 'production'; res.json({ status: dbStatus === 'connected' ? 'OK' : 'DEGRADED', ...(isProd ? {} : { timestamp: new Date().toISOString(), uptime: process.uptime(), environment: config.nodeEnv, database: dbStatus, version }) }); }); const PORT = config.port; const server = app.listen(PORT, () => { logger.info(`Server läuft auf Port ${PORT} (${config.nodeEnv})`); }); // Graceful shutdown (Docker stop / Kubernetes rolling restart) let shuttingDown = false; const shutdown = (signal) => { if (shuttingDown) return; shuttingDown = true; logger.info(`${signal} empfangen, fahre Server herunter...`); const forceExit = setTimeout(() => { logger.warn('Shutdown-Timeout erreicht, beende Prozess hart'); process.exit(1); }, 10000); forceExit.unref(); server.close(() => { logger.info('HTTP-Server geschlossen'); mongoose.connection.close(false).then(() => { logger.info('MongoDB-Verbindung geschlossen'); process.exit(0); }).catch(() => process.exit(1)); }); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); // If a frontend build exists, serve it as static files (useful for local testing). // Muss vor dem errorHandler stehen – der gehoert als letztes Middleware registriert. const path = require('path'); const fs = require('fs'); const buildPath = path.join(__dirname, '..', 'frontend', 'build'); if (fs.existsSync(buildPath)) { logger.info('Frontend-Build gefunden — serviere statische Dateien von frontend/build'); app.use(express.static(buildPath)); // Serve index.html for any unknown GET route (SPA fallback) app.get('*', (req, res, next) => { if (req.path.startsWith('/api') || req.path === '/health') return next(); res.sendFile(path.join(buildPath, 'index.html')); }); } // Error handler (must be last) app.use(errorHandler);