diff --git a/app/Livewire/Auth/TwoFactorSetup.php b/app/Livewire/Auth/TwoFactorSetup.php index 8452e58..d030f5f 100644 --- a/app/Livewire/Auth/TwoFactorSetup.php +++ b/app/Livewire/Auth/TwoFactorSetup.php @@ -9,7 +9,7 @@ use Livewire\Attributes\Validate; use Livewire\Component; use PragmaRX\Google2FAQRCode\Google2FA; -#[Layout('layouts.auth')] +#[Layout('layouts.app')] class TwoFactorSetup extends Component { public string $secret = ''; @@ -19,8 +19,8 @@ class TwoFactorSetup extends Component public function mount() { - if (Auth::user()->hasTwoFactorEnabled()) { - return $this->redirect(route('dashboard'), navigate: true); + if (Auth::user()->hasTotp()) { + return $this->redirect(route('settings'), navigate: true); } $this->secret = (new Google2FA)->generateSecretKey(); @@ -39,10 +39,14 @@ class TwoFactorSetup extends Component 'two_factor_confirmed_at' => now(), ])->save(); - // Generate backup codes and show them once before entering the panel. - Auth::user()->replaceRecoveryCodes(); + // 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); + } - return $this->redirect(route('two-factor.recovery'), navigate: true); + return $this->redirect(route('settings'), navigate: true); } public function qrCode(): string diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index ae4267a..35110cc 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -8,13 +8,17 @@ use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Layout; use Livewire\Attributes\On; +use Livewire\Attributes\Url; use Livewire\Component; #[Layout('layouts.app')] class Index extends Component { + #[Url] public string $tab = 'profile'; + public bool $openRecoveryModal = false; + public string $name = ''; public string $email = ''; @@ -29,6 +33,11 @@ class Index extends Component { $this->name = Auth::user()->name; $this->email = Auth::user()->email; + + if (session('open_recovery_modal')) { + $this->tab = 'security'; + $this->openRecoveryModal = true; + } } public function updateProfile(): void @@ -104,6 +113,7 @@ class Index extends Component { return view('livewire.settings.index', [ 'twoFactorEnabled' => Auth::user()->hasTwoFactorEnabled(), + 'hasTotp' => Auth::user()->hasTotp(), ])->title(__('settings.title')); } } diff --git a/app/Livewire/Settings/WebauthnKeys.php b/app/Livewire/Settings/WebauthnKeys.php index acdc74f..5edf1cf 100644 --- a/app/Livewire/Settings/WebauthnKeys.php +++ b/app/Livewire/Settings/WebauthnKeys.php @@ -27,14 +27,21 @@ class WebauthnKeys extends Component public function register(array $attestation, WebauthnService $webauthn): void { - // Re-check 2FA enrollment (it may have been disabled in another tab since the - // ceremony started) so a key is never created against a reset second factor. - abort_unless($webauthn->available() && Auth::user()->hasTwoFactorEnabled(), 404); + // WebAuthn must be available; no existing-factor guard here — registering a key + // may itself be the FIRST factor enrollment (the guard in options() already + // vetted the ceremony start; only require availability at submission time). + abort_unless($webauthn->available(), 404); $this->validate(); $cred = $webauthn->verifyRegistration(Auth::user(), $attestation, trim($this->newName)); $this->reset('newName'); + // First factor enrolled and no codes yet → generate + show the recovery modal. + if (! Auth::user()->hasRecoveryCodes()) { + Auth::user()->replaceRecoveryCodes(); + $this->dispatch('openModal', component: 'modals.recovery-codes'); + } + AuditEvent::create([ 'user_id' => Auth::id(), 'actor' => Auth::user()->name ?? 'system', diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index a277b13..cd081af 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -11,6 +11,9 @@ @endphp