jagd-apps/stoeberhunde/backend/server.js

123 lines
4.0 KiB
JavaScript

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();
// Seed database if empty (runs in all environments on first start)
const User = require('./models/User');
const userCount = await User.countDocuments();
if (userCount === 0) {
logger.info('Datenbank ist leer, starte Seeding...');
const { exec } = require('child_process');
await new Promise((resolve) => {
exec('node seed.js', { cwd: __dirname }, (seedError, stdout, stderr) => {
if (seedError) logger.error('Fehler beim Seeding:', seedError.message);
if (stdout) logger.info(stdout.trim());
if (stderr) logger.warn(stderr.trim());
resolve();
});
});
}
} 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
})
});
});
// Error handler (must be last)
app.use(errorHandler);
const PORT = config.port;
const server = app.listen(PORT, () => {
logger.info(`Server läuft auf Port ${PORT} (${config.nodeEnv})`);
});
// Graceful shutdown on SIGTERM (Docker stop / Kubernetes rolling restart)
process.on('SIGTERM', () => {
logger.info('SIGTERM empfangen, fahre Server herunter...');
server.close(() => {
logger.info('HTTP-Server geschlossen');
mongoose.connection.close(false).then(() => {
logger.info('MongoDB-Verbindung geschlossen');
process.exit(0);
}).catch(() => process.exit(1));
});
});
// If a frontend build exists, serve it as static files (useful for local testing)
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'));
});
}