where('uuid', $uuid)->firstOrFail(); $this->authorize('downloadConfig', $peer); $this->uuid = $uuid; $this->name = $peer->name; } /** * Every later request re-checks: once revealed, the plaintext sits in the * handoff cache for ten minutes, and an open modal would otherwise keep * handing it out after the access was revoked or reassigned — exactly the * window purgeSecret() exists to close. */ public function hydrate(): void { $peer = VpnPeer::withTrashed()->where('uuid', $this->uuid)->first(); if ($peer === null || Gate::denies('downloadConfig', $peer)) { ConfigHandoff::forget($this->token); $this->token = null; abort(403); } } public function reveal(): void { // A retry must not still be showing the previous attempt's complaint. $this->resetErrorBag('password'); $peer = VpnPeer::query()->where('uuid', $this->uuid)->firstOrFail(); $this->authorize('downloadConfig', $peer); if (! $operator = $this->currentOperator()) { return; } // Rate-limited per user, so a stolen session cannot grind the password. $key = 'vpn-config:'.$operator->id; if (RateLimiter::tooManyAttempts($key, 5)) { $this->addError('password', __('vpn.too_many_attempts', ['seconds' => RateLimiter::availableIn($key)])); return; } if (! Hash::check($this->password, $operator->password)) { RateLimiter::hit($key, 300); Log::warning('VPN config download refused: wrong password', [ 'user_id' => $operator->id, 'peer' => $peer->uuid, ]); $this->addError('password', __('vpn.wrong_password')); return; } RateLimiter::clear($key); $plaintext = ConfigVault::decrypt((string) $peer->config_secret); if ($plaintext === null) { // Wrong or rotated key, or a tampered record. Never guess. $this->addError('password', __('vpn.config_unreadable')); return; } // Counted in the database, not read-modify-written here: two concurrent // retrievals would otherwise record one. This is an audit trail, so // under-reporting is the one failure mode it must not have. VpnPeer::query()->whereKey($peer->getKey())->update([ 'last_downloaded_at' => now(), 'download_count' => DB::raw('download_count + 1'), ]); Log::info('VPN config downloaded', ['user_id' => $operator->id, 'peer' => $peer->uuid]); $this->reset('password'); $this->token = ConfigHandoff::put($plaintext); } public function toggleQr(): void { $this->showQr = ! $this->showQr; } public function filename(): string { return Str::limit(Str::slug($this->name) ?: 'clupilot', 15, '').'.conf'; } public function render() { $config = ConfigHandoff::get($this->token); return view('livewire.admin.vpn-config-access', [ 'config' => $config, 'qrSvg' => $config !== null && $this->showQr ? QrCode::svg($config) : null, ]); } }