Summary
Platform hat aktuell keinen Weg für User, ihr Passwort zurückzusetzen (wenn vergessen) oder zu ändern (wenn eingeloggt). Dieses Issue umfasst beide Flows in einem PR, da sie sich den Dex-UpdatePassword-gRPC-Wrapper und die Validierungslogik teilen.
Scope:
- Forgot Password — Email-basierter Reset-Flow für nicht-eingeloggte User
- Change Password — In-Session-Änderung für eingeloggte User (altes Passwort als Bestätigung)
Current state
actions/register.ts erstellt User in Dex via createDexUser() + TinyBase + Email-Verification (file-token pattern)
lib/email-verification.ts — file-based token store (data/verification-tokens/<token>.json, 24h TTL, single-use, crypto.randomBytes(32))
lib/dex/client.ts — wrappt CreatePassword und VerifyPassword, aber noch nicht UpdatePassword (existiert in api.proto Zeile 278)
app/(auth)/ hat nur register/ und signin/ — kein forgot-password/ oder reset-password/
Open design questions
Q1 — Token TTL für Reset-Link
- A) 1 Stunde (Industry-Standard für Password-Reset, kurzlebig)
- B) 24 Stunden (konsistent mit Email-Verification)
- Empfehlung: A — Reset-Tokens sind sensibler als Verification-Tokens, kürzere TTL reduziert Risiko bei Email-Kompromittierung.
Q2 — Email-Enumeration-Schutz
- A) Immer gleiche Antwort: "Wenn die Email existiert, haben wir einen Link geschickt"
- B) Direktes Feedback: "Kein Account für diese Email gefunden"
- Empfehlung: A — verhindert User-Enumeration über den Reset-Endpoint. Standard-Security-Pattern.
Q3 — Sessions nach Reset invalidieren
- A) Ja — alle bestehenden Sessions ungültig, User muss überall neu einloggen
- B) Nein — nur Passwort tauschen, Sessions bleiben
- Empfehlung: A — wichtig falls Account kompromittiert ist. Technisch: NextAuth-Session-Tokens + Dex
offline-sessions für den User löschen.
Q4 — Rate-Limiting
- A) Jetzt implementieren (z.B. max 3 Requests/Stunde pro Email+IP)
- B) Später, als eigenes Cross-Cutting-Issue (betrifft auch Register, Login, Verify)
- Empfehlung: B — Rate-Limiting ist ein separates Concern, gehört nicht isoliert in diesen PR. Als Interim-Schutz: Reset-Endpoint sendet keinen neuen Token, solange ein noch gültiger existiert (dämpft Email-Spam automatisch).
Proposed design (pending Q1–Q4)
New files
app/(auth)/forgot-password/page.tsx — Email-Input, triggert Reset-Request
app/(auth)/reset-password/page.tsx — Passwort-Formular mit Token-Param
app/settings/password/page.tsx — Change-Password (eingeloggt, alt+neu)
actions/password-reset.ts — requestPasswordReset, confirmPasswordReset
actions/password-change.ts — changePassword (nutzt verifyDexPassword für alt)
lib/password-reset.ts — Token-Management analog email-verification.ts, separater Ordner data/password-reset-tokens/
Modified files
lib/dex/client.ts — neue Funktion updateDexPassword(email, newPassword)
components/form/SignInForm.tsx (oder equivalent) — Link "Passwort vergessen?"
- Sidebar/Profile-Menu — Link zu
/settings/password
Email-Template
- Neue Template-Funktion
sendPasswordResetEmail() in lib/password-reset.ts — spiegelt sendVerificationEmail()
- Subject: "Reset your Enopax password", Link:
\${BASE_URL}/reset-password?token=\${token}
Flow — Forgot Password
- User →
/forgot-password, trägt Email ein
- Action
requestPasswordReset(email):
- TinyBase-User per Email suchen
- Wenn existiert: Token erzeugen, Email senden
- Wenn nicht: trotzdem Success-Response (Q2=A)
- User klickt Email-Link →
/reset-password?token=...
- Neues Passwort + Bestätigung
- Action
confirmPasswordReset(token, newPassword):
- Token verifizieren + löschen
updateDexPassword(email, newPassword) (Q1=A: 1h TTL)
- NextAuth-Session des Users invalidieren (Q3=A)
- Redirect →
/signin mit Success-Flash
Flow — Change Password
- Eingeloggter User →
/settings/password
- Formular: aktuelles Passwort, neues Passwort, Bestätigung
- Action
changePassword(current, new):
verifyDexPassword(session.email, current) — wenn falsch → Fehler
updateDexPassword(session.email, new)
- Optional (Q3 extension): andere Sessions des Users invalidieren
- Success-Flash, Seite reloaden
Security decisions baked in
- Password-Hash via
bcrypt-ts (konsistent mit createDexUser)
- Min-Length 8 Zeichen (konsistent mit Register)
- Tokens:
crypto.randomBytes(32).toString('hex') (konsistent mit email-verification)
- Single-use-Tokens, file-based persistence in
data/password-reset-tokens/ mit mode 0o600
Testing
- Jest-Tests für
lib/password-reset.ts (Token-Lifecycle: create, verify, expire, single-use)
- Jest-Tests für
actions/password-reset.ts und actions/password-change.ts (Mock Dex-Client)
- Manueller E2E-Test gegen Dev-Dex
- Security-Smoke: Token-Reuse blocked, Expired-Token blocked, Falscher-Alt-Passwort blocked
Acceptance criteria
Out of scope
- Rate-Limiting (separates Issue, Q4=B)
- 2FA/MFA (getrennte Initiative)
- Password-Complexity-Rules jenseits der 8-Zeichen-Minimum-Regel
- Password-Breach-Check (z.B. HaveIBeenPwned)
Next steps
- User beantwortet Q1–Q4 (oder bestätigt Empfehlungen)
- Spec-Doc in `docs/specs/YYYY-MM-DD-password-reset-design.md` schreiben + committen
- Implementation plan via `superpowers:writing-plans` erstellen
- PR in kleinen Schritten (per User-Preference: "NEVER do all steps at once")
Summary
Platform hat aktuell keinen Weg für User, ihr Passwort zurückzusetzen (wenn vergessen) oder zu ändern (wenn eingeloggt). Dieses Issue umfasst beide Flows in einem PR, da sie sich den Dex-
UpdatePassword-gRPC-Wrapper und die Validierungslogik teilen.Scope:
Current state
actions/register.tserstellt User in Dex viacreateDexUser()+ TinyBase + Email-Verification (file-token pattern)lib/email-verification.ts— file-based token store (data/verification-tokens/<token>.json, 24h TTL, single-use,crypto.randomBytes(32))lib/dex/client.ts— wrapptCreatePasswordundVerifyPassword, aber noch nichtUpdatePassword(existiert inapi.protoZeile 278)app/(auth)/hat nurregister/undsignin/— keinforgot-password/oderreset-password/Open design questions
Q1 — Token TTL für Reset-Link
Q2 — Email-Enumeration-Schutz
Q3 — Sessions nach Reset invalidieren
offline-sessionsfür den User löschen.Q4 — Rate-Limiting
Proposed design (pending Q1–Q4)
New files
app/(auth)/forgot-password/page.tsx— Email-Input, triggert Reset-Requestapp/(auth)/reset-password/page.tsx— Passwort-Formular mit Token-Paramapp/settings/password/page.tsx— Change-Password (eingeloggt, alt+neu)actions/password-reset.ts—requestPasswordReset,confirmPasswordResetactions/password-change.ts—changePassword(nutztverifyDexPasswordfür alt)lib/password-reset.ts— Token-Management analogemail-verification.ts, separater Ordnerdata/password-reset-tokens/Modified files
lib/dex/client.ts— neue FunktionupdateDexPassword(email, newPassword)components/form/SignInForm.tsx(oder equivalent) — Link "Passwort vergessen?"/settings/passwordEmail-Template
sendPasswordResetEmail()inlib/password-reset.ts— spiegeltsendVerificationEmail()\${BASE_URL}/reset-password?token=\${token}Flow — Forgot Password
/forgot-password, trägt Email einrequestPasswordReset(email):/reset-password?token=...confirmPasswordReset(token, newPassword):updateDexPassword(email, newPassword)(Q1=A: 1h TTL)/signinmit Success-FlashFlow — Change Password
/settings/passwordchangePassword(current, new):verifyDexPassword(session.email, current)— wenn falsch → FehlerupdateDexPassword(session.email, new)Security decisions baked in
bcrypt-ts(konsistent mitcreateDexUser)crypto.randomBytes(32).toString('hex')(konsistent mitemail-verification)data/password-reset-tokens/mit mode0o600Testing
lib/password-reset.ts(Token-Lifecycle: create, verify, expire, single-use)actions/password-reset.tsundactions/password-change.ts(Mock Dex-Client)Acceptance criteria
/forgot-passwordeinen Reset-Link anfordernEMAIL_SERVERkonfiguriert)/settings/passwordsein Passwort ändern (braucht aktuelles Passwort)Out of scope
Next steps