jagd-apps/stoeberhunde/backend/server.js

147 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'));
// Unbekannte API-Pfade als JSON beantworten. Ohne das faellt die Anfrage bis zum
// Express-Standard durch und liefert eine HTML-Seite ("Cannot GET /api/foo"),
// mit der ein JSON-Client nichts anfangen kann.
app.use('/api', (req, res) => {
res.status(404).json({ success: false, message: 'Endpunkt nicht gefunden' });
});
// 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);