From 07122f504d511d36b7962a38bf04abd347d870bd Mon Sep 17 00:00:00 2001 From: Robin Malburn Date: Fri, 9 May 2025 11:07:31 +0100 Subject: [PATCH 1/3] Updates the panel password reset request and password reset flow so that they fully support the canAccessPanel gating checks. --- .../password-reset/request-password-reset.php | 2 + .../PasswordReset/RequestPasswordReset.php | 7 +++- .../Auth/PasswordReset/ResetPassword.php | 17 +++++++- .../RequestPasswordResetTest.php | 34 ++++++++++++++- .../Auth/PasswordReset/ResetPasswordTest.php | 42 ++++++++++++++++++- 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php b/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php index 97c276f265..4d84cb3d97 100644 --- a/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php +++ b/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php @@ -37,6 +37,8 @@ return [ 'body' => 'Please try again in :seconds seconds.', ], + 'sent' => 'If that email address is valid in our records, we will send you an email to reset your password.', + ], ]; diff --git a/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php b/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php index c9f2059eff..490c493275 100644 --- a/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php +++ b/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php @@ -11,6 +11,7 @@ use Filament\Facades\Filament; use Filament\Forms\Components\Component; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; +use Filament\Models\Contracts\FilamentUser; use Filament\Notifications\Auth\ResetPassword as ResetPasswordNotification; use Filament\Notifications\Notification; use Filament\Pages\Concerns\InteractsWithFormActions; @@ -68,6 +69,10 @@ class RequestPasswordReset extends SimplePage throw new Exception("Model [{$userClass}] does not have a [notify()] method."); } + if (!($user instanceof FilamentUser) || !$user->canAccessPanel(Filament::getCurrentPanel())) { + return; + } + $notification = app(ResetPasswordNotification::class, ['token' => $token]); $notification->url = Filament::getResetPasswordUrl($token, $user); @@ -81,7 +86,7 @@ class RequestPasswordReset extends SimplePage return; } - $this->getSentNotification($status)?->send(); + $this->getSentNotification('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent')?->send(); $this->form->fill(); } diff --git a/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php b/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php index 65d7dbfe56..46485b5b52 100644 --- a/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php +++ b/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php @@ -11,6 +11,7 @@ use Filament\Forms\Components\Component; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Http\Responses\Auth\Contracts\PasswordResetResponse; +use Filament\Models\Contracts\FilamentUser; use Filament\Notifications\Notification; use Filament\Pages\Concerns\InteractsWithFormActions; use Filament\Pages\SimplePage; @@ -76,18 +77,30 @@ class ResetPassword extends SimplePage $data['email'] = $this->email; $data['token'] = $this->token; + $hasPanelAccess = true; + $status = Password::broker(Filament::getAuthPasswordBroker())->reset( $this->getCredentialsFromFormData($data), - function (CanResetPassword | Model | Authenticatable $user) use ($data) { + function (CanResetPassword | Model | Authenticatable $user) use ($data, &$hasPanelAccess) { + + if (!($user instanceof FilamentUser) || !$user->canAccessPanel(Filament::getCurrentPanel())) { + $hasPanelAccess = false; + return; + } + $user->forceFill([ 'password' => Hash::make($data['password']), 'remember_token' => Str::random(60), ])->save(); event(new PasswordReset($user)); - }, + } ); + if ($hasPanelAccess === false) { + $status = Password::INVALID_USER; + } + if ($status === Password::PASSWORD_RESET) { Notification::make() ->title(__($status)) diff --git a/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php b/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php index 21447fe176..f3c70dcc99 100644 --- a/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php +++ b/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php @@ -3,9 +3,11 @@ use Filament\Facades\Filament; use Filament\Notifications\Auth\ResetPassword; use Filament\Pages\Auth\PasswordReset\RequestPasswordReset; +use Filament\Panel; use Filament\Tests\Models\User; use Filament\Tests\TestCase; use Illuminate\Support\Facades\Notification; +use Filament\Notifications\Notification as FilamentNotification; use function Filament\Tests\livewire; @@ -39,11 +41,41 @@ it('can request password reset', function () { 'email' => $userToResetPassword->email, ]) ->call('request') - ->assertNotified(); + ->assertNotified( + FilamentNotification::make() + ->success() + ->title(__('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent')) + ); Notification::assertSentTo($userToResetPassword, ResetPassword::class); }); +it('can gate password resets based on panel access', function () { + Notification::fake(); + + $this->assertGuest(); + + $userToResetPassword = User::factory()->create(); + + $testPanel = Panel::make(); + $testPanel->id('test'); + + Filament::setCurrentPanel($testPanel); + + livewire(RequestPasswordReset::class) + ->fillForm([ + 'email' => $userToResetPassword->email, + ]) + ->call('request') + ->assertNotified( + FilamentNotification::make() + ->success() + ->title(__('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent')) + ); + + Notification::assertNotSentTo($userToResetPassword, ResetPassword::class); +}); + it('can throttle requests', function () { Notification::fake(); diff --git a/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php b/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php index 4ad3ee225b..83429f3a85 100644 --- a/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php +++ b/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php @@ -8,6 +8,8 @@ use Illuminate\Auth\Events\PasswordReset; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Password; use Illuminate\Support\Str; +use Filament\Panel; +use Filament\Notifications\Notification; use function Filament\Tests\livewire; @@ -58,7 +60,11 @@ it('can reset password', function () { ->set('password', 'new-password') ->set('passwordConfirmation', 'new-password') ->call('resetPassword') - ->assertNotified() + ->assertNotified( + Notification::make() + ->success() + ->title(__('passwords.reset')) + ) ->assertRedirect(Filament::getLoginUrl()); Event::assertDispatched(PasswordReset::class); @@ -69,6 +75,40 @@ it('can reset password', function () { ]); }); +it('can gate reset password based on panel access', function () { + Event::fake(); + + $this->assertGuest(); + + $userToResetPassword = User::factory()->create(); + $token = Password::createToken($userToResetPassword); + + $testPanel = Panel::make(); + $testPanel->id('test'); + + Filament::setCurrentPanel($testPanel); + + livewire(ResetPassword::class, [ + 'email' => $userToResetPassword->email, + 'token' => $token, + ]) + ->set('password', 'new-password') + ->set('passwordConfirmation', 'new-password') + ->call('resetPassword') + ->assertNotified( + Notification::make() + ->danger() + ->title(__('passwords.user')) + ); + + Event::assertNotDispatched(PasswordReset::class); + + $this->assertCredentials([ + 'email' => $userToResetPassword->email, + 'password' => 'password', + ]); +}); + it('requires request signature', function () { $userToResetPassword = User::factory()->make(); $token = Password::createToken($userToResetPassword); From 88330ec8b40190ad71d2636000b130d0e8bea20c Mon Sep 17 00:00:00 2001 From: Robin Malburn Date: Fri, 9 May 2025 10:16:37 +0000 Subject: [PATCH 2/3] Fixes phpcs styling. --- .../src/Pages/Auth/PasswordReset/RequestPasswordReset.php | 2 +- .../panels/src/Pages/Auth/PasswordReset/ResetPassword.php | 3 ++- .../Panels/Auth/PasswordReset/RequestPasswordResetTest.php | 2 +- tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php b/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php index 490c493275..a608fec7cc 100644 --- a/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php +++ b/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php @@ -69,7 +69,7 @@ class RequestPasswordReset extends SimplePage throw new Exception("Model [{$userClass}] does not have a [notify()] method."); } - if (!($user instanceof FilamentUser) || !$user->canAccessPanel(Filament::getCurrentPanel())) { + if (! ($user instanceof FilamentUser) || ! $user->canAccessPanel(Filament::getCurrentPanel())) { return; } diff --git a/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php b/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php index 46485b5b52..4119af7cb2 100644 --- a/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php +++ b/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php @@ -83,8 +83,9 @@ class ResetPassword extends SimplePage $this->getCredentialsFromFormData($data), function (CanResetPassword | Model | Authenticatable $user) use ($data, &$hasPanelAccess) { - if (!($user instanceof FilamentUser) || !$user->canAccessPanel(Filament::getCurrentPanel())) { + if (! ($user instanceof FilamentUser) || ! $user->canAccessPanel(Filament::getCurrentPanel())) { $hasPanelAccess = false; + return; } diff --git a/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php b/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php index f3c70dcc99..01ebb8dd34 100644 --- a/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php +++ b/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php @@ -2,12 +2,12 @@ use Filament\Facades\Filament; use Filament\Notifications\Auth\ResetPassword; +use Filament\Notifications\Notification as FilamentNotification; use Filament\Pages\Auth\PasswordReset\RequestPasswordReset; use Filament\Panel; use Filament\Tests\Models\User; use Filament\Tests\TestCase; use Illuminate\Support\Facades\Notification; -use Filament\Notifications\Notification as FilamentNotification; use function Filament\Tests\livewire; diff --git a/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php b/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php index 83429f3a85..2824f311e9 100644 --- a/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php +++ b/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php @@ -1,15 +1,15 @@ Date: Sat, 10 May 2025 14:37:29 +0100 Subject: [PATCH 3/3] clean up --- .../auth/password-reset/request-password-reset.php | 6 ++++-- .../Auth/PasswordReset/RequestPasswordReset.php | 14 +++++++++----- .../src/Pages/Auth/PasswordReset/ResetPassword.php | 6 ++++-- .../PasswordReset/RequestPasswordResetTest.php | 14 ++++++-------- .../Auth/PasswordReset/ResetPasswordTest.php | 8 ++------ 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php b/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php index 4d84cb3d97..a26eaa4e23 100644 --- a/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php +++ b/packages/panels/resources/lang/en/pages/auth/password-reset/request-password-reset.php @@ -32,13 +32,15 @@ return [ 'notifications' => [ + 'sent' => [ + 'body' => 'If your account doesn\'t exist, you will not receive the email.', + ], + 'throttled' => [ 'title' => 'Too many requests', 'body' => 'Please try again in :seconds seconds.', ], - 'sent' => 'If that email address is valid in our records, we will send you an email to reset your password.', - ], ]; diff --git a/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php b/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php index a608fec7cc..dbb6b6403e 100644 --- a/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php +++ b/packages/panels/src/Pages/Auth/PasswordReset/RequestPasswordReset.php @@ -63,16 +63,19 @@ class RequestPasswordReset extends SimplePage $status = Password::broker(Filament::getAuthPasswordBroker())->sendResetLink( $this->getCredentialsFromFormData($data), function (CanResetPassword $user, string $token): void { + if ( + ($user instanceof FilamentUser) && + (! $user->canAccessPanel(Filament::getCurrentPanel())) + ) { + return; + } + if (! method_exists($user, 'notify')) { $userClass = $user::class; throw new Exception("Model [{$userClass}] does not have a [notify()] method."); } - if (! ($user instanceof FilamentUser) || ! $user->canAccessPanel(Filament::getCurrentPanel())) { - return; - } - $notification = app(ResetPasswordNotification::class, ['token' => $token]); $notification->url = Filament::getResetPasswordUrl($token, $user); @@ -86,7 +89,7 @@ class RequestPasswordReset extends SimplePage return; } - $this->getSentNotification('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent')?->send(); + $this->getSentNotification($status)?->send(); $this->form->fill(); } @@ -116,6 +119,7 @@ class RequestPasswordReset extends SimplePage { return Notification::make() ->title(__($status)) + ->body(($status === Password::RESET_LINK_SENT) ? __('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent.body') : null) ->success(); } diff --git a/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php b/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php index 4119af7cb2..84beb935fe 100644 --- a/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php +++ b/packages/panels/src/Pages/Auth/PasswordReset/ResetPassword.php @@ -82,8 +82,10 @@ class ResetPassword extends SimplePage $status = Password::broker(Filament::getAuthPasswordBroker())->reset( $this->getCredentialsFromFormData($data), function (CanResetPassword | Model | Authenticatable $user) use ($data, &$hasPanelAccess) { - - if (! ($user instanceof FilamentUser) || ! $user->canAccessPanel(Filament::getCurrentPanel())) { + if ( + ($user instanceof FilamentUser) && + (! $user->canAccessPanel(Filament::getCurrentPanel())) + ) { $hasPanelAccess = false; return; diff --git a/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php b/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php index 01ebb8dd34..a5c9f07015 100644 --- a/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php +++ b/tests/src/Panels/Auth/PasswordReset/RequestPasswordResetTest.php @@ -4,7 +4,6 @@ use Filament\Facades\Filament; use Filament\Notifications\Auth\ResetPassword; use Filament\Notifications\Notification as FilamentNotification; use Filament\Pages\Auth\PasswordReset\RequestPasswordReset; -use Filament\Panel; use Filament\Tests\Models\User; use Filament\Tests\TestCase; use Illuminate\Support\Facades\Notification; @@ -44,23 +43,21 @@ it('can request password reset', function () { ->assertNotified( FilamentNotification::make() ->success() - ->title(__('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent')) + ->title(__('passwords.sent')) + ->body(__('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent.body')) ); Notification::assertSentTo($userToResetPassword, ResetPassword::class); }); -it('can gate password resets based on panel access', function () { +it('cannot request password reset without panel access', function () { Notification::fake(); $this->assertGuest(); $userToResetPassword = User::factory()->create(); - $testPanel = Panel::make(); - $testPanel->id('test'); - - Filament::setCurrentPanel($testPanel); + Filament::setCurrentPanel(Filament::getPanel('custom')); livewire(RequestPasswordReset::class) ->fillForm([ @@ -70,7 +67,8 @@ it('can gate password resets based on panel access', function () { ->assertNotified( FilamentNotification::make() ->success() - ->title(__('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent')) + ->title(__('passwords.sent')) + ->body(__('filament-panels::pages/auth/password-reset/request-password-reset.notifications.sent.body')) ); Notification::assertNotSentTo($userToResetPassword, ResetPassword::class); diff --git a/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php b/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php index 2824f311e9..f4f83b8719 100644 --- a/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php +++ b/tests/src/Panels/Auth/PasswordReset/ResetPasswordTest.php @@ -3,7 +3,6 @@ use Filament\Facades\Filament; use Filament\Notifications\Notification; use Filament\Pages\Auth\PasswordReset\ResetPassword; -use Filament\Panel; use Filament\Tests\Models\User; use Filament\Tests\TestCase; use Illuminate\Auth\Events\PasswordReset; @@ -75,7 +74,7 @@ it('can reset password', function () { ]); }); -it('can gate reset password based on panel access', function () { +it('cannot reset password without panel access', function () { Event::fake(); $this->assertGuest(); @@ -83,10 +82,7 @@ it('can gate reset password based on panel access', function () { $userToResetPassword = User::factory()->create(); $token = Password::createToken($userToResetPassword); - $testPanel = Panel::make(); - $testPanel->id('test'); - - Filament::setCurrentPanel($testPanel); + Filament::setCurrentPanel(Filament::getPanel('custom')); livewire(ResetPassword::class, [ 'email' => $userToResetPassword->email,