hasTotp()) { return $this->redirect(route('settings'), navigate: true); } $this->secret = (new Google2FA)->generateSecretKey(); } public function confirm() { $this->validate(); try { $matches = (new Google2FA)->verifyKey($this->secret, preg_replace('/\s+/', '', $this->code) ?? ''); } catch (Google2FAException $e) { $matches = false; } if (! $matches) { throw ValidationException::withMessages(['code' => __('auth.code_mismatch')]); } Auth::user()->forceFill([ 'two_factor_secret' => $this->secret, 'two_factor_confirmed_at' => now(), ])->save(); // First factor enrolled and no codes yet → generate the single recovery set and // ask Settings to pop the modal (the app layout hosts wire-elements/modal). if (! Auth::user()->hasRecoveryCodes()) { Auth::user()->replaceRecoveryCodes(); session()->flash('open_recovery_modal', true); // put (not flash): must survive the redirect AND the later openModal Livewire // request; the modal pulls it on mount to reveal the freshly generated codes once. session()->put('2fa.codes_fresh', true); // One-time download grant (a timestamp) — the download route requires + consumes // it within a short window, so the fresh set can be downloaded once and a later // direct hit 403s. session()->put('2fa.download_grant', now()->timestamp); } return $this->redirect(route('settings'), navigate: true); } public function qrCode(): string { return (new Google2FA)->getQRCodeInline('Clusev', Auth::user()->email, $this->secret); } public function render() { return view('livewire.auth.two-factor-setup')->title(__('auth.title_setup')); } }