- PasswordReset las den Query-Parameter token nie aus. Wer auf den Link
in der Reset-Mail klickte, landete wieder auf Schritt 1 mit leerem
Token-Feld — der Mailversand aus d9fecb6 lief damit ins Leere. Der
Token wird jetzt aus der URL übernommen und direkt Schritt 2 gezeigt.
- "Zurück zum Login" und die Weiterleitung nach dem Zurücksetzen zeigten
auf "/" und damit beim Unterpfad-Deployment auf das Portal.
- PublicUserList rief JSON.parse(localStorage.getItem(...)) ungeschützt
im Render-Pfad auf. Ein beschädigter Eintrag — oder ein Browser, der
Site-Data blockiert — ließ die gesamte öffentliche Liste weiß werden.
Lesen und Schreiben laufen jetzt über try/catch, und der Wert wird auf
Plausibilität geprüft.
- Der Typfilter-Effekt feuerte auch beim Mounten, obwohl useUsers bereits
selbst lädt: jeder Aufruf der öffentlichen Liste setzte zwei identische
Anfragen ab. Der erste Lauf wird jetzt übersprungen.
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>
nginx vererbt add_header nicht in Blöcke, die eigene add_header setzen.
Dadurch gingen X-Frame-Options, X-Content-Type-Options und
X-XSS-Protection ausgerechnet bei den HTML-Antworten verloren – jeder
location-Block mit Cache-Control hat sie stillschweigend abgeschaltet.
Verifiziert: nginx -t ist für alle drei Konfigurationen erfolgreich.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- UserCard.js ließ sich nicht übersetzen: die öffnende Hälfte des Ternary
um den GPS-Bereich ({isEditingGPS ? () fehlte, während das ") : (" und
das ")}" stehen geblieben waren. isEditingGPS wurde dadurch nur noch
gesetzt, nie gelesen. Da die Datei in allen drei Apps identisch ist,
scheiterte überall der Build.
- Der Einladungs-Flow aus 8384ad9 war nur zur Hälfte da: Backend und
Login-Formular verlangen einen Invite-Token, aber kein Frontend hat
/invite-token je aufgerufen – der Admin konnte gar keinen erzeugen und
ein Führer ohne Passwort kam nicht ins System. Die Benutzerkarte hat
jetzt einen Button dafür (nur bei hinterlegter E-Mail), verdrahtet über
UserList und AdminPanel.
- api.js: bei einem Fehler ohne Request-Config lief der Retry-Zweig in
"config.retry = 2" auf undefined und verdeckte den echten Fehler.
- useUsers: "mehr laden" hat die aktiven Filter verworfen und die
ungefilterte zweite Seite angehängt. Mit dem serverseitigen Typfilter
aus d9fecb6 fällt das jetzt deutlich stärker auf.
- Die öffentliche Liste zeigte statt des Telefon-Emojis ein kaputtes
Ersatzzeichen.
- Der Logo-Upload nannte SVG als erlaubtes Format, das Backend lehnt es
bewusst ab.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Build läuft über Vite ("build": "vite build"), der Code las
Umgebungsvariablen aber über process.env – das gibt es im Browser-Bundle
nicht. Damit war der Build-Arg REACT_APP_API_URL wirkungslos und der
Zugriff selbst lief in "process is not defined".
- constants.js nutzt import.meta.env und leitet den Deployment-Basispfad
aus import.meta.env.BASE_URL ab (Fallback: Laufzeit-Erkennung).
REACT_APP_* heißt überall VITE_* (Dockerfile, Compose, .env.example,
Doku).
- /verwaltung und /passwort-zuruecksetzen wurden gegen
window.location.pathname ohne Basispfad verglichen und trafen unter
/nachsuche/ nie zu – die Passwort-Reset-Seite war produktiv nicht
erreichbar. Neue Helfer withBase()/normalizePath().
- Der Service Worker wurde als '/sw.js' registriert und war damit der des
Portals mit Scope '/'. Registrierung läuft jetzt über BASE_URL, inkl.
passendem Scope.
- index.html verlinkte '/manifest.json' absolut, also das Portal-Manifest:
die App hätte sich als Portal installiert. Jetzt %BASE_URL%.
- public/index.html (CRA-Rest) kollidierte im Vite-Build mit der
index.html im Projektwurzelverzeichnis und wurde entfernt.
- manifest.json verwies auf ein favicon.ico, das es nicht gibt.
- package.json: Scripts auf Vite umgestellt. react-scripts,
@testing-library/* und web-vitals entfernt – nichts davon wird
importiert, und react-scripts stand im Widerspruch zum Build.
- Die ungenutzten Konstanten USER_TYPES/USER_TYPE_LABELS/RULES sind
entfallen (die echten Werte kommen aus der Datenbank).
- Header bekam eine Prop onXLogin, die er gar nicht entgegennimmt und die
den Logout-Handler durchreichte.
Verifiziert: alle drei Frontends bauen, im Bundle kein process.env mehr,
alle Pfade korrekt auf den jeweiligen Basispfad.
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>
- Docker: bind all backend/frontend ports to 127.0.0.1 only (was 0.0.0.0)
- Docker: add shared jagd-network; portal uses container names instead of host ports
- Fix: set-password endpoints now require valid invite token (drohnenfuehrer, stoeberhunde)
- Fix: auth cookie secure flag enabled in production
- Fix: password reset token no longer logged in production
- Add: inviteLimiter (10/15min) on set-password routes in all three apps
- Add: importUsers capped at 500 entries to prevent DoS
- Refactor: rename handler -> drohnenfuehrer/stoeberhundefuehrer across all apps
- Dockerfile: icon-192.png und icon-512.png in Container aufgenommen
- manifest.json: favicon.ico entfernt (wurde als text/html geliefert)
- manifest.json: Cache-Busting ?v=2 für alle Icons
- index.html: Logo-Bild auf ?v=2 aktualisiert
- extra8002.conf: manifest.json Content-Type auf application/manifest+json gesetzt
- Alle Sub-App manifest.json: scope von '/' auf jeweiligen Pfad korrigiert
(/nachsuche/, /drohnenfuehrer/, /stoeberhunde/)
- icon-192.png und icon-512.png: aus logo-fallingbostel.png generiert