diff --git a/app/Livewire/Auth/ForgotPassword.php b/app/Livewire/Auth/ForgotPassword.php
new file mode 100644
index 0000000..20f9fca
--- /dev/null
+++ b/app/Livewire/Auth/ForgotPassword.php
@@ -0,0 +1,81 @@
+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'));
+ }
+}
diff --git a/resources/views/livewire/auth/forgot-password.blade.php b/resources/views/livewire/auth/forgot-password.blade.php
new file mode 100644
index 0000000..789c32e
--- /dev/null
+++ b/resources/views/livewire/auth/forgot-password.blade.php
@@ -0,0 +1,45 @@
+@php
+ $label = 'mb-1.5 block font-mono text-[11px] uppercase tracking-wider text-ink-3';
+ $field = 'h-11 w-full rounded-md border border-line bg-inset px-3 text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none';
+ $err = 'mt-1.5 flex items-center gap-1.5 font-mono text-[11px] text-offline';
+@endphp
+
+
+
{{ __('auth.security') }}
+
{{ __('auth.forgot_heading') }}
+
{{ __('auth.forgot_subtitle') }}
+
+
+
+
+ @if ($this->mailEnabled())
+
{{ __('auth.forgot_send_email') }}
+ @endif
+
+
{{ __('auth.back_to_login') }}
+
diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php
index ac8a847..4f89cc1 100644
--- a/resources/views/livewire/auth/login.blade.php
+++ b/resources/views/livewire/auth/login.blade.php
@@ -24,6 +24,7 @@
@error('password') {{ $message }}
@enderror
+ {{ __('auth.forgot_link') }}