Modul-Review 1: Auth-Nachbesserungen (Sicherheit & UX)
Deploy App / deploy (push) Successful in 1m10s
Deploy App / deploy (push) Successful in 1m10s
Ergebnis der ersten Test- und Review-Runde zum Modul "Zugang & App-Rahmen": - Login-Timing-Ausgleich: unbekannter Benutzer wird gegen Dummy-bcrypt-Hash geprueft -> Antwortzeit verraet nicht mehr, ob ein Name existiert - Zurueck-Knopf nach Logout: pageshow-Waechter prueft die Session erneut und leitet die aus dem bfcache zurueckgeholte Ansicht auf /login - Rate-Limiting (neues lib/rate-limit.ts): Login 10/15min, Registrierung 5/h je IP, Passwortaenderung 10/15min je Benutzer; 429 + Retry-After - Passwort-Dialog laeuft neu ueber Modal -> schliesst auf Esc (Fokus-Falle, aria-modal inklusive) - Registrierungs-Fehler getrennt: nur belegter Name = 409 mit freundlicher Meldung, sonst 500 statt roher Prisma-Meldung SPEZIFIKATION 0.27 (3.1.2/3/4, neues 3.1.6). 261 -> 267 Tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+40
-6
@@ -4,10 +4,10 @@
|
||||
| | |
|
||||
|---|---|
|
||||
| **Dokument** | Funktionale und Technische Spezifikation FPT |
|
||||
| **Version** | 0.26 |
|
||||
| **Datum** | 2026-07-21 |
|
||||
| **Version** | 0.27 |
|
||||
| **Datum** | 2026-07-24 |
|
||||
| **Status** | Lebendes Dokument |
|
||||
| **Codestand** | Arbeitsstand nach `c429624` inkl. anpassbarem Pensionsalter (Branch `main`) |
|
||||
| **Codestand** | Arbeitsstand nach `d6855ef` inkl. Sicherheits-Nachbesserungen Auth (Branch `main`) |
|
||||
| **Ersetzt** | `FDD_TDD_FPT.docx` (v1–v5) im Ordner `Info Dateien` – diese sind ab Version 0.1 dieses Dokuments obsolet |
|
||||
| **Geltungsbereich** | Gesamter Code im Verzeichnis `FPT` |
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
| Version | Datum | Autor | Änderung |
|
||||
|---|---|---|---|
|
||||
| 0.27 | 2026-07-24 | Claude (Opus 4.8) | **Modul-Review 1 (Zugang & App-Rahmen): fünf Nachbesserungen an der Authentifizierung.** Ergebnis der ersten gemeinsamen Test- und Review-Runde. (1) **Timing-Ausgleich beim Login:** Ein unbekannter Benutzer wird neu gegen einen Dummy-bcrypt-Hash geprüft, damit die Antwortzeit dieselbe ist wie bei einem bekannten -- vorher liess sich aus der Dauer ablesen, ob ein Benutzername existiert (Kap. 3.1.2). (2) **Zurück-Knopf nach Logout:** Die aus dem Browser-Cache (bfcache) zurückgeholte, eingefrorene Ansicht prüft neu beim `pageshow` die Session und leitet ohne Anmeldung sofort auf `/login` -- vorher blieb die alte Ansicht sichtbar (Kap. 3.1.3). (3) **Rate-Limiting:** Login (10/15 min je IP), Registrierung (5/h je IP) und Passwortänderung (10/15 min je Benutzer) sind gegen Durchprobieren gebremst; neues In-Memory-Modul `rate-limit.ts`, 429 mit `Retry-After` (Kap. 3.1.6). (4) **Passwort-Dialog** läuft neu über die zentrale `Modal`-Komponente und schliesst damit auf Esc (Fokus-Falle, aria-modal inklusive) -- war zuvor von Hand gebaut (Kap. 3.1.4). (5) **Registrierungs-Fehler** werden sauber getrennt: nur der belegte Benutzername ist ein 409 mit freundlicher Meldung, jeder andere Fehler ein 500 statt einer rohen Prisma-Meldung als Konflikt. 6 Tests ergänzt (261 → 267). Offen für Roadmap #34: kein Passwort-Längen-Maximum (bcrypt-72-Byte-Grenze), keine ARIA-Labels auf Login/Palette. |
|
||||
| 0.26 | 2026-07-21 | Claude (Opus 4.8) | **Pensionsalter anpassen** (Roadmap Nr. 44, neue Kapitel 3.12, 4.4.7 und 4.16). Bisher war das Pensionsalter faktisch unantastbar: Es bestimmt, wo eine Lebensphase endet – ein frei geändertes Alter hätte die Phasengrenze zerrissen. Neu wird nicht das Alter geändert, sondern **die Grenze verschoben**: Die Phase davor wird länger, die danach kürzer, die Gesamtdauer bleibt gleich. Der Spielraum endet dort, wo eine angrenzende Phase unter ein Jahr fiele; ein Schritt weiter **entfällt sie ganz**, was vorher bestätigt wird, weil dabei zwei Übergänge **zusammengelegt** werden (bereits getroffene Entscheide bleiben, leere Felder werden aus dem entfallenden Übergang ergänzt, einmalige Cash-Beträge werden addiert – der Steuersatz betragsgewichtet). Das Pensionsalter ist damit auch **Treiber im Tornado** und **Regler in der Live-Simulation**. **AHV-Referenzalter (4.4.7):** Die Rente beginnt neu **immer mit 65**, unabhängig vom Pensionsalter – wer länger arbeitet, erhält sie zusätzlich zum Lohn; wer früher aufhört, zahlt bis 65 einen **Beitrag als Nichterwerbstätige(r)**, der als laufende Ausgabe auf die Verzehrquote schlägt und mit 65 wegfällt (neues Feld `ahvContribution`, ohne Default, Hilfetext nennt die reale Bandbreite von rund 530 bis 26'500 CHF pro Jahr). Beide Wechsel können **innerhalb** einer Phase liegen, die AHV wird deshalb **jahresweise** statt phasenweise gerechnet. **Punkt B:** Ein Einkommen, das einer **Person** zugeordnet ist, fällt bei deren Pensionierung auf 0 – bisher lief der Lohn stillschweigend in die Pension weiter. Gemeinsame Einkommen (Mieterträge o. Ä.) bleiben; ein ausdrücklich erfasster Betrag gewinnt, damit ein Teilzeitpensum modellierbar bleibt. **Punkt A:** Wiederkehr-Parameter (Raten, Beiträge, Amortisation, Wertsteigerung, Zinssatz) werden neu **live aus der Vorphase geerbt** statt beim Anlegen der Phase kopiert – sichtbar als angehaktes **«Aus Vorphase übernehmen»** je Feld. Vorher blieb die Kopie stehen, wenn man die Vorphase später änderte. **Punkt C:** Der Kapitalzufluss am Pensions-Übergang (PK, 3a, Verkaufserlös) lässt sich in **Prozent** auf Amortisation, Anlage und Cash aufteilen – bewusst nicht in Franken, weil sich der Betrag mit dem Pensionsalter ändert und eine Quote mitskaliert. **Nebenbei ein echter Fehler behoben:** Der fortgeschriebene Basiswert für Einkommen und Ausgaben wurde **vor** der Jahresschleife berechnet – effektive Werte kamen dadurch nie in der Folgephase an. Neues Modul `retirement.ts`, neuer Endpunkt `POST /api/scenarios/<id>/retirement`; 40 Tests ergänzt (221 → 261). |
|
||||
| 0.25 | 2026-07-21 | Claude (Opus 4.8) | **PDF-Berichte** (Roadmap Nr. 11, neues Kapitel 3.11). Neuer Unterpunkt **«Berichte»** auf Plan-Ebene: Liste der erzeugten Berichte plus Assistent zum Anlegen (Titel, Notiz, nominal **oder** real, Plan- oder effektive Daten, bis zu **drei** Szenarien, beliebige gespeicherte Analysen). Das **Layout ist immer gleich**; die Auswahl bestimmt nur, welche Bausteine erscheinen: Deckblatt mit Zusammenfassung und drei Kernaussagen, dann je Szenario Kennzahlen, Vermögensverlauf, Lebensphasen und Annahmen, danach Vergleich, Plan/Ist, Analysen und die Hinweise. **Die PDF-Datei wird als Datei abgelegt** (BYTEA in Postgres, nicht im Container-Dateisystem, das jeder Deploy neu baut): Ein Bericht muss in drei Jahren byte-identisch wieder herunterladbar sein – eine Neuerzeugung könnte das nach Änderungen an Plan, Rechenkern oder Layout nicht garantieren. **Kennzahlen je Szenario:** Endvermögen, Kapitalreichweite, Vermögen und Vorsorgekapital bei Pensionierung, AHV- und PK-Rente sowie die **offenen Entscheide** – die einzige unmittelbar handlungsleitende Zahl. Damit Bericht und Matrix nie verschiedene Zahlen nennen, liegt deren Zählung neu als reine Funktion in `decisions.ts`, die beide benutzen. **Zu jeder Kennzahl steht ihre Grundlage** als kurzer Verweis; die vollständigen Annahmen (Startwerte, Renditen, Raten je Element) stehen **einmal** je Szenario, statt bei jeder Kennzahl wiederholt zu werden. Ein **Haftungsausschluss** ist verpflichtend und durch einen Test gesichert – ein formal gesetztes PDF wird sonst als Beratung gelesen. **Technik:** `pdfkit` in der Node-Runtime statt Headless-Browser (kein Chromium im Image); `@react-pdf/renderer` schied aus, weil es mit React 19 / Next 16 bricht. Diagramme entstehen als **echte Vektoren** aus den gespeicherten Zahlen – genau dafür wurden die Analysen in 0.24 als Zahlen und nicht als Bilder abgelegt. `pdfkit` ist als externes Paket deklariert, weil es Font-Metriken über Dateipfade lädt und gebündelt erst in der Produktion bräche. Neue Tabelle `Report`, Endpunkte unter `/api/plans/<id>/reports`, neue Module `report.ts`, `report-pdf.ts`, `decisions.ts`; 9 Tests ergänzt (212 → 221). |
|
||||
| 0.24 | 2026-07-20 | Claude (Opus 4.8) | **Navigation auf Plan-Ebene und gespeicherte Analysen** (neues Kapitel 3.10). Die Seitenleiste ist neu zweistufig: Unter jedem Plan liegen die Unterpunkte **Szenarien**, **Effektive Werte** und **Analysen**; ein Klick auf den Plan-Namen öffnet ein **Plan-Dashboard** (Kennzahlen – Haushaltsdaten direkt, gerechnete Werte ausdrücklich «laut Basisszenario», dazu die Ist-Abweichung, falls erfasst). Die **Szenario-Liste** zeigt je Szenario Version, Elementzahl, Endvermögen und Ruinalter mit den Aktionen Historie und Matrix; das Basisszenario ist hervorgehoben, der Szenario-Baum in der Seitenleiste bleibt daneben erhalten. Die **Analysen-Ansicht** bietet vier umklappende Kacheln (Grafiken, Live-Simulation, Monte-Carlo, Einflussfaktoren – Umklappen auch per Antippen für Touch). **Grafiken** öffnen neu nicht mehr alle drei Diagramme, sondern lassen zuerst **eines** wählen; der Szenario-Vergleich zieht mit zu den Grafiken, der CSV-Export auf die Matrix. **Gespeicherte Analysen:** Jede Grafik, MC-Simulation und Einflussfaktoren-Berechnung kann festgehalten werden – als **Zahlen, nicht als Bild** (read-only, es wird nichts neu gerechnet). Das hält den Datensatz klein und macht ihn druckfähig: Der spätere PDF-Bericht (Roadmap Nr. 11) zeichnet daraus vektoriell in Druckauflösung, was ein Screenshot nicht könnte. Bewusst **nicht** gespeichert wird `finalWealthSorted` (megabyteweise). Jedes Werkzeug schreibt sein Ergebnis in dieselbe generische Form, sodass eine Nur-Lese-Ansicht genügt. Neue Tabelle `SavedAnalysis`, neue Endpunkte unter `/api/plans/<id>/analyses` und `/dashboard`; neue Komponenten `PlanViews`, `SavedAnalysisView`, `SaveAnalysisButton`, neues Modul `analyses.ts`. Kein Eingriff in den Rechenkern; Testbestand 212 (Migration um `SavedAnalysis` erweitert). |
|
||||
@@ -222,7 +223,9 @@ Referenz: `src/lib/users.ts` Zeilen 6–22, `src/app/api/auth/register/route.ts`
|
||||
|
||||
- Benutzername + Passwort; Prüfung via `bcrypt.compare`.
|
||||
- Fehlermeldung ist bewusst unspezifisch: „Benutzername oder Passwort falsch." (HTTP 401) –
|
||||
verrät nicht, ob der Benutzer existiert.
|
||||
verrät nicht, ob der Benutzer existiert. Auch die **Antwortzeit** verrät es nicht: Ein
|
||||
unbekannter Benutzer wird gegen einen Dummy-bcrypt-Hash geprüft, damit die Dauer dieselbe
|
||||
ist wie bei einem bekannten mit falschem Passwort (`TIMING_DUMMY_HASH` in `users.ts`).
|
||||
- Bei Erfolg: JWT (HS256, Payload `{ userId }`, Gültigkeit 30 Tage) im HttpOnly-Cookie
|
||||
`fpt_session` (`sameSite=lax`, `secure` nur in Produktion, `maxAge` 30 Tage).
|
||||
|
||||
@@ -234,11 +237,21 @@ Referenz: `src/app/api/auth/login/route.ts`, `src/lib/auth.ts`.
|
||||
bereits kopiertes Token bis zum Ablauf technisch gültig – es gibt keine serverseitige
|
||||
Token-Sperrliste.
|
||||
|
||||
Nach dem Abmelden führt der **Zurück-Knopf** des Browsers zur zuletzt gezeigten Ansicht aus
|
||||
dem *bfcache* zurück -- ohne neue Anfrage, also ohne dass Middleware oder Session-Prüfung
|
||||
greifen. Damit die eingefrorene, scheinbar noch angemeldete Ansicht nicht stehen bleibt,
|
||||
prüft die Hauptseite beim `pageshow`-Ereignis (nur bei einer aus dem bfcache zurückgeholten
|
||||
Seite) erneut `/api/auth/me` und leitet ohne gültige Session sofort auf `/login`. Es sind
|
||||
dabei nie echte Daten freigegeben -- das Cookie ist gelöscht --, aber die alten Zahlen sollen
|
||||
auf einem geteilten Rechner gar nicht erst wieder sichtbar werden. Referenz: `src/app/page.tsx`.
|
||||
|
||||
### 3.1.4 Passwortänderung
|
||||
|
||||
Im Profilmenü. Erfordert das aktuelle Passwort; das neue Passwort muss ≥ 6 Zeichen haben und
|
||||
wird im Dialog gegen eine Wiederholung geprüft (Client-seitig). Nach Erfolg erscheint 1.2 s lang
|
||||
„Passwort geändert.", dann schliesst der Dialog.
|
||||
„Passwort geändert.", dann schliesst der Dialog. Der Dialog läuft über die zentrale
|
||||
`Modal`-Komponente und schliesst damit auch auf **Esc** (Fokus-Falle und `aria-modal`
|
||||
inklusive, siehe 3.7.6).
|
||||
|
||||
Referenz: `src/app/api/auth/change-password/route.ts`, `src/components/ProfileMenu.tsx` Zeilen 117–140.
|
||||
|
||||
@@ -255,6 +268,26 @@ Zweistufig:
|
||||
`getOwnedElement` in `src/lib/queries.ts`). Ein fremder Datensatz führt zu HTTP 404
|
||||
(nicht 403) – die Existenz wird nicht preisgegeben.
|
||||
|
||||
### 3.1.6 Missbrauchsschutz (Rate-Limiting)
|
||||
|
||||
Login, Registrierung und Passwortänderung sind gegen wiederholtes Durchprobieren gebremst
|
||||
(`src/lib/rate-limit.ts`):
|
||||
|
||||
| Endpunkt | Grenze | Schlüssel |
|
||||
|---|---|---|
|
||||
| `POST /api/auth/login` | 10 / 15 min | Client-IP |
|
||||
| `POST /api/auth/register` | 5 / 60 min | Client-IP |
|
||||
| `POST /api/auth/change-password` | 10 / 15 min | Benutzer-ID |
|
||||
|
||||
Bei Überschreitung: **HTTP 429** mit lesbarer Meldung und `Retry-After`-Header. Der Zähler ist
|
||||
ein **In-Memory-Fixed-Window** – bewusst einfach, weil das Tool in einem einzigen Container
|
||||
läuft; nach einem Deploy ist er leer. Die Client-IP kommt aus `X-Forwarded-For` (Traefik);
|
||||
ohne Header fallen alle auf denselben Eimer, was im Zweifel eher zu stark als zu schwach
|
||||
bremst. Für einen Mehrinstanz-Betrieb müsste der Zähler nach Redis o. Ä. wandern (Roadmap #34).
|
||||
|
||||
**Bewusst offen (Roadmap #34, Pentest):** kein Passwort-Längen-Maximum (bcrypt prüft still nur
|
||||
die ersten 72 Byte), keine ARIA-Labels auf der Login-Maske und der Befehls-Palette.
|
||||
|
||||
## 3.2 Plan-Verwaltung
|
||||
|
||||
### 3.2.1 Plan erstellen
|
||||
@@ -3765,7 +3798,8 @@ Include `src/**/*.test.ts`). Es gibt **keine** Komponenten-, API- oder E2E-Tests
|
||||
| `server-boundary.test.ts` | 1 | statischer Wächter: kein Modul unter `src/lib` importiert aus `src/components` |
|
||||
| `diff.test.ts` | 9 | Abweichungs-Erkennung gegen das Eltern-Szenario |
|
||||
| `migrations.test.ts` | 3 | spielt alle Migrationen gegen echtes PostgreSQL (PGlite) ein; prüft zusätzlich die V7-**Datenübernahme** (Basisszenario gewinnt, Pensionsalter bleiben szenario-eigen) |
|
||||
| **Total** | **261** | |
|
||||
| `rate-limit.test.ts` | 6 | Fixed-Window: erlaubt bis Limit, blockt danach, startet nach Fensterablauf neu, trennt je Schlüssel; Client-IP aus X-Forwarded-For / X-Real-IP |
|
||||
| **Total** | **267** | |
|
||||
|
||||
## 8.2 Testfälle
|
||||
|
||||
|
||||
Reference in New Issue
Block a user