fix(backend): Seeding beendet nicht mehr den Server, Routing und Shutdown korrigiert
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
4b61db82fd
commit
718d84a545
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in New Issue