diff --git a/app/Livewire/Portal/Auth/ForgotPassword.php b/app/Livewire/Portal/Auth/ForgotPassword.php new file mode 100644 index 00000000..3c5ff86d --- /dev/null +++ b/app/Livewire/Portal/Auth/ForgotPassword.php @@ -0,0 +1,57 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + ResetPassword::createUrlUsing(fn (User $user, string $token): string => route('portal.password.reset', [ + 'token' => $token, + ])); + + $status = Password::sendResetLink(['email' => $this->email]); + + if ($status === Password::RESET_LINK_SENT) { + $this->linkSent = true; + Flux::toastSuccess(__($status)); + + return; + } + + throw ValidationException::withMessages([ + 'email' => __($status), + ]); + } + + public function render(): View + { + return view('portal::livewire.auth.forgot-password') + ->title('Forgot Password'); + } +} diff --git a/app/Livewire/Portal/Auth/ResetPassword.php b/app/Livewire/Portal/Auth/ResetPassword.php new file mode 100644 index 00000000..2f0e4603 --- /dev/null +++ b/app/Livewire/Portal/Auth/ResetPassword.php @@ -0,0 +1,92 @@ +token = $token; + $this->email = $email ?? ''; + } + + /** + * Reset the user's password. + * + * @throws ValidationException + */ + public function resetPassword(): void + { + $this->validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', PasswordRule::defaults()], + ]); + + $status = Password::reset( + [ + 'email' => $this->email, + 'password' => $this->password, + 'password_confirmation' => $this->password_confirmation, + 'token' => $this->token, + ], + function (User $user, string $password): void { + $user->forceFill([ + 'password' => Hash::make($password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + if ($status === Password::PASSWORD_RESET) { + Auth::attempt(['email' => $this->email, 'password' => $this->password], true); + Session::regenerate(); + + Flux::toastSuccess(__($status)); + + $this->redirect(route('portal.dashboard'), navigate: true); + + return; + } + + throw ValidationException::withMessages([ + 'email' => __($status), + ]); + } + + public function render(): View + { + return view('portal::livewire.auth.reset-password') + ->title('Reset Password'); + } +} diff --git a/app/Notifications/ResetPasswordNotification.php b/app/Notifications/ResetPasswordNotification.php index 6780618b..5d5d8c85 100644 --- a/app/Notifications/ResetPasswordNotification.php +++ b/app/Notifications/ResetPasswordNotification.php @@ -32,9 +32,14 @@ public function toMail(mixed $notifiable): MailMessage #[Override] protected function resetUrl(mixed $notifiable): string { + // Check if a custom URL callback was set (e.g., by Portal) + if (static::$createUrlCallback) { + return call_user_func(static::$createUrlCallback, $notifiable, $this->token); + } + + // Default: use localized route for web return localized_route('localized.password.reset', [ 'token' => $this->token, - 'email' => $notifiable->getEmailForPasswordReset(), ]); } } diff --git a/resources/views/portal/livewire/auth/forgot-password.blade.php b/resources/views/portal/livewire/auth/forgot-password.blade.php new file mode 100644 index 00000000..588aedd5 --- /dev/null +++ b/resources/views/portal/livewire/auth/forgot-password.blade.php @@ -0,0 +1,37 @@ +