diff --git a/app/Livewire/Admin/Settings.php b/app/Livewire/Admin/Settings.php index 432d3c3..e9d2cfd 100644 --- a/app/Livewire/Admin/Settings.php +++ b/app/Livewire/Admin/Settings.php @@ -155,7 +155,9 @@ class Settings extends Component { $this->authorize('staff.manage'); - $result = DB::transaction(function () use ($id) { + $revokedPeers = []; + + $result = DB::transaction(function () use ($id, &$revokedPeers) { Role::query()->where('name', 'Owner')->lockForUpdate()->first(); $target = User::query()->whereKey($id)->lockForUpdate()->first(); @@ -177,12 +179,20 @@ class Settings extends Component foreach (VpnPeer::query()->where('user_id', $target->id)->get() as $peer) { $peer->purgeSecret(); $peer->delete(); - ApplyVpnPeer::dispatch($peer->public_key, null, false); + $revokedPeers[] = $peer->public_key; } return 'revoked'; }); + // Dispatched only once the rows are committed: a worker picking the job + // up mid-transaction would still see the peer as active, leave it on the + // hub, and never be asked again — a revoked colleague would keep their + // tunnel until the next reconciliation happened to notice. + foreach ($revokedPeers as $publicKey) { + ApplyVpnPeer::dispatch($publicKey, null, false); + } + $this->flash($result); } diff --git a/tests/Feature/Admin/VpnTest.php b/tests/Feature/Admin/VpnTest.php index 4966c16..2374fe3 100644 --- a/tests/Feature/Admin/VpnTest.php +++ b/tests/Feature/Admin/VpnTest.php @@ -640,3 +640,18 @@ it('takes the tunnel away when a staff member is revoked', function () { ->and(VpnPeer::withTrashed()->first()->config_secret)->toBeNull(); Queue::assertPushed(ApplyVpnPeer::class, fn ($job) => $job->publicKey === $peer->public_key && ! $job->enabled); }); + +it('removes a revoked colleague from the hub only after the rows are committed', function () { + $hub = vpnHub(); + $support = operator('Support'); + $peer = VpnPeer::factory()->ownedBy($support)->create(); + $hub->addPeer($peer->public_key, $peer->allowed_ip); + + // Sync queue: the job runs the moment it is dispatched, so if that happened + // inside the transaction it would still see a live peer and leave it up. + Livewire::actingAs(operator('Owner'))->test(App\Livewire\Admin\Settings::class) + ->call('revokeStaff', $support->id); + + expect($hub->peerIps())->toBe([]) + ->and(VpnPeer::withTrashed()->count())->toBe(0); // tombstone cleared by the removal +});