diff --git a/src/FilamentImpersonateServiceProvider.php b/src/FilamentImpersonateServiceProvider.php index 4431663..abd0f53 100644 --- a/src/FilamentImpersonateServiceProvider.php +++ b/src/FilamentImpersonateServiceProvider.php @@ -37,8 +37,8 @@ public function registeringPackage(): void Event::listen(EnterImpersonation::class, fn () => $this->clearAuthHashes()); Event::listen(LeaveImpersonation::class, fn () => $this->clearAuthHashes()); - Event::listen(Login::class, fn () => Impersonation::clear()); - Event::listen(Logout::class, fn () => Impersonation::clear()); + Event::listen(Login::class, fn (Login $event) => $this->clearImpersonationForGuard($event->guard)); + Event::listen(Logout::class, fn (Logout $event) => $this->clearImpersonationForGuard($event->guard)); $this->registerIcon(); } @@ -53,6 +53,35 @@ public function bootingPackage(): void $this->loadViewsFrom(__DIR__.'/../resources/views', 'impersonate'); } + /** + * Clear impersonation in response to an auth event, but only when the event + * belongs to a guard involved in the active impersonation. + * + * Apps that share a single session across multiple guards (e.g. an admin + * guard alongside a separate customer/storefront guard) would otherwise have + * an active impersonation silently torn down when an unrelated guard fires + * Login/Logout — a customer logging in on the storefront, say. The + * impersonator's own session key is untouched, so they remain authenticated + * as the impersonated user but isImpersonating() returns false, leaving them + * with no banner and no way to leave. Only the impersonator's guard chain may + * end the impersonation. + */ + protected function clearImpersonationForGuard(?string $guard): void + { + if (Impersonation::isImpersonating()) { + $impersonationGuards = array_filter([ + Impersonation::getImpersonatorGuardName(), + Impersonation::getImpersonatorGuardUsingName(), + ]); + + if ($guard !== null && $impersonationGuards !== [] && ! in_array($guard, $impersonationGuards, true)) { + return; + } + } + + Impersonation::clear(); + } + protected function clearAuthHashes(): void { $guards = collect([ diff --git a/tests/ImpersonationClearOnAuthEventTest.php b/tests/ImpersonationClearOnAuthEventTest.php new file mode 100644 index 0000000..83670cb --- /dev/null +++ b/tests/ImpersonationClearOnAuthEventTest.php @@ -0,0 +1,70 @@ +admin = User::create([ + 'name' => 'Admin', + 'email' => 'admin@example.com', + 'password' => bcrypt('password'), + ]); + + $this->targetUser = User::create([ + 'name' => 'Target User', + 'email' => 'target@example.com', + 'password' => bcrypt('password'), + ]); + + $this->actingAs($this->admin); +}); + +afterEach(function () { + if (Impersonation::isImpersonating()) { + Impersonation::leave(); + } +}); + +it('keeps the impersonation when a login fires on an unrelated guard', function () { + Impersonation::enter($this->admin, $this->targetUser, 'web'); + + // A different guard sharing the same session (e.g. a customer/storefront + // guard) authenticates. This must not tear down the admin impersonation. + event(new Login('customer', $this->targetUser, false)); + + expect(Impersonation::isImpersonating())->toBeTrue(); +}); + +it('keeps the impersonation when a logout fires on an unrelated guard', function () { + Impersonation::enter($this->admin, $this->targetUser, 'web'); + + event(new Logout('customer', $this->targetUser)); + + expect(Impersonation::isImpersonating())->toBeTrue(); +}); + +it('clears the impersonation when a fresh login fires on the impersonator guard', function () { + Impersonation::enter($this->admin, $this->targetUser, 'web'); + + event(new Login('web', $this->admin, false)); + + expect(Impersonation::isImpersonating())->toBeFalse(); +}); + +it('clears the impersonation when a logout fires on the impersonator guard', function () { + Impersonation::enter($this->admin, $this->targetUser, 'web'); + + event(new Logout('web', $this->targetUser)); + + expect(Impersonation::isImpersonating())->toBeFalse(); +}); + +it('still clears on login when not impersonating', function () { + expect(Impersonation::isImpersonating())->toBeFalse(); + + event(new Login('customer', $this->targetUser, false)); + + expect(Impersonation::isImpersonating())->toBeFalse(); +});