Plan, Zusammenfassung, Verifikation und STATE.md zum Quick-Vorgang 260921-fi3. Befund: auth.service legt mustChangePassword in den JWT, jwt.strategy liess das Feld beim Auspacken fallen. request.user.mustChangePassword war damit immer undefined und der global registrierte ForcePasswordChangeInterceptor hat seit seiner Einfuehrung nie blockiert. Durchgesetzt wurde der Zwangswechsel allein von der Web-Middleware; jeder Weg daran vorbei umging ihn. Gemessen: eine Sitzung mit mustChangePassword=true erhielt auf GET /users 200 samt vollstaendiger Benutzerliste. Behoben, und belegt bei gleicher Rolle und gleicher Route: GET /modules/active liefert 403 FORCE_PASSWORD_CHANGE mit Zwang und 200 ohne. Neue Spezifikationen gegen den alten Stand 6 von 12 rot, danach 12 von 12 gruen, vom Verifier unabhaengig nachgestellt. Der Browser-Ablauf wurde vollstaendig durchgespielt: niemand wird ausgesperrt, der Wechsel gelingt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
16 KiB
phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | actuals | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | coverage | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260921-fi3 | 01 | auth |
|
|
|
|
|
|
|
|
|
|
|
~55min | 2026-09-21 | complete |
Quick 260921-fi3: Erzwungener Passwortwechsel wirklich durchgesetzt Summary
JwtStrategy liess mustChangePassword auf dem Weg zum Interceptor fallen — der global registrierte ForcePasswordChangeInterceptor hat seither nie etwas blockiert; jetzt durchgesetzt und mit einem Nahttest gepinnt, der nachweislich gegen den alten Quelltext scheitert.
Performance
- Dauer: ca. 55 Minuten
- Abgeschlossen: 2026-09-21
- Aufgaben: 3/3
- Geänderte Dateien: 8
Accomplishments
- Die eigentliche Ursache behoben:
JwtStrategy.validatereichtmustChangePasswordjetzt durch (strenger Vergleich mittrue), statt dass der Interceptor auf ein Feld prüft, das nie ankam. - Die Erlaubnisliste des Interceptors von Teilstring-Vergleich auf exakten Abgleich von Methode und Pfad umgestellt — schließt die Teilstring-Falle, ohne dass es heute eine ausnutzbare Route dafür gäbe.
- Am laufenden System nachgewiesen: eine Sitzung mit gesetzter Kennzeichnung bekommt jetzt
403auf/usersund/modules/active(vorher200), während/auth/me,/auth/change-passwordund/auth/logout— die drei Wege, die der Zwangswechsel im Browser tatsächlich braucht — unverändert erreichbar bleiben. VehicleTable: das bisher ungelesene Beschäftigt-Kennzeichen (const [, setIsDeleting]) wieder lesbar gemacht, Dialogschaltflächen währenddessen gesperrt,confirmDeletebricht bei bereits laufender Löschung selbst ab — ein Doppelklick löst nachweislich nur einDELETEaus.- Alle sieben fest verdrahteten Texte und sechs Vorlesehilfen der Fahrzeugtabelle jetzt über next-intl, sieben neue Schlüssel im Bereich
dkvFleet, deckungsgleich inde.jsonunden.json.
Task Commits
Jede Aufgabe wurde atomar committet:
- Aufgabe 1: Erzwungenen Passwortwechsel an der API wirklich durchsetzen -
f7c02b7(fix) - Aufgabe 2: Fahrzeugtabelle — Doppelauslösung sperren, next-intl -
e56cce4(fix) - Aufgabe 3: Gesamtabnahme - keine eigene Quelldatei-Änderung (reine Abnahme), Ergebnisse unten dokumentiert; fließt in den Abschluss-Commit dieses Berichts.
Beide Aufgaben mit Code-Änderung liefen TDD (tdd="true"): Testdateien zuerst, RED-Lauf gegen den alten Quelltext protokolliert, dann Quelltext angepasst bis GREEN.
Files Created/Modified
apps/api/src/auth/strategies/jwt.strategy.ts-validatereichtmustChangePasswordjetzt durch (strenger Vergleich mittrue)apps/api/src/auth/strategies/jwt.strategy.spec.ts- neu; pinnt Durchreichung, Alt-Sitzungs-Fallback, unveränderte Feldweitergabeapps/api/src/auth/interceptors/force-password-change.interceptor.ts- Erlaubnisliste auf exakten Methode+Pfad-Abgleich umgestellt, Kopfkommentar korrigiertapps/api/src/auth/interceptors/force-password-change.interceptor.spec.ts- neu; Nahttest, Erlaubnisliste, Teilstring-Falle, Kennzeichnung nicht gesetzt, öffentliche Route, keinrequest.userapps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.tsx- Reentrancy-Sperre gegen Doppelklick, alle Texte/Vorlesehilfen über next-intlapps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.test.tsx- drei neue Testfälle, Testabbau-Fehler behoben (vi.clearAllMocks())apps/web/src/messages/de.json- sieben neue Schlüssel im BereichdkvFleet(form.saveRow,form.deleteConfirm, fünferrors.*)apps/web/src/messages/en.json- dieselben sieben Schlüssel, englische Übersetzung
Decisions Made
load()inVehicleTablebehält bewusst[]als Abhängigkeitsfeld statt[t]: der next-intl-Mock im Test erzeugt bei jedem Aufruf vonuseTranslationseine neue Funktionsidentität; hätteloadvontabgehangen, wäreloadbei jedem Render neu erzeugt worden und deruseEffect, derloadbeim Mount aufruft, hätte bei jedem Render erneut ausgelöst — ein Abruf-Loop, der in der Praxis (echtes next-intl memoisiertt) nicht auftritt, aber im Test sofort sichtbar wurde, weil er den Mock-Warteschlangenzustand für spätere Testfälle leerte.vi.restoreAllMocks()im Testabbau vonVehicleTable.test.tsxdurchvi.clearAllMocks()ersetzt:restoreAllMocksleert die Aufrufzählung reinervi.fn()-Mocks (ohne echtes Original) nicht zuverlässig, wodurch der neue Doppelklick-Test (toHaveBeenCalledTimes(1)) eine aus dem vorigen Testfall übertragene Zählung sah. Dieser Fund war vorher unsichtbar, weil bisher kein Test die Aufrufzahl prüfte, nurtoHaveBeenCalledWith(...).
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] load() mit t als Abhängigkeit hätte einen Abruf-bei-jedem-Render-Zyklus ausgelöst
- Gefunden während: Aufgabe 2, beim ersten vollen Testlauf der Datei (nicht isoliert)
- Problem: Beim Verdrahten der Ladefehlermeldung über
t('errors.loadVehiclesFailed')wurdetnaheliegend inloads Abhängigkeitsfeld aufgenommen ([t]). Da der next-intl-Mock im Test bei jedem Aufruf eine neue Funktion zurückgibt, wurdeloadbei jedem Render neu erzeugt, und deruseEffect, derloadbeim Mount aufruft, löste dadurch bei jedem Render erneut aus — ein bestehender, vorher grüner Testfall ("clicking pencil icon...") schlug dadurch fehl, weil die Mock-Warteschlange eines späteren Tests durch die vielen zusätzlichen Aufrufe verzerrt wurde. - Fix: Abhängigkeitsfeld auf
[]zurückgesetzt (wie im Ausgangszustand), mit Kommentar, der begründet, warumthier bewusst fehlt. - Dateien geändert:
apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.tsx - Verifikation: Vollständiger Testlauf der Datei wieder grün (8/8)
- Committed in:
e56cce4(Teil des Aufgabe-2-Commits)
2. [Rule 1 - Bug] vi.restoreAllMocks() leerte die Aufrufzählung von mockDeleteVehicle nicht zwischen Testfällen
- Gefunden während: Aufgabe 2, beim Verifizieren des neuen Doppelklick-Testfalls
- Problem: Der neue Testfall erwartete
mockDeleteVehiclegenau einmal aufgerufen; tatsächlich zeigte er zwei Aufrufe. Debugging (temporäresconsole.loginconfirmDelete, per Backup-Datei wieder entfernt) zeigte:confirmDeletewurde in diesem Testfall nur einmal ausgeführt — der zweite gezählte Aufruf war aus dem vorigen Testfall ("clicking trash icon...") übrig geblieben, weilvi.restoreAllMocks()im Testabbau die Aufrufliste reinervi.fn()-Mocks nicht zurücksetzt. - Fix:
vi.restoreAllMocks()durchvi.clearAllMocks()ersetzt, mit erklärendem Kommentar. - Dateien geändert:
apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.test.tsx - Verifikation: Vollständiger Testlauf der Datei grün (8/8), Doppelklick-Test zählt jetzt korrekt genau einen Aufruf
- Committed in:
e56cce4(Teil des Aufgabe-2-Commits)
Gesamtzahl Abweichungen: 2 automatisch behoben (beide Regel 1 — Fehlerkorrektur, beide beim Testen der eigenen neuen Testfälle in Aufgabe 2 gefunden, nicht im Ausgangscode) Auswirkung auf den Plan: Beide Korrekturen waren nötig, damit die vom Plan geforderten neuen Testfälle tatsächlich das prüfen, was sie behaupten zu prüfen. Kein Umfang über den Plan hinaus.
Issues Encountered
Keine offenen Probleme. Die beiden oben dokumentierten Deviations sind bereits gelöst.
User Setup Required
Keine — keine externe Dienstkonfiguration nötig.
Gesamtabnahme (Aufgabe 3)
Fünf Zahlen vorher/nachher
| Kennzahl | Ausgangsstand | Nachher | Erwartung erfüllt? |
|---|---|---|---|
pnpm type-check |
4/4 | 4/4 | ja |
pnpm test apps/api |
69 Dateien / 1124 Tests | 71 Dateien / 1136 Tests | ja (+2 Dateien, +12 Tests aus Aufgabe 1) |
pnpm test apps/web |
66 Dateien / 459 Tests | 66 Dateien / 462 Tests | ja (+3 Tests aus Aufgabe 2, keine neue Datei) |
pnpm lint |
5/5, 0 Fehlerstufe | 5/5, 0 Fehlerstufe | ja |
| Warnungssumme | 464 (api 357, web 107) | 466 (api 358, web 108) | ja — genau an der erlaubten Obergrenze von 466, nicht darüber |
Die zwei zusätzlichen Warnungen sind identifiziert und bewusst belassen (keine bestehende Unterdrückungs-Konvention im Projekt — es gibt an keiner Stelle in apps/api oder apps/web einen biome-ignore-Kommentar, das Projekt führt Lint-Warnungen stattdessen als gezählten Rückstand):
apps/api/src/auth/interceptors/force-password-change.interceptor.ts:33—lint/suspicious/noExplicitAnyan der neuennormalizePath(request: any)-Hilfsfunktion. Das bestehende Muster derselben Datei (intercept(...): Observable<any>, unverändert) verwendet ebenfallsanyfür den Request/Response-Rahmen von NestJS-Interceptoren.apps/web/src/app/(portal)/modules/dkv-fleet/settings/components/VehicleTable.tsx:182—lint/correctness/useExhaustiveDependenciesanload, weiltabsichtlich aus dem Abhängigkeitsfeld ausgeschlossen ist (siehe Decisions Made oben — mittin den Abhängigkeiten löst der next-intl-Mock im Test einen Abruf-Loop aus).
HTTP-Messung aus Aufgabe 1
| Route | Methode | Ergebnis mit gesetzter Kennzeichnung | Bewertung |
|---|---|---|---|
/users |
GET | 403 | erwartet — vorher 200 |
/modules/active |
GET | 403 | erwartet — vorher ungeprüft, jetzt durchgesetzt |
/auth/me |
GET | 200 | erwartet — Erlaubnisliste greift |
/auth/change-password |
POST | 401 | erwartet — Fachfehler des Handlers (falsches aktuelles Passwort), NICHT 403; beweist, dass die Erlaubnisliste durchlässt und der Handler selbst entscheidet |
/auth/logout |
POST | 200 | erwartet — Erlaubnisliste greift |
Nach der Messung wurde mustChangePassword für admin in der Datenbank wieder auf false gesetzt.
Befund 3 (SplitTab.tsx) — Begründung in einem Satz
SplitTab.tsx bleibt unangetastet: downloadAllAsZip liegt auf Modulebene und kann den next-intl-Hook nicht aufrufen, ein übersetzter Downloadname bringt echte Nachteile (Umlaute auf Windows-Freigaben, sprachabhängige Anhangsnamen) ohne Nutzen (ein Dateiname wird nie vorgelesen oder gesucht), und die Dateien im Archiv tragen ohnehin die vom Server gelieferten Zertifikatsnamen.
Datenbank-Endstand
admin|f
nutzer2|f
nutzer1|f
Alle drei Nutzer stehen wieder auf dem Ausgangsstand — kein Nutzer trägt die Zwangswechsel-Kennzeichnung.
Next Phase Readiness
Kein Folge-Plan in dieser Kette. Die drei Befunde aus der Lint-Rückstandsaufgabe 260921-bi2 sind damit vollständig abgearbeitet: Befund 1 (Sicherheitslücke) behoben und mit einem RED→GREEN-Nahttest gepinnt, Befund 2 (Doppelauslösung/next-intl) behoben, Befund 3 (SplitTab.tsx-Dateiname) bewusst nicht angefasst, begründet oben und im Plan.
Self-Check: PASSED
Alle acht geänderten/neu erstellten Quelldateien sowie diese Zusammenfassung wurden auf Existenz geprüft (FOUND für jede). Beide Task-Commits (f7c02b7, e56cce4) wurden in git log --oneline --all gefunden.