mailEnabled()) { return; } $this->validate(['email' => ['required', 'email']]); \Illuminate\Support\Facades\Password::sendResetLink(['email' => $this->email]); // Generic — never reveal whether the email exists. $this->dispatch('notify', message: __('auth.reset_link_sent')); } /** Reset the password by proving 2FA possession (TOTP or a one-time backup code). */ public function resetPassword() { $this->validate([ 'email' => ['required', 'email'], 'code' => ['required', 'string'], 'password' => ['required', 'confirmed', Password::min(12)->mixedCase()->numbers()], ]); $key = 'forgot:'.md5(strtolower($this->email).'|'.request()->ip()); if (RateLimiter::tooManyAttempts($key, 5)) { throw ValidationException::withMessages([ 'code' => __('auth.too_many_attempts', ['seconds' => RateLimiter::availableIn($key)]), ]); } $user = User::where('email', $this->email)->first(); $clean = preg_replace('/\s+/', '', $this->code); $ok = $user && $user->hasTwoFactorEnabled() && ((new Google2FA)->verifyKey($user->two_factor_secret, $clean) || $user->useRecoveryCode($this->code)); if (! $ok) { RateLimiter::hit($key, 60); // Generic message — never reveal whether the email exists. throw ValidationException::withMessages(['code' => __('auth.reset_invalid')]); } RateLimiter::clear($key); $user->forceFill(['password' => Hash::make($this->password), 'must_change_password' => false])->save(); AuditEvent::create([ 'user_id' => $user->id, 'actor' => $user->name ?? 'system', 'action' => 'password.reset', 'target' => $user->email, 'ip' => request()->ip(), ]); session()->flash('status', __('auth.reset_done')); return $this->redirect(route('login'), navigate: true); } public function render() { return view('livewire.auth.forgot-password')->title(__('auth.title_forgot')); } }