140 lines
4.5 KiB
JavaScript
140 lines
4.5 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();
|
||
|
||
// 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);
|