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

5.2 KiB
Raw Blame History

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.

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.phppasswords.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).