Beim Hochfahren des echten Stacks aufgefallen: d9fecb6 mountet ein Named
Volume auf einen DATEIpfad (geocode-cache:/app/geocode-cache.json). Ein Named
Volume ist immer ein Verzeichnis — im laufenden Container liegt an der Stelle
also ein Ordner. Jeder Lese- und Schreibversuch lief in EISDIR und wurde von
den try/catch-Bloecken in utils/geocode.js still verschluckt: der Cache wurde
nie geladen und nie gespeichert.
Im laufenden drohnenfuehrer-backend nachgestellt:
Schreiben schlaegt fehl: EISDIR - illegal operation on a directory
Lesen schlaegt fehl: EISDIR
- Der Cache-Pfad ist jetzt ueber GEOCODE_CACHE_FILE konfigurierbar und faellt
ohne die Variable auf den bisherigen Ort zurueck.
- Das Volume wird als Verzeichnis unter /app/cache gemountet, die Compose-
Dateien setzen GEOCODE_CACHE_FILE=/app/cache/geocode-cache.json.
- .dockerignore schliesst geocode-cache.json aus: eine bei lokalen Tests
entstandene Datei war ueber COPY . . ins Image gewandert und liess den
Container gar nicht mehr starten ("not a directory").
Geprueft: im neu gebauten nachsuche-backend ist /app/cache/geocode-cache.json
schreibbar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Der Geocode-Cache war nur auf dem Erfolgspfad begrenzt. Die beiden
Negativ-Pfade (cache.set(key, null)) schrieben ungebremst — über den
öffentlichen GET /api/public/geocode genügten erfundene Postleitzahlen,
um Speicher und die alle 60 s geschriebene Cache-Datei beliebig wachsen
zu lassen. Bei NODE_OPTIONS=--max_old_space_size=256 eine reale Grenze.
Alle Schreibpfade laufen jetzt über rememberInCache(); loadCache()
beschneidet eine bereits gewachsene Datei auf das Limit.
Geprüft: eine künstlich auf 5000 Einträge aufgeblähte Cache-Datei wird
beim Start auf 1000 reduziert.
- Der Cache war trotz Kommentar kein LRU, sondern ein FIFO: ein Treffer
hat den Eintrag nicht ans Ende gerückt, häufig genutzte Adressen flogen
zuerst raus. Treffer werden jetzt neu einsortiert.
- /api/public/geocode bekommt einen eigenen Limiter (20/15 min). Der
Endpunkt löst ausgehende Nominatim-Anfragen aus und teilt sich mit dem
Geocoding im Admin-Bereich die globale Wartezeit von 1100 ms.
Geprüft: Anfragen 21-24 liefern 429.
- resetPassword prüfte 6 Zeichen, das Admin-Schema verlangt 12. Passwörter
dazwischen kamen durch und scheiterten erst an der Mongoose-Validierung
— beim Nutzer kam "Serverfehler" (500) an. Jetzt 400 mit klarer Meldung.
- APP_URL wird aktiv gesetzt statt auskommentiert, und der Rückfallwert
hängt den Unterpfad an. Vorher fiel appUrl auf CORS_ORIGIN zurück, und
der Link in der Reset-Mail zeigte auf das Portal statt in die App.
Geprüft: erzeugte URL lautet .../nachsuche/passwort-zuruecksetzen?token=…
- Die beim Zusammenführen verlorengegangene Platzhalter-Prüfung für
JWT_SECRET ist wieder da (fängt auch CHANGE_ME_IN_PRODUCTION).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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>
Ergänzt die Absicherung aus 8384ad9/d9fecb6 um die Lücken, die dort offen
geblieben sind.
- authenticateToken prüft jetzt die Rolle. Der C-01-Fix vergleicht nur
`decoded.app`, und genau den Claim tragen Handler-Tokens nicht
({ id, role: 'handler' }) – `if (decoded.app && ...)` lief bei ihnen
also ins Leere. Ein eingeloggter Hundeführer konnte seinen Bearer-Token
weiterhin gegen /api/users, /api/config und /api/audit-logs schicken und
hatte volle Admin-Rechte. Admin-Tokens tragen zusätzlich role: 'admin';
Tokens ohne role bleiben bis zum Ablauf gültig, niemand wird ausgeloggt.
- Logout wird als LOGOUT protokolliert statt als LOGIN, und die Route
liest den Benutzernamen über attachUserIfPresent (neu). Vorher lag gar
keine Auth-Middleware auf /logout, der Eintrag lautete immer "unknown".
- forgotPassword sucht case-insensitive wie der Login. Ein als "Thorsten"
angelegtes Konto konnte sich als "thorsten" anmelden, aber kein Passwort
zurücksetzen.
- CSV-Export escaped führende =, +, -, @ (neues utils/csv.js, von
userController und auditController gemeinsam genutzt). Ein Name wie
=HYPERLINK(...) wurde sonst von Excel als Formel ausgeführt.
- Massen-Operationen filtern soft-deleted Einträge; der pre(/^find/)-Hook
des Modells greift bei updateMany nicht.
- Die öffentliche Liste liefert wieder phone, landline, address und photo.
Die UI zeigt diese Felder seit jeher an, die API lieferte sie seit dem
PII-Commit nicht mehr – Anrufen war öffentlich nicht mehr möglich.
E-Mail, Hashes und Invite-Felder bleiben ausgeschlossen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>