diff --git a/app/Livewire/Wireguard/Index.php b/app/Livewire/Wireguard/Index.php index 71ae6cf..808e16f 100644 --- a/app/Livewire/Wireguard/Index.php +++ b/app/Livewire/Wireguard/Index.php @@ -27,6 +27,12 @@ class Index extends Component public string $newPeer = ''; + public string $newEndpoint = ''; + + public string $newPort = ''; + + public string $newSubnet = ''; + /** id of an in-flight write-request we are polling for. */ public ?string $pendingId = null; @@ -112,6 +118,156 @@ class Index extends Component $this->audit('wg.remove-peer', $name); } + public function setEndpoint(WgBridge $bridge): void + { + $this->validate(['newEndpoint' => ['required', 'regex:/^[A-Za-z0-9.:_-]{1,128}$/']], [ + 'newEndpoint.regex' => __('wireguard.endpoint_invalid'), + 'newEndpoint.required' => __('wireguard.endpoint_invalid'), + ]); + if (! $this->throttle()) { + return; + } + $this->pendingId = $bridge->request('set-endpoint', ['endpoint' => $this->newEndpoint]); + $this->pendingAction = 'set-endpoint'; + $this->audit('wg.set-endpoint', $this->newEndpoint); + $this->newEndpoint = ''; + } + + public function confirmGate(bool $on): void + { + $this->dispatch('openModal', + component: 'modals.confirm-action', + arguments: [ + 'heading' => $on ? __('wireguard.gate_on_title') : __('wireguard.gate_off_title'), + 'body' => $on ? __('wireguard.gate_on_body') : __('wireguard.gate_off_body'), + 'confirmLabel' => $on ? __('wireguard.gate_turn_on') : __('wireguard.gate_turn_off'), + 'danger' => ! $on, + 'icon' => 'shield', + 'notify' => '', + 'token' => ConfirmToken::issue( + 'wgGateToggle', + ['on' => $on ? '1' : '0'], + $on ? 'wg.gate-up' : 'wg.gate-down', + $on ? 'on' : 'off', + ), + ], + ); + } + + #[On('wgGateToggle')] + public function applyGateToggle(string $confirmToken, WgBridge $bridge): void + { + try { + $payload = ConfirmToken::consume($confirmToken, 'wgGateToggle'); + } catch (InvalidConfirmToken) { + return; + } + + $this->runGate(($payload['params']['on'] ?? '0') === '1', $bridge); + } + + public function runGate(bool $on, ?WgBridge $bridge = null): void + { + if (! $this->throttle()) { + return; + } + $bridge ??= app(WgBridge::class); + $this->pendingId = $bridge->request($on ? 'gate-up' : 'gate-down', []); + $this->pendingAction = $on ? 'gate-up' : 'gate-down'; + $this->audit($on ? 'wg.gate-up' : 'wg.gate-down', $on ? 'on' : 'off'); + } + + public function confirmSetPort(): void + { + $this->validate( + ['newPort' => ['required', 'regex:/^\d{1,5}$/']], + ['newPort.regex' => __('wireguard.port_invalid'), 'newPort.required' => __('wireguard.port_invalid')], + ); + if ((int) $this->newPort < 1 || (int) $this->newPort > 65535) { + $this->addError('newPort', __('wireguard.port_invalid')); + + return; + } + + $this->dispatch('openModal', + component: 'modals.confirm-action', + arguments: [ + 'heading' => __('wireguard.port_confirm_title'), + 'body' => __('wireguard.port_confirm_body'), + 'confirmLabel' => __('wireguard.apply'), + 'danger' => true, + 'icon' => 'alert', + 'notify' => '', + 'token' => ConfirmToken::issue( + 'wgSetPort', + ['port' => $this->newPort], + 'wg.set-port', + $this->newPort, + ), + ], + ); + } + + #[On('wgSetPort')] + public function applySetPort(string $confirmToken, WgBridge $bridge): void + { + try { + $payload = ConfirmToken::consume($confirmToken, 'wgSetPort'); + } catch (InvalidConfirmToken) { + return; + } + + if (! $this->throttle()) { + return; + } + $this->pendingId = $bridge->request('set-port', ['port' => (string) ($payload['params']['port'] ?? '')]); + $this->pendingAction = 'set-port'; + $this->newPort = ''; + } + + public function confirmSetSubnet(): void + { + $this->validate( + ['newSubnet' => ['required', 'regex:#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#']], + ['newSubnet.regex' => __('wireguard.subnet_invalid'), 'newSubnet.required' => __('wireguard.subnet_invalid')], + ); + + $this->dispatch('openModal', + component: 'modals.confirm-action', + arguments: [ + 'heading' => __('wireguard.subnet_confirm_title'), + 'body' => __('wireguard.subnet_confirm_body'), + 'confirmLabel' => __('wireguard.apply'), + 'danger' => true, + 'icon' => 'alert', + 'notify' => '', + 'token' => ConfirmToken::issue( + 'wgSetSubnet', + ['subnet' => $this->newSubnet], + 'wg.set-subnet', + $this->newSubnet, + ), + ], + ); + } + + #[On('wgSetSubnet')] + public function applySetSubnet(string $confirmToken, WgBridge $bridge): void + { + try { + $payload = ConfirmToken::consume($confirmToken, 'wgSetSubnet'); + } catch (InvalidConfirmToken) { + return; + } + + if (! $this->throttle()) { + return; + } + $this->pendingId = $bridge->request('set-subnet', ['subnet' => (string) ($payload['params']['subnet'] ?? '')]); + $this->pendingAction = 'set-subnet'; + $this->newSubnet = ''; + } + public function pollResult(WgBridge $bridge): void { if ($this->pendingId === null) { diff --git a/app/Support/Confirm/ConfirmToken.php b/app/Support/Confirm/ConfirmToken.php index d740b39..f3aac14 100644 --- a/app/Support/Confirm/ConfirmToken.php +++ b/app/Support/Confirm/ConfirmToken.php @@ -57,6 +57,9 @@ class ConfirmToken 'banCleared', 'bansClearedAll', 'wgPeerRemoved', + 'wgGateToggle', + 'wgSetPort', + 'wgSetSubnet', ]; /** How long an issued confirm stays valid (seconds) — generous for a human click. */ diff --git a/lang/de/wireguard.php b/lang/de/wireguard.php index caaff12..1e0a783 100644 --- a/lang/de/wireguard.php +++ b/lang/de/wireguard.php @@ -46,4 +46,26 @@ return [ 'new_peer_title' => 'Neuer Peer — Konfiguration', 'new_peer_once' => 'Diese Konfiguration wird NUR EINMAL angezeigt und nicht gespeichert. Jetzt importieren (QR scannen oder Text kopieren).', 'close' => 'Schließen', + 'settings_title' => 'Einstellungen', + 'apply' => 'Anwenden', + 'gate_toggle' => 'Gate', + 'gate_turn_on' => 'Einschalten', + 'gate_turn_off' => 'Ausschalten', + 'gate_on_title' => 'Gate einschalten?', + 'gate_on_body' => 'Das Panel ist danach nur noch über den Tunnel erreichbar. Vorher sicherstellen, dass der Tunnel das Panel erreicht — sonst nur per SSH ("clusev wg down") zurück.', + 'gate_off_title' => 'Gate ausschalten?', + 'gate_off_body' => 'Das Panel wird wieder öffentlich erreichbar (nur 2FA + Anmeldeschutz schützen dann).', + 'endpoint_label' => 'Öffentlicher Endpoint', + 'endpoint_hint' => 'IP oder DNS:Port — betrifft nur künftige Peer-Configs.', + 'endpoint_invalid' => 'Ungültiger Endpoint.', + 'port_label' => 'Listen-Port (UDP)', + 'port_hint' => 'Ändern startet den Tunnel neu — alle Clients müssen den Port aktualisieren.', + 'port_invalid' => 'Port 1–65535.', + 'port_confirm_title' => 'Port ändern?', + 'port_confirm_body' => 'Der Tunnel wird neu gestartet. Alle Clients müssen den neuen Port in ihrer Config eintragen, sonst verlieren sie die Verbindung.', + 'subnet_label' => 'Subnetz', + 'subnet_hint' => 'Stark destruktiv — re-adressiert den Server; bestehende Peers müssen neu angelegt werden.', + 'subnet_invalid' => 'Ungültiges Subnetz (CIDR).', + 'subnet_confirm_title' => 'Subnetz ändern?', + 'subnet_confirm_body' => 'Der Server bekommt eine neue Adresse und der Tunnel wird neu gestartet. ALLE bestehenden Peers verlieren den Zugang und müssen neu angelegt werden.', ]; diff --git a/lang/en/wireguard.php b/lang/en/wireguard.php index 11810f0..7fb500e 100644 --- a/lang/en/wireguard.php +++ b/lang/en/wireguard.php @@ -46,4 +46,26 @@ return [ 'new_peer_title' => 'New peer — configuration', 'new_peer_once' => 'This configuration is shown ONCE and never stored. Import it now (scan the QR or copy the text).', 'close' => 'Close', + 'settings_title' => 'Settings', + 'apply' => 'Apply', + 'gate_toggle' => 'Gate', + 'gate_turn_on' => 'Turn on', + 'gate_turn_off' => 'Turn off', + 'gate_on_title' => 'Turn gate on?', + 'gate_on_body' => 'The panel will then only be reachable through the tunnel. Make sure the tunnel reaches the panel first — otherwise only SSH ("clusev wg down") gets you back.', + 'gate_off_title' => 'Turn gate off?', + 'gate_off_body' => 'The panel becomes publicly reachable again (only 2FA + login protection then guard it).', + 'endpoint_label' => 'Public endpoint', + 'endpoint_hint' => 'IP or DNS:port — affects only future peer configs.', + 'endpoint_invalid' => 'Invalid endpoint.', + 'port_label' => 'Listen port (UDP)', + 'port_hint' => 'Changing this restarts the tunnel — all clients must update the port.', + 'port_invalid' => 'Port 1–65535.', + 'port_confirm_title' => 'Change port?', + 'port_confirm_body' => 'The tunnel restarts. All clients must set the new port in their config or they lose the connection.', + 'subnet_label' => 'Subnet', + 'subnet_hint' => 'Highly destructive — re-addresses the server; existing peers must be re-created.', + 'subnet_invalid' => 'Invalid subnet (CIDR).', + 'subnet_confirm_title' => 'Change subnet?', + 'subnet_confirm_body' => 'The server gets a new address and the tunnel restarts. ALL existing peers lose access and must be re-created.', ]; diff --git a/resources/views/livewire/wireguard/index.blade.php b/resources/views/livewire/wireguard/index.blade.php index 9988ee7..0c1b48a 100644 --- a/resources/views/livewire/wireguard/index.blade.php +++ b/resources/views/livewire/wireguard/index.blade.php @@ -150,6 +150,67 @@
{{ __('wireguard.port_hint') }}
+ @error('newPort') {{ $message }} @enderror +{{ __('wireguard.subnet_hint') }}
+ @error('newSubnet') {{ $message }} @enderror +