docs(spec): SMTP-aware forgot-password (15-min link + 2FA fallback)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
6290cec6ab
commit
cb3360b235
|
|
@ -0,0 +1,81 @@
|
|||
# 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).
|
||||
Loading…
Reference in New Issue