fix(vpn): classify adopted peers as system, not staff; drop duplicate env line

Codex spotted the migration labelling sync-adopted peers as staff with no
owner, which breaks the invariant the same migration introduces. The live sync
had the identical hole — it never set kind at all, so every adopted peer landed
on the 'staff' default. Both now use system for a peer nobody is behind, and
promote it to host once the link is known.

.env.example carried ADMIN_HOSTS twice; phpdotenv keeps the first, so a fresh
checkout would have got the list without localhost and 404'd its own console.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 22:34:18 +02:00
parent 0d9b62eb50
commit 1112b4cdec
4 changed files with 20 additions and 6 deletions

View File

@ -168,7 +168,6 @@ MONITORING_ATTEMPTS=2
# Erzeugen: head -c 32 /dev/urandom | base64
VPN_CONFIG_KEY=
ADMIN_HOSTS=admin.dev.clupilot.com,10.10.90.185
ADMIN_HOSTS=admin.dev.clupilot.com,10.10.90.185,localhost,127.0.0.1
# ── Nextcloud blueprint template ─────────────────────────────────────────

View File

@ -99,6 +99,9 @@ class SyncVpnPeers implements ShouldBeUnique, ShouldQueue
// never created through the console.
'allowed_ip' => strtok($snapshot->allowedIps, '/') ?: $snapshot->allowedIps,
'host_id' => $host?->id,
// Never "staff": a staff access has an owner, and nobody is
// behind a peer we merely found on the interface.
'kind' => $host !== null ? VpnPeer::KIND_HOST : VpnPeer::KIND_SYSTEM,
'name' => $host?->name ?? __('vpn.unknown_peer'),
'enabled' => true,
]);
@ -112,6 +115,9 @@ class SyncVpnPeers implements ShouldBeUnique, ShouldQueue
// (created_by is null exactly for sync-adopted rows).
if ($peer->host_id === null && $host !== null) {
$observed['host_id'] = $host->id;
if ($peer->kind === VpnPeer::KIND_SYSTEM) {
$observed['kind'] = VpnPeer::KIND_HOST;
}
if ($peer->created_by === null) {
$observed['name'] = $host->name;
}

View File

@ -32,13 +32,18 @@ return new class extends Migration
$table->unsignedInteger('download_count')->default(0);
});
// Existing rows: pipeline peers are host peers, the rest belong to
// whoever created them (created_by is the only ownership hint we have).
// Existing rows fall into three groups, and the difference matters:
// pipeline peers are host peers; peers someone created in the console
// belong to that person; and peers the sync adopted off the interface
// have no human behind them at all — calling those "staff" would break
// the very invariant this migration introduces (a staff access has an
// owner).
DB::table('vpn_peers')->whereNotNull('host_id')->update(['kind' => 'host']);
DB::table('vpn_peers')->whereNull('host_id')->update([
DB::table('vpn_peers')->whereNull('host_id')->whereNotNull('created_by')->update([
'kind' => 'staff',
'user_id' => DB::raw('created_by'),
]);
DB::table('vpn_peers')->whereNull('host_id')->whereNull('created_by')->update(['kind' => 'system']);
}
public function down(): void

View File

@ -223,6 +223,7 @@ it('adopts host peers from the hub and marks vanished ones absent', function ()
$adopted = VpnPeer::query()->where('public_key', $key)->firstOrFail();
expect($adopted->name)->toBe('pve-fsn1') // named after its host, not "unknown"
->and($adopted->kind)->toBe(VpnPeer::KIND_HOST)
->and($adopted->host_id)->toBe($host->id)
->and($adopted->present)->toBeTrue()
->and($adopted->rx_bytes)->toBe(2048)
@ -456,13 +457,16 @@ it('renames a peer it adopted before the host link was known', function () {
(new SyncVpnPeers)->handle($hub);
$peer = VpnPeer::query()->where('public_key', $key)->firstOrFail();
expect($peer->name)->toBe(__('vpn.unknown_peer'))
->and($peer->host_id)->toBeNull();
->and($peer->host_id)->toBeNull()
// Not staff: a staff access has an owner, and this one has nobody.
->and($peer->kind)->toBe(VpnPeer::KIND_SYSTEM);
Host::factory()->active()->create(['name' => 'pve-fsn1', 'wg_pubkey' => $key]);
(new SyncVpnPeers)->handle($hub);
expect($peer->fresh()->name)->toBe('pve-fsn1')
->and($peer->fresh()->host_id)->not->toBeNull();
->and($peer->fresh()->host_id)->not->toBeNull()
->and($peer->fresh()->kind)->toBe(VpnPeer::KIND_HOST);
});
it('does not rename an access an operator named themselves', function () {