fix(vpn): purging a host no longer leaves a phantom access behind

The FK only nulled host_id, so the adopted peer stayed enabled-but-absent: shown
as "wird angewendet" forever, and re-enabling it would have put the deleted
host's key and address back on the hub. PurgeHost now tombstones that row, which
hands it to the same revocation machinery as everything else.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 22:00:13 +02:00
parent c54b718566
commit d1302e17bc
2 changed files with 30 additions and 0 deletions

View File

@ -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);
}

View File

@ -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);
});