From 718d84a54566aae6573338369555cc572cb97d0c Mon Sep 17 00:00:00 2001 From: thomas Date: Tue, 25 Aug 2026 08:09:53 +0200 Subject: [PATCH] fix(backend): Seeding beendet nicht mehr den Server, Routing und Shutdown korrigiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - REGRESSION aus d9fecb6: server.js ruft seed.js seit dem Wechsel von exec('node seed.js') auf require('./seed') im selben Prozess auf. seedDatabase() endet aber mit mongoose.connection.close() und process.exit(0) – auf einer frischen Datenbank hat sich der Server damit unmittelbar nach dem Seeding selbst beendet. Verbindungsabbau und Exit macht jetzt nur noch der CLI-Aufruf (npm run seed); zusätzlich verbindet seed.js nicht erneut, wenn die Verbindung bereits steht. - Seeding hing an User.countDocuments(). Die Seed-Liste ist bewusst leer, die Bedingung war also bei jedem Start wahr, und seed.js hat die Config per findOneAndUpdate überschrieben: App-Name, Regeln und Texte des Admins gingen bei jedem Neustart verloren. Bedingung hängt jetzt an Admin und Config, die Config wird nur noch angelegt, nie überschrieben. - DELETE /api/users/bulk war nicht erreichbar: die Route stand hinter /users/:id, Express hat den Pfad an deleteUser mit id="bulk" gegeben (CastError -> 404). Bulk-Routen stehen jetzt davor. - getAdminActivity rief mongoose.Types.ObjectId() ohne new auf. Seit bson 5 ist das eine echte Klasse, der Endpunkt lief immer in einen 500er. Ungültige IDs liefern jetzt 400 statt 500. - connectDB beendete den Prozess mit process.exit(1), womit die Retry-Schleife in server.js toter Code war. Jetzt wird geworfen. - geocode.js rief in seinen Signal-Handlern process.exit(0) und hat den Graceful Shutdown mittendrin abgeschnitten. Der Cache wird nur noch geschrieben; das Beenden gehört server.js, das jetzt auch SIGINT behandelt und einen Timeout hat. - errorHandler stand vor dem Static-/SPA-Fallback und war damit nicht mehr letzte Middleware. - Jagdsprache in seed.js: "Verbreiten Sie den Standort und den Anschuss" -> "Verbrechen". Da der Seed die Config anlegt, ist das der Text, den die Nutzer sehen. Co-Authored-By: Claude Opus 5 --- drohnenfuehrer/backend/config/database.js | 4 +- .../backend/controllers/auditController.js | 16 ++-- drohnenfuehrer/backend/package-lock.json | 75 ++++++++++++++++++- drohnenfuehrer/backend/routes/userRoutes.js | 7 +- drohnenfuehrer/backend/seed.js | 22 ++++-- drohnenfuehrer/backend/server.js | 46 ++++++++---- drohnenfuehrer/backend/utils/geocode.js | 15 ++-- nachsuche/backend/config/database.js | 4 +- .../backend/controllers/auditController.js | 16 ++-- nachsuche/backend/package-lock.json | 71 +++++++++++++++++- nachsuche/backend/routes/userRoutes.js | 7 +- nachsuche/backend/seed.js | 24 +++--- nachsuche/backend/server.js | 46 ++++++++---- nachsuche/backend/utils/geocode.js | 15 ++-- stoeberhunde/backend/config/database.js | 4 +- .../backend/controllers/auditController.js | 16 ++-- stoeberhunde/backend/package-lock.json | 75 ++++++++++++++++++- stoeberhunde/backend/routes/userRoutes.js | 7 +- stoeberhunde/backend/seed.js | 22 ++++-- stoeberhunde/backend/server.js | 46 ++++++++---- stoeberhunde/backend/utils/geocode.js | 15 ++-- 21 files changed, 425 insertions(+), 128 deletions(-) diff --git a/drohnenfuehrer/backend/config/database.js b/drohnenfuehrer/backend/config/database.js index b5fc0b0..a88409b 100644 --- a/drohnenfuehrer/backend/config/database.js +++ b/drohnenfuehrer/backend/config/database.js @@ -10,8 +10,10 @@ const connectDB = async () => { }); logger.info('MongoDB verbunden'); } catch (error) { + // Nicht process.exit(): der Aufrufer (server.js) implementiert einen Retry. + // Ein Exit hier hat den Retry zu totem Code gemacht. logger.error('MongoDB Verbindungsfehler:', error.message); - process.exit(1); + throw error; } }; diff --git a/drohnenfuehrer/backend/controllers/auditController.js b/drohnenfuehrer/backend/controllers/auditController.js index 720a072..cf4b6be 100644 --- a/drohnenfuehrer/backend/controllers/auditController.js +++ b/drohnenfuehrer/backend/controllers/auditController.js @@ -1,5 +1,7 @@ +const mongoose = require('mongoose'); const AuditLog = require('../models/AuditLog'); const logger = require('../utils/logger'); +const { escapeCell } = require('../utils/csv'); /** * Get all audit logs with pagination and filtering @@ -99,8 +101,13 @@ const getAdminActivity = async (req, res) => { try { const { adminId } = req.params; + if (!mongoose.isValidObjectId(adminId)) { + return res.status(400).json({ success: false, message: 'Ungültige Admin-ID' }); + } + const stats = await AuditLog.aggregate([ - { $match: { adminId: require('mongoose').Types.ObjectId(adminId) } }, + // ObjectId ist seit bson 5 eine echte Klasse und braucht new. + { $match: { adminId: new mongoose.Types.ObjectId(adminId) } }, { $group: { _id: '$action', @@ -214,13 +221,6 @@ const exportAuditLogs = async (req, res) => { .limit(10000) .lean(); - const escapeCell = (val) => { - if (val == null) return ''; - const str = String(val); - return str.includes(',') || str.includes('"') || str.includes('\n') - ? `"${str.replace(/"/g, '""')}"` : str; - }; - const header = [ 'Zeitstempel', 'Aktion', 'Ressource', 'Ressourcen-Name', 'Admin', 'IP-Adresse', 'Methode', 'Pfad', 'Status-Code', 'Dauer (ms)', diff --git a/drohnenfuehrer/backend/package-lock.json b/drohnenfuehrer/backend/package-lock.json index a48a616..4158c96 100644 --- a/drohnenfuehrer/backend/package-lock.json +++ b/drohnenfuehrer/backend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "tracking-leaders-backend", + "name": "drohnenfuehrer-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tracking-leaders-backend", + "name": "drohnenfuehrer-backend", "version": "1.0.0", "dependencies": { "bcryptjs": "^2.4.3", @@ -15,9 +15,12 @@ "express": "^4.18.2", "express-rate-limit": "^8.2.1", "express-validator": "^7.3.1", + "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", "mongoose": "^7.5.0", - "winston": "^3.19.0" + "nodemailer": "^6.9.16", + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "jest": "^30.2.0", @@ -2880,6 +2883,15 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3242,6 +3254,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", + "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4579,6 +4603,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", @@ -4754,6 +4787,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", @@ -4840,6 +4882,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6249,6 +6300,24 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", diff --git a/drohnenfuehrer/backend/routes/userRoutes.js b/drohnenfuehrer/backend/routes/userRoutes.js index d255f24..c458486 100644 --- a/drohnenfuehrer/backend/routes/userRoutes.js +++ b/drohnenfuehrer/backend/routes/userRoutes.js @@ -32,6 +32,11 @@ router.get('/users', authenticateToken, getAllUsers); router.get('/users/export', authenticateToken, auditLog('EXPORT', 'User'), exportUsers); router.post('/users/import', authenticateToken, auditLog('IMPORT', 'User'), importUsers); router.get('/users/deleted', authenticateToken, getDeletedUsers); +// Bulk-Operationen MÜSSEN vor /users/:id stehen, sonst schluckt die +// :id-Route den Pfad /users/bulk und die Massen-Löschung läuft ins Leere. +router.patch('/users/bulk', authenticateToken, auditLog('BULK_UPDATE', 'User'), bulkUpdateUsers); +router.delete('/users/bulk', authenticateToken, auditLog('BULK_DELETE', 'User'), bulkDeleteUsers); + router.get('/users/:id', authenticateToken, getUserById); router.post('/users', authenticateToken, auditLog('CREATE', 'User'), createUser); router.put('/users/:id', authenticateToken, auditLog('UPDATE', 'User'), updateUser); @@ -45,7 +50,5 @@ router.post('/users/:id/photo', authenticateToken, auditLog('UPDATE', 'User'), u router.delete('/users/:id/photo', authenticateToken, auditLog('UPDATE', 'User'), deleteUserPhoto); // Bulk operations -router.patch('/users/bulk', authenticateToken, auditLog('BULK_UPDATE', 'User'), bulkUpdateUsers); -router.delete('/users/bulk', authenticateToken, auditLog('BULK_DELETE', 'User'), bulkDeleteUsers); module.exports = router; diff --git a/drohnenfuehrer/backend/seed.js b/drohnenfuehrer/backend/seed.js index 882ae03..39f89cd 100644 --- a/drohnenfuehrer/backend/seed.js +++ b/drohnenfuehrer/backend/seed.js @@ -9,7 +9,10 @@ const users = []; const seedDatabase = async () => { try { - await mongoose.connect(config.mongoUri); + // Beim Aufruf aus server.js besteht die Verbindung bereits. + if (mongoose.connection.readyState !== 1) { + await mongoose.connect(config.mongoUri); + } logger.info('MongoDB verbunden für Seeding...'); @@ -51,6 +54,8 @@ const seedDatabase = async () => { } // Seed config - always update userTypes + sections + rules + // Config NUR anlegen, niemals ueberschreiben. Vorher hat jeder Neustart + // die im Admin-Panel gepflegten Texte, Regeln und den App-Namen zurueckgesetzt. const existingConfig = await Config.findOne(); const configData = { userTypes: [ @@ -90,22 +95,23 @@ const seedDatabase = async () => { await Config.create(configData); logger.info('✅ Konfiguration erstellt'); } else { - await Config.findOneAndUpdate({}, configData, { new: true }); - logger.info('✅ Konfiguration aktualisiert'); + logger.info('ℹ️ Konfiguration existiert bereits, bleibt unveraendert'); } logger.info('✅ Datenbank-Seeding abgeschlossen'); - await mongoose.connection.close(); - process.exit(0); } catch (error) { logger.error('❌ Fehler beim Seeding:', error); - await mongoose.connection.close(); - process.exit(1); + throw error; } }; +// Verbindung schliessen und den Prozess beenden darf nur der CLI-Aufruf +// (npm run seed). server.js ruft seedDatabase() im selben Prozess auf – ein +// process.exit(0) hier hat den frisch gestarteten Server sofort wieder beendet. if (require.main === module) { - seedDatabase(); + seedDatabase() + .then(async () => { await mongoose.connection.close(); process.exit(0); }) + .catch(async () => { await mongoose.connection.close(); process.exit(1); }); } module.exports = seedDatabase; diff --git a/drohnenfuehrer/backend/server.js b/drohnenfuehrer/backend/server.js index b07d7ee..a5feddb 100644 --- a/drohnenfuehrer/backend/server.js +++ b/drohnenfuehrer/backend/server.js @@ -16,11 +16,17 @@ 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...'); + // 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(); @@ -83,17 +89,24 @@ app.get('/health', async (req, res) => { }); }); -// 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...'); +// 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(() => { @@ -101,9 +114,13 @@ process.on('SIGTERM', () => { process.exit(0); }).catch(() => process.exit(1)); }); -}); +}; -// If a frontend build exists, serve it as static files (useful for local testing) +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'); @@ -117,3 +134,6 @@ if (fs.existsSync(buildPath)) { res.sendFile(path.join(buildPath, 'index.html')); }); } + +// Error handler (must be last) +app.use(errorHandler); diff --git a/drohnenfuehrer/backend/utils/geocode.js b/drohnenfuehrer/backend/utils/geocode.js index 1c3457d..99a22cc 100644 --- a/drohnenfuehrer/backend/utils/geocode.js +++ b/drohnenfuehrer/backend/utils/geocode.js @@ -45,14 +45,13 @@ const saveCache = async () => { setInterval(saveCache, CACHE_SAVE_INTERVAL).unref(); // Save on process exit -process.on('SIGINT', async () => { - await saveCache(); - process.exit(0); -}); -process.on('SIGTERM', async () => { - await saveCache(); - process.exit(0); -}); +// Cache beim Herunterfahren sichern – ohne process.exit(): das Beenden gehört +// dem Shutdown-Handler in server.js, der sonst mittendrin abgeschnitten wird. +const flushOnShutdown = () => { + saveCache().catch(err => logger.error('Failed to flush geocoding cache:', err.message)); +}; +process.on('SIGINT', flushOnShutdown); +process.on('SIGTERM', flushOnShutdown); // Initialize cache loading loadCache().catch(err => logger.error('Cache initialization error:', err)); diff --git a/nachsuche/backend/config/database.js b/nachsuche/backend/config/database.js index b5fc0b0..a88409b 100644 --- a/nachsuche/backend/config/database.js +++ b/nachsuche/backend/config/database.js @@ -10,8 +10,10 @@ const connectDB = async () => { }); logger.info('MongoDB verbunden'); } catch (error) { + // Nicht process.exit(): der Aufrufer (server.js) implementiert einen Retry. + // Ein Exit hier hat den Retry zu totem Code gemacht. logger.error('MongoDB Verbindungsfehler:', error.message); - process.exit(1); + throw error; } }; diff --git a/nachsuche/backend/controllers/auditController.js b/nachsuche/backend/controllers/auditController.js index 720a072..cf4b6be 100644 --- a/nachsuche/backend/controllers/auditController.js +++ b/nachsuche/backend/controllers/auditController.js @@ -1,5 +1,7 @@ +const mongoose = require('mongoose'); const AuditLog = require('../models/AuditLog'); const logger = require('../utils/logger'); +const { escapeCell } = require('../utils/csv'); /** * Get all audit logs with pagination and filtering @@ -99,8 +101,13 @@ const getAdminActivity = async (req, res) => { try { const { adminId } = req.params; + if (!mongoose.isValidObjectId(adminId)) { + return res.status(400).json({ success: false, message: 'Ungültige Admin-ID' }); + } + const stats = await AuditLog.aggregate([ - { $match: { adminId: require('mongoose').Types.ObjectId(adminId) } }, + // ObjectId ist seit bson 5 eine echte Klasse und braucht new. + { $match: { adminId: new mongoose.Types.ObjectId(adminId) } }, { $group: { _id: '$action', @@ -214,13 +221,6 @@ const exportAuditLogs = async (req, res) => { .limit(10000) .lean(); - const escapeCell = (val) => { - if (val == null) return ''; - const str = String(val); - return str.includes(',') || str.includes('"') || str.includes('\n') - ? `"${str.replace(/"/g, '""')}"` : str; - }; - const header = [ 'Zeitstempel', 'Aktion', 'Ressource', 'Ressourcen-Name', 'Admin', 'IP-Adresse', 'Methode', 'Pfad', 'Status-Code', 'Dauer (ms)', diff --git a/nachsuche/backend/package-lock.json b/nachsuche/backend/package-lock.json index a48a616..40c3e5c 100644 --- a/nachsuche/backend/package-lock.json +++ b/nachsuche/backend/package-lock.json @@ -15,9 +15,12 @@ "express": "^4.18.2", "express-rate-limit": "^8.2.1", "express-validator": "^7.3.1", + "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", "mongoose": "^7.5.0", - "winston": "^3.19.0" + "nodemailer": "^6.9.16", + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "jest": "^30.2.0", @@ -2880,6 +2883,15 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3242,6 +3254,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", + "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4579,6 +4603,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", @@ -4754,6 +4787,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", @@ -4840,6 +4882,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6249,6 +6300,24 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", diff --git a/nachsuche/backend/routes/userRoutes.js b/nachsuche/backend/routes/userRoutes.js index d255f24..c458486 100644 --- a/nachsuche/backend/routes/userRoutes.js +++ b/nachsuche/backend/routes/userRoutes.js @@ -32,6 +32,11 @@ router.get('/users', authenticateToken, getAllUsers); router.get('/users/export', authenticateToken, auditLog('EXPORT', 'User'), exportUsers); router.post('/users/import', authenticateToken, auditLog('IMPORT', 'User'), importUsers); router.get('/users/deleted', authenticateToken, getDeletedUsers); +// Bulk-Operationen MÜSSEN vor /users/:id stehen, sonst schluckt die +// :id-Route den Pfad /users/bulk und die Massen-Löschung läuft ins Leere. +router.patch('/users/bulk', authenticateToken, auditLog('BULK_UPDATE', 'User'), bulkUpdateUsers); +router.delete('/users/bulk', authenticateToken, auditLog('BULK_DELETE', 'User'), bulkDeleteUsers); + router.get('/users/:id', authenticateToken, getUserById); router.post('/users', authenticateToken, auditLog('CREATE', 'User'), createUser); router.put('/users/:id', authenticateToken, auditLog('UPDATE', 'User'), updateUser); @@ -45,7 +50,5 @@ router.post('/users/:id/photo', authenticateToken, auditLog('UPDATE', 'User'), u router.delete('/users/:id/photo', authenticateToken, auditLog('UPDATE', 'User'), deleteUserPhoto); // Bulk operations -router.patch('/users/bulk', authenticateToken, auditLog('BULK_UPDATE', 'User'), bulkUpdateUsers); -router.delete('/users/bulk', authenticateToken, auditLog('BULK_DELETE', 'User'), bulkDeleteUsers); module.exports = router; diff --git a/nachsuche/backend/seed.js b/nachsuche/backend/seed.js index 68fa284..dd11e3f 100644 --- a/nachsuche/backend/seed.js +++ b/nachsuche/backend/seed.js @@ -12,7 +12,10 @@ const users = []; const seedDatabase = async () => { try { - await mongoose.connect(config.mongoUri); + // Beim Aufruf aus server.js besteht die Verbindung bereits. + if (mongoose.connection.readyState !== 1) { + await mongoose.connect(config.mongoUri); + } logger.info('MongoDB verbunden für Seeding...'); @@ -54,6 +57,8 @@ const seedDatabase = async () => { } // Seed config - always update userTypes + sections + rules + // Config NUR anlegen, niemals ueberschreiben. Vorher hat jeder Neustart + // die im Admin-Panel gepflegten Texte, Regeln und den App-Namen zurueckgesetzt. const existingConfig = await Config.findOne(); const configData = { userTypes: [ @@ -63,7 +68,7 @@ const seedDatabase = async () => { { code: 'LAB', label: 'Labrador' } ], rules: [ - "Verbreiten Sie den Standort und den Anschuss.", + "Verbrechen Sie den Standort und den Anschuss.", "Vertreten Sie keine Pirschzeichen.", "Versuchen Sie die Nachsuche möglichst nicht erst mit ungeübten Hunden.", "Benachrichtigen Sie unverzüglich den Nachsuchenführer und die evtl. betroffenen Revierinhaber der Nachbarjagdbezirke.", @@ -97,22 +102,23 @@ const seedDatabase = async () => { await Config.create(configData); logger.info('✅ Konfiguration erstellt'); } else { - await Config.findOneAndUpdate({}, configData, { new: true }); - logger.info('✅ Konfiguration aktualisiert'); + logger.info('ℹ️ Konfiguration existiert bereits, bleibt unveraendert'); } logger.info('✅ Datenbank-Seeding abgeschlossen'); - await mongoose.connection.close(); - process.exit(0); } catch (error) { logger.error('❌ Fehler beim Seeding:', error); - await mongoose.connection.close(); - process.exit(1); + throw error; } }; +// Verbindung schliessen und den Prozess beenden darf nur der CLI-Aufruf +// (npm run seed). server.js ruft seedDatabase() im selben Prozess auf – ein +// process.exit(0) hier hat den frisch gestarteten Server sofort wieder beendet. if (require.main === module) { - seedDatabase(); + seedDatabase() + .then(async () => { await mongoose.connection.close(); process.exit(0); }) + .catch(async () => { await mongoose.connection.close(); process.exit(1); }); } module.exports = seedDatabase; diff --git a/nachsuche/backend/server.js b/nachsuche/backend/server.js index bccddaa..4bd9c65 100644 --- a/nachsuche/backend/server.js +++ b/nachsuche/backend/server.js @@ -16,11 +16,17 @@ 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...'); + // 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(); @@ -83,17 +89,24 @@ app.get('/health', async (req, res) => { }); }); -// 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...'); +// 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(() => { @@ -101,9 +114,13 @@ process.on('SIGTERM', () => { process.exit(0); }).catch(() => process.exit(1)); }); -}); +}; -// If a frontend build exists, serve it as static files (useful for local testing) +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'); @@ -117,3 +134,6 @@ if (fs.existsSync(buildPath)) { res.sendFile(path.join(buildPath, 'index.html')); }); } + +// Error handler (must be last) +app.use(errorHandler); diff --git a/nachsuche/backend/utils/geocode.js b/nachsuche/backend/utils/geocode.js index 1c3457d..99a22cc 100644 --- a/nachsuche/backend/utils/geocode.js +++ b/nachsuche/backend/utils/geocode.js @@ -45,14 +45,13 @@ const saveCache = async () => { setInterval(saveCache, CACHE_SAVE_INTERVAL).unref(); // Save on process exit -process.on('SIGINT', async () => { - await saveCache(); - process.exit(0); -}); -process.on('SIGTERM', async () => { - await saveCache(); - process.exit(0); -}); +// Cache beim Herunterfahren sichern – ohne process.exit(): das Beenden gehört +// dem Shutdown-Handler in server.js, der sonst mittendrin abgeschnitten wird. +const flushOnShutdown = () => { + saveCache().catch(err => logger.error('Failed to flush geocoding cache:', err.message)); +}; +process.on('SIGINT', flushOnShutdown); +process.on('SIGTERM', flushOnShutdown); // Initialize cache loading loadCache().catch(err => logger.error('Cache initialization error:', err)); diff --git a/stoeberhunde/backend/config/database.js b/stoeberhunde/backend/config/database.js index b5fc0b0..a88409b 100644 --- a/stoeberhunde/backend/config/database.js +++ b/stoeberhunde/backend/config/database.js @@ -10,8 +10,10 @@ const connectDB = async () => { }); logger.info('MongoDB verbunden'); } catch (error) { + // Nicht process.exit(): der Aufrufer (server.js) implementiert einen Retry. + // Ein Exit hier hat den Retry zu totem Code gemacht. logger.error('MongoDB Verbindungsfehler:', error.message); - process.exit(1); + throw error; } }; diff --git a/stoeberhunde/backend/controllers/auditController.js b/stoeberhunde/backend/controllers/auditController.js index 720a072..cf4b6be 100644 --- a/stoeberhunde/backend/controllers/auditController.js +++ b/stoeberhunde/backend/controllers/auditController.js @@ -1,5 +1,7 @@ +const mongoose = require('mongoose'); const AuditLog = require('../models/AuditLog'); const logger = require('../utils/logger'); +const { escapeCell } = require('../utils/csv'); /** * Get all audit logs with pagination and filtering @@ -99,8 +101,13 @@ const getAdminActivity = async (req, res) => { try { const { adminId } = req.params; + if (!mongoose.isValidObjectId(adminId)) { + return res.status(400).json({ success: false, message: 'Ungültige Admin-ID' }); + } + const stats = await AuditLog.aggregate([ - { $match: { adminId: require('mongoose').Types.ObjectId(adminId) } }, + // ObjectId ist seit bson 5 eine echte Klasse und braucht new. + { $match: { adminId: new mongoose.Types.ObjectId(adminId) } }, { $group: { _id: '$action', @@ -214,13 +221,6 @@ const exportAuditLogs = async (req, res) => { .limit(10000) .lean(); - const escapeCell = (val) => { - if (val == null) return ''; - const str = String(val); - return str.includes(',') || str.includes('"') || str.includes('\n') - ? `"${str.replace(/"/g, '""')}"` : str; - }; - const header = [ 'Zeitstempel', 'Aktion', 'Ressource', 'Ressourcen-Name', 'Admin', 'IP-Adresse', 'Methode', 'Pfad', 'Status-Code', 'Dauer (ms)', diff --git a/stoeberhunde/backend/package-lock.json b/stoeberhunde/backend/package-lock.json index a48a616..60e39de 100644 --- a/stoeberhunde/backend/package-lock.json +++ b/stoeberhunde/backend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "tracking-leaders-backend", + "name": "stoeberhunde-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tracking-leaders-backend", + "name": "stoeberhunde-backend", "version": "1.0.0", "dependencies": { "bcryptjs": "^2.4.3", @@ -15,9 +15,12 @@ "express": "^4.18.2", "express-rate-limit": "^8.2.1", "express-validator": "^7.3.1", + "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", "mongoose": "^7.5.0", - "winston": "^3.19.0" + "nodemailer": "^6.9.16", + "winston": "^3.19.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "jest": "^30.2.0", @@ -2880,6 +2883,15 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3242,6 +3254,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz", + "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4579,6 +4603,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", @@ -4754,6 +4787,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", @@ -4840,6 +4882,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6249,6 +6300,24 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", diff --git a/stoeberhunde/backend/routes/userRoutes.js b/stoeberhunde/backend/routes/userRoutes.js index d255f24..c458486 100644 --- a/stoeberhunde/backend/routes/userRoutes.js +++ b/stoeberhunde/backend/routes/userRoutes.js @@ -32,6 +32,11 @@ router.get('/users', authenticateToken, getAllUsers); router.get('/users/export', authenticateToken, auditLog('EXPORT', 'User'), exportUsers); router.post('/users/import', authenticateToken, auditLog('IMPORT', 'User'), importUsers); router.get('/users/deleted', authenticateToken, getDeletedUsers); +// Bulk-Operationen MÜSSEN vor /users/:id stehen, sonst schluckt die +// :id-Route den Pfad /users/bulk und die Massen-Löschung läuft ins Leere. +router.patch('/users/bulk', authenticateToken, auditLog('BULK_UPDATE', 'User'), bulkUpdateUsers); +router.delete('/users/bulk', authenticateToken, auditLog('BULK_DELETE', 'User'), bulkDeleteUsers); + router.get('/users/:id', authenticateToken, getUserById); router.post('/users', authenticateToken, auditLog('CREATE', 'User'), createUser); router.put('/users/:id', authenticateToken, auditLog('UPDATE', 'User'), updateUser); @@ -45,7 +50,5 @@ router.post('/users/:id/photo', authenticateToken, auditLog('UPDATE', 'User'), u router.delete('/users/:id/photo', authenticateToken, auditLog('UPDATE', 'User'), deleteUserPhoto); // Bulk operations -router.patch('/users/bulk', authenticateToken, auditLog('BULK_UPDATE', 'User'), bulkUpdateUsers); -router.delete('/users/bulk', authenticateToken, auditLog('BULK_DELETE', 'User'), bulkDeleteUsers); module.exports = router; diff --git a/stoeberhunde/backend/seed.js b/stoeberhunde/backend/seed.js index 472a158..9b4dd72 100644 --- a/stoeberhunde/backend/seed.js +++ b/stoeberhunde/backend/seed.js @@ -9,7 +9,10 @@ const users = []; const seedDatabase = async () => { try { - await mongoose.connect(config.mongoUri); + // Beim Aufruf aus server.js besteht die Verbindung bereits. + if (mongoose.connection.readyState !== 1) { + await mongoose.connect(config.mongoUri); + } logger.info('MongoDB verbunden für Seeding...'); @@ -51,6 +54,8 @@ const seedDatabase = async () => { } // Seed config - always update userTypes + sections + rules + // Config NUR anlegen, niemals ueberschreiben. Vorher hat jeder Neustart + // die im Admin-Panel gepflegten Texte, Regeln und den App-Namen zurueckgesetzt. const existingConfig = await Config.findOne(); const configData = { userTypes: [ @@ -90,22 +95,23 @@ const seedDatabase = async () => { await Config.create(configData); logger.info('✅ Konfiguration erstellt'); } else { - await Config.findOneAndUpdate({}, configData, { new: true }); - logger.info('✅ Konfiguration aktualisiert'); + logger.info('ℹ️ Konfiguration existiert bereits, bleibt unveraendert'); } logger.info('✅ Datenbank-Seeding abgeschlossen'); - await mongoose.connection.close(); - process.exit(0); } catch (error) { logger.error('❌ Fehler beim Seeding:', error); - await mongoose.connection.close(); - process.exit(1); + throw error; } }; +// Verbindung schliessen und den Prozess beenden darf nur der CLI-Aufruf +// (npm run seed). server.js ruft seedDatabase() im selben Prozess auf – ein +// process.exit(0) hier hat den frisch gestarteten Server sofort wieder beendet. if (require.main === module) { - seedDatabase(); + seedDatabase() + .then(async () => { await mongoose.connection.close(); process.exit(0); }) + .catch(async () => { await mongoose.connection.close(); process.exit(1); }); } module.exports = seedDatabase; diff --git a/stoeberhunde/backend/server.js b/stoeberhunde/backend/server.js index 2586fe0..cae7a54 100644 --- a/stoeberhunde/backend/server.js +++ b/stoeberhunde/backend/server.js @@ -16,11 +16,17 @@ 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...'); + // 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(); @@ -83,17 +89,24 @@ app.get('/health', async (req, res) => { }); }); -// 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...'); +// 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(() => { @@ -101,9 +114,13 @@ process.on('SIGTERM', () => { process.exit(0); }).catch(() => process.exit(1)); }); -}); +}; -// If a frontend build exists, serve it as static files (useful for local testing) +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'); @@ -117,3 +134,6 @@ if (fs.existsSync(buildPath)) { res.sendFile(path.join(buildPath, 'index.html')); }); } + +// Error handler (must be last) +app.use(errorHandler); diff --git a/stoeberhunde/backend/utils/geocode.js b/stoeberhunde/backend/utils/geocode.js index 1c3457d..99a22cc 100644 --- a/stoeberhunde/backend/utils/geocode.js +++ b/stoeberhunde/backend/utils/geocode.js @@ -45,14 +45,13 @@ const saveCache = async () => { setInterval(saveCache, CACHE_SAVE_INTERVAL).unref(); // Save on process exit -process.on('SIGINT', async () => { - await saveCache(); - process.exit(0); -}); -process.on('SIGTERM', async () => { - await saveCache(); - process.exit(0); -}); +// Cache beim Herunterfahren sichern – ohne process.exit(): das Beenden gehört +// dem Shutdown-Handler in server.js, der sonst mittendrin abgeschnitten wird. +const flushOnShutdown = () => { + saveCache().catch(err => logger.error('Failed to flush geocoding cache:', err.message)); +}; +process.on('SIGINT', flushOnShutdown); +process.on('SIGTERM', flushOnShutdown); // Initialize cache loading loadCache().catch(err => logger.error('Cache initialization error:', err));