fix(auth): R15 review — restore one-time-code autofill, memoise pendingUser in verify

- backup field: autocomplete=off -> one-time-code (backup codes ARE one-time codes;
  matches the main challenge field, the rest of the app, and the spec; restores
  password-manager OTP autofill that the earlier polish broke)
- both verify() use the trait's memoised $this->pendingUser() instead of a fresh
  User::find(), saving a redundant query on the failed-attempt re-render (Pint dropped
  the now-unused User import in TwoFactorBackup)
- spec: align trait backing-state visibility note to the implemented 'private'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-20 16:19:40 +02:00
parent 7c1a32518d
commit 4ea4050c80
4 changed files with 5 additions and 5 deletions

View File

@ -3,7 +3,6 @@
namespace App\Livewire\Auth;
use App\Livewire\Concerns\CompletesTwoFactorChallenge;
use App\Models\User;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
@ -30,7 +29,7 @@ class TwoFactorBackup extends Component
$this->assertNotRateLimited();
$user = User::find(session('2fa.user'));
$user = $this->pendingUser();
if (! $user || ! $user->hasTwoFactorEnabled()) {
session()->forget(['2fa.user', '2fa.remember']);

View File

@ -37,7 +37,7 @@ class TwoFactorChallenge extends Component
$this->assertNotRateLimited();
$user = User::find(session('2fa.user'));
$user = $this->pendingUser();
if (! $user || ! $user->hasTwoFactorEnabled()) {
session()->forget(['2fa.user', '2fa.remember']);

View File

@ -40,7 +40,8 @@ Extract the logic currently inline in `TwoFactorChallenge` into a trait used by
components, so there is no duplication and **one shared rate-limit bucket set**:
- `pendingUser(): ?User` — the memoised `User::find(session('2fa.user'))` resolver (move the existing
one; keep its backing state `protected` so both components share it).
one; keep its backing state `private` — an internal memoisation detail; each component gets its
own copy, and Livewire persists only public props, so the visibility is runtime-irrelevant).
- `getPendingHasTotpProperty(): bool` — **keep the existing Livewire computed-property magic getter**
(move it verbatim into the trait, do **not** turn it into a plain `pendingHasTotp()` method). This
keeps every blade's `$this->pendingHasTotp` usage working unchanged — **no property→method rewrites

View File

@ -14,7 +14,7 @@
<form wire:submit="verify" class="space-y-4">
<div>
<label for="code" class="{{ $label }}">{{ __('auth.challenge_backup_label') }}</label>
<input wire:model="code" id="code" inputmode="text" autocomplete="off" autofocus
<input wire:model="code" id="code" inputmode="text" autocomplete="one-time-code" autofocus
class="{{ $fieldText }}" placeholder="{{ __('auth.challenge_backup_placeholder') }}" />
@error('code') <p class="{{ $err }}"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
</div>