diff --git a/app/Provisioning/Jobs/PurgeHost.php b/app/Provisioning/Jobs/PurgeHost.php index af8ed52..61b74da 100644 --- a/app/Provisioning/Jobs/PurgeHost.php +++ b/app/Provisioning/Jobs/PurgeHost.php @@ -3,6 +3,7 @@ namespace App\Provisioning\Jobs; use App\Models\Host; +use App\Models\VpnPeer; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -44,6 +45,14 @@ class PurgeHost implements ShouldQueue } if (filled($host->wg_pubkey)) { + // Tombstone the console's view of this peer as well. The FK only + // nulls host_id, which would leave an enabled-but-absent row behind: + // shown forever as "wird angewendet", and re-enabling it would put a + // deleted host's key back on the hub. Soft-deleting hands it to the + // same machinery every other revocation uses — the removal below + // clears the tombstone, and a sync retries if that removal is lost. + VpnPeer::query()->where('public_key', $host->wg_pubkey)->delete(); + RemoveWireguardPeer::dispatch($host->wg_pubkey); } diff --git a/tests/Feature/Admin/VpnTest.php b/tests/Feature/Admin/VpnTest.php index 99a741d..e82ee8f 100644 --- a/tests/Feature/Admin/VpnTest.php +++ b/tests/Feature/Admin/VpnTest.php @@ -430,3 +430,24 @@ it('does not rename an access an operator named themselves', function () { expect($peer->fresh()->name)->toBe('Notebook Boban'); }); + +it('does not leave a phantom access behind when a host is purged', function () { + $hub = vpnHub(); + $key = Keypair::generate()->publicKey; + $host = Host::factory()->active()->create(['name' => 'pve-fsn1', 'wg_pubkey' => $key]); + $hub->addPeer($key, '10.66.0.7'); + + (new SyncVpnPeers)->handle($hub); // console adopts the host peer + expect(VpnPeer::query()->count())->toBe(1); + + (new App\Provisioning\Jobs\PurgeHost($host->uuid))->handle(); + + // No enabled-but-absent leftover the operator could toggle back on. + expect(VpnPeer::query()->count())->toBe(0) + ->and(VpnPeer::onlyTrashed()->count())->toBe(1); + + // The queued removal clears both the hub peer and the tombstone. + (new ApplyVpnPeer($key, null, false))->handle($hub); + expect($hub->peerIps())->toBe([]) + ->and(VpnPeer::withTrashed()->count())->toBe(0); +});