clusev/docs/superpowers/specs/2026-06-15-forgot-password-...

82 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# SMTP-aware forgot-password (15-min e-mail link + 2FA fallback) — Design
**Date:** 2026-06-15 · **Branch:** `feat/v1-foundation` · **Status:** approved
Rework "Passwort vergessen" so the **e-mail reset link** is the primary path when the operator has
configured SMTP, falling back to the existing **2FA-possession reset** when no mailer is available
(a self-hosted box may have no SMTP). The public forgot screen no longer advertises the
`clusev:reset-admin` CLI (anyone could read it pre-login) — that last-resort recovery is documented in
**Settings → Security** (admin-only) and the **README**.
This is sub-project #1 of a larger batch; #2#5 (de-Claude the repo + history, professional README,
installer overhaul, themed MOTD) are separate beta-prep specs, tackled after this.
## 1. Mail availability
`ForgotPassword::mailEnabled()` already returns whether a real mailer is set (`config('mail.default')`
not `log`/`array`/null). With the 0.9.0 in-panel SMTP override applied at boot, `mail.default` becomes
`smtp` once configured — so `mailEnabled()` is the single source of truth. (No new method needed.)
## 2. Flow — `Auth\ForgotPassword` (full-page, auth-layout)
- **SMTP set (`mailEnabled()` true):**
- Default view: **email only** + "Reset-Link senden" → `Password::sendResetLink(['email' => …])` (the
Laravel broker; sends the `ResetPassword` notification through the configured mailer). Always show
the generic "Falls die E-Mail existiert, wurde ein Reset-Link gesendet." (no account enumeration).
- A small secondary toggle **"Kein E-Mail-Zugang? Mit 2FA-Code zurücksetzen"** reveals the inline
2FA-proof form (so a user with their authenticator but no inbox access still recovers).
- **No SMTP (`mailEnabled()` false):**
- Show ONLY the 2FA-proof inline reset (email + TOTP/backup-code + new password) — the current
`resetPassword()` logic (rate-limited, remember-token rotation, audited, generic errors). No
e-mail-link option (nothing can be delivered).
- The two paths reuse the existing `sendResetLink()` and `resetPassword()` methods; only the **view**
becomes mail-aware (Blade `@if ($this->mailEnabled())`), plus an Alpine toggle for the secondary form.
## 3. Reset link target — `Auth\ResetPassword`
Unchanged: the token link lands here to set the new password. **Token expiry → 15 minutes:** set
`config/auth.php` `passwords.users.expire = 15` (was Laravel's default 60). Confirm the
`password_reset_tokens` table migration exists (it ships with the users-table migration).
## 4. Remove the CLI hint from the public screen
- Delete the `reset_no_2fa_note` line (it names `clusev:reset-admin`) from
`resources/views/livewire/auth/forgot-password.blade.php`. Keep the lang key or repurpose it.
- **Settings → Security** (`Settings\Security` view, admin-only): add a discreet recovery note —
"Konto-Wiederherstellung (letzter Ausweg): per SSH auf den Host und `clusev:reset-admin` ausführen."
(admin-only context, so showing the command is fine here.) New lang key.
- **README:** add a short "Wiederherstellung / Account recovery" section documenting `clusev:reset-admin`
(the full professional README is sub-project #3; for #1 add the minimal recovery section so the doc
trail exists).
## 5. Copy
The forgot subtitle should reflect the SMTP-on default. With SMTP: "Wir senden dir einen Reset-Link
(15 Minuten gültig)." With the 2FA fallback toggle present. Without SMTP: the current "mit 2FA-Code
oder Backup-Code" subtitle. Drive the subtitle off `mailEnabled()`. German + English (R16).
## Files
- `app/Livewire/Auth/ForgotPassword.php` — subtitle helper / no logic change to the two existing
actions (sendResetLink / resetPassword); maybe a `public bool $showCodeReset` toggle for the
secondary form when SMTP is on.
- `resources/views/livewire/auth/forgot-password.blade.php` — mail-aware layout + Alpine/Livewire
toggle; remove the CLI hint.
- `config/auth.php``passwords.users.expire = 15`.
- `app/Livewire/Settings/Security.php` + its view — the admin-only recovery note.
- `lang/{de,en}/auth.php` — subtitle variants, the "use 2FA code" toggle label, drop/repurpose
`reset_no_2fa_note`.
- `lang/{de,en}/settings.php` (or `auth`) — the Settings recovery note.
- `README.md` — a minimal recovery section.
- Tests.
## Testing
- SMTP on (`config(['mail.default' => 'smtp'])`): the forgot view shows the email-only form (assertSee
the send-link button, assertDontSee the new-password fields by default); `sendResetLink` dispatches a
`ResetPassword` notification (use `Notification::fake()` + `assertSentTo`); the secondary toggle
reveals the 2FA form.
- SMTP off: the view shows the 2FA-proof inline form; `resetPassword` with a valid TOTP/backup code
resets + rotates remember-token (existing tests still pass).
- Token expiry is 15 minutes (assert `config('auth.passwords.users.expire') === 15`).
- The public forgot view contains NO `clusev:reset-admin` string; the Settings → Security view DOES
(admin-only).
- Pint, full suite green, Codex clean.
## Out of scope (separate beta-prep specs)
- #2 de-Claude repo + git history, #3 professional README, #4 installer overhaul, #5 themed MOTD.
- Changing the WebAuthn/TOTP mechanics or the SMTP config itself (0.9.0).