clusev/app/Livewire/Auth/ForgotPassword.php

103 lines
3.3 KiB
PHP

<?php
namespace App\Livewire\Auth;
use App\Models\AuditEvent;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.auth')]
class ForgotPassword extends Component
{
public string $email = '';
public string $code = '';
public string $password = '';
public string $password_confirmation = '';
/** Reveals the secondary inline 2FA-proof form when a mailer is configured. */
public bool $showCodeReset = false;
/** True when a real mailer is configured (then the email-link option is offered too). */
public function mailEnabled(): bool
{
return ! in_array(config('mail.default'), ['log', 'array', null], true);
}
/** Secondary path: email a reset link (only meaningful when a real mailer is set). */
public function sendResetLink(): void
{
if (! $this->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();
$ok = $user
&& $user->hasTwoFactorEnabled()
&& ($user->verifyTotp($this->code) || $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);
// Rotate remember_token too, so a stolen remember-me cookie can't survive the reset.
$user->forceFill([
'password' => Hash::make($this->password),
'must_change_password' => false,
'remember_token' => Str::random(60),
])->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'));
}
}