window = in_array($seconds, self::WINDOWS, true) ? $seconds : 3600; } public function addPeer(WgBridge $bridge): void { $this->validate(['newPeer' => ['required', 'regex:/^[A-Za-z0-9._-]{1,64}$/']], [ 'newPeer.regex' => __('wireguard.peer_name_invalid'), 'newPeer.required' => __('wireguard.peer_name_invalid'), ]); if (! $this->throttle()) { return; } $name = $this->newPeer; $this->pendingId = $bridge->request('add-peer', ['name' => $name]); $this->pendingAction = 'add-peer'; $this->newPeer = ''; $this->audit('wg.add-peer', $name); } /** Opens the wire-elements/modal confirm dialog for peer removal. */ public function confirmRemovePeer(string $name): void { if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) { return; } $this->dispatch('openModal', component: 'modals.confirm-action', arguments: [ 'heading' => __('wireguard.remove_confirm_title'), 'body' => __('wireguard.remove_confirm_body'), 'confirmLabel' => __('wireguard.remove'), 'danger' => true, 'icon' => 'trash', 'notify' => '', 'token' => ConfirmToken::issue( 'wgPeerRemoved', ['name' => $name], 'wg.remove-peer', $name, ), ], ); } /** Apply handler — called after the ConfirmAction modal confirms the removal. */ #[On('wgPeerRemoved')] public function applyRemovePeer(string $confirmToken, WgBridge $bridge): void { try { $payload = ConfirmToken::consume($confirmToken, 'wgPeerRemoved'); } catch (InvalidConfirmToken) { return; } $name = $payload['params']['name'] ?? ''; if ($name === '' || preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) { return; } $this->removePeer($bridge, $name); } public function removePeer(WgBridge $bridge, string $name): void { if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1 || ! $this->throttle()) { return; } $this->pendingId = $bridge->request('remove-peer', ['name' => $name]); $this->pendingAction = 'remove-peer'; $this->audit('wg.remove-peer', $name); } public function pollResult(WgBridge $bridge): void { if ($this->pendingId === null) { return; } $res = $bridge->result($this->pendingId); if ($res === null) { return; } $this->pendingId = null; if ($res['ok'] && $this->pendingAction === 'add-peer' && $res['config'] !== null) { $this->resultConfig = $res['config']; $this->resultQr = $res['qr']; } elseif (! $res['ok']) { $this->dispatch('notify', message: __('wireguard.action_failed'), level: 'error'); } else { $this->dispatch('notify', message: __('wireguard.action_done')); } $this->pendingAction = null; } public function dismissResult(): void { $this->resultConfig = null; $this->resultQr = null; } private function throttle(): bool { $key = 'wg-request:'.Auth::id(); if (RateLimiter::tooManyAttempts($key, 10)) { $this->dispatch('notify', message: __('wireguard.throttled'), level: 'error'); return false; } RateLimiter::hit($key, 60); return true; } private function audit(string $action, string $target): void { AuditEvent::create([ 'user_id' => Auth::id(), 'actor' => Auth::user()?->name ?? 'system', 'action' => $action, 'target' => $target, 'ip' => request()->ip(), ]); } public function render(WgStatus $wg, WgTraffic $traffic) { $window = in_array($this->window, self::WINDOWS, true) ? $this->window : 3600; return view('livewire.wireguard.index', [ 'status' => $wg->read(), 'traffic' => $traffic->series($window), 'windows' => self::WINDOWS, ])->title(__('wireguard.title')); } }