From f0b395ce231bc7839d8a0b1e6b56dc5831634e4b Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 6 Aug 2025 14:06:08 +0200 Subject: [PATCH] Add and test command for deleteing expired impersonation tokens --- .../ClearExpiredImpersonationTokens.php | 36 +++++++++++++ src/TenancyServiceProvider.php | 1 + tests/TenantUserImpersonationTest.php | 54 +++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 src/Commands/ClearExpiredImpersonationTokens.php diff --git a/src/Commands/ClearExpiredImpersonationTokens.php b/src/Commands/ClearExpiredImpersonationTokens.php new file mode 100644 index 00000000..3b28bafc --- /dev/null +++ b/src/Commands/ClearExpiredImpersonationTokens.php @@ -0,0 +1,36 @@ +components->info('Removing expired impersonation tokens.'); + + $ttl = (int) $this->option('ttl') ?: UserImpersonation::$ttl; + $expirationDate = now()->subSeconds($ttl); + + $impersonationTokenModel = UserImpersonation::modelClass(); + + $deletedTokenCount = $impersonationTokenModel::where('created_at', '<', $expirationDate) + ->delete(); + + $this->components->info($deletedTokenCount . ' expired impersonation ' . str('token')->plural($deletedTokenCount) . ' deleted.'); + + return 0; + } +} diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 22a81624..bfa8f14e 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -104,6 +104,7 @@ class TenancyServiceProvider extends ServiceProvider Commands\ClearPendingTenants::class, Commands\CreatePendingTenants::class, Commands\CreateUserWithRLSPolicies::class, + Commands\ClearExpiredImpersonationTokens::class, ]); if (static::$migrateFreshOverride) { diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index 0f43522c..8984338a 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -350,6 +350,60 @@ test('tokens are cleaned up when in wrong tenant context before aborting', funct expect(ImpersonationToken::find($token->token))->toBeNull(); }); +test('expired impersonation tokens can be cleaned up using a command', function () { + $tenant = Tenant::create(); + migrateTenants(); + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'foo', + 'email' => 'foo@bar', + 'password' => bcrypt('password'), + ]); + }); + + // Create tokens + $oldToken = tenancy()->impersonate($tenant, $user->id, '/dashboard'); + $activeToken = tenancy()->impersonate($tenant, $user->id, '/dashboard'); + + // Make one of the tokens expired by updating its created_at + $oldToken->update([ + 'created_at' => Carbon::now()->subSeconds(UserImpersonation::$ttl + 10), + ]); + + // Both tokens exist + expect(ImpersonationToken::find($activeToken->token))->not()->toBeNull(); + expect(ImpersonationToken::find($oldToken->token))->not()->toBeNull(); + + pest()->artisan('tenants:clear-expired-impersonation-tokens') + ->assertExitCode(0); + + // The expired token was deleted + expect(ImpersonationToken::find($oldToken->token))->toBeNull(); + // The active token still exists + expect(ImpersonationToken::find($activeToken->token))->not()->toBeNull(); + + // Update the active token to make it expired according to the default ttl (60s) + $activeToken->update([ + 'created_at' => Carbon::now()->subSeconds(70), + ]); + + // The --ttl option can be used to specify a custom TTL instead of updating UserImpersonation::$ttl. + // The passed ttl will be used in place of the default ttl, + // and with ttl set to 80s, the active token should not be deleted + pest()->artisan('tenants:clear-expired-impersonation-tokens', [ + '--ttl' => 80, + ])->assertExitCode(0); + + expect(ImpersonationToken::find($activeToken->token))->not()->toBeNull(); + + // With ttl set to 40s, the active token should be deleted + pest()->artisan('tenants:clear-expired-impersonation-tokens', [ + '--ttl' => 40, + ])->assertExitCode(0); + + expect(ImpersonationToken::find($activeToken->token))->toBeNull(); +}); + function migrateTenants() { pest()->artisan('tenants:migrate')->assertExitCode(0);