From 8b1bbf401ad77ab24e1d58560cc7ea54dc3956a3 Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 14 Jun 2026 23:10:36 +0200 Subject: [PATCH] feat(servers): move fail2ban banned-IP list into a scrollable modal The fail2ban-Status panel listed every jail + banned IP inline (very long). Now a compact summary + 'Gesperrte IPs ansehen' opens a scrollable modal (Fail2banBans) that reuses Fail2banService::status/unban and dispatches fail2banChanged. serverId is #[Locked]. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Livewire/Modals/Fail2banBans.php | 112 ++++++++++++++++++ lang/de/servers.php | 4 + lang/en/servers.php | 4 + .../livewire/modals/fail2ban-bans.blade.php | 55 +++++++++ .../views/livewire/servers/show.blade.php | 36 ++---- 5 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 app/Livewire/Modals/Fail2banBans.php create mode 100644 resources/views/livewire/modals/fail2ban-bans.blade.php diff --git a/app/Livewire/Modals/Fail2banBans.php b/app/Livewire/Modals/Fail2banBans.php new file mode 100644 index 0000000..686b487 --- /dev/null +++ b/app/Livewire/Modals/Fail2banBans.php @@ -0,0 +1,112 @@ +serverId = $serverId; + $this->loadJails($fail2ban); + } + + public static function modalMaxWidth(): string + { + return 'lg'; + } + + /** Read the live jail/banned-IP status (same Fail2banService::status the page calls). */ + private function loadJails(Fail2banService $fail2ban): void + { + $server = Server::find($this->serverId); + if (! $server) { + $this->error = __('common.server_not_found'); + + return; + } + + try { + $status = $fail2ban->status($server); + $this->jails = $status['jails'] ?? []; + $this->error = ! empty($status['readError']) ? __('servers.fail2ban_read_error') : null; + $this->loaded = true; + } catch (Throwable $e) { + $this->error = Str::limit($e->getMessage(), 120); + } + } + + /** + * Unban one IP — mirrors Show::unbanIp: call Fail2banService::unban, audit the + * success, refresh this list, and dispatch `fail2banChanged` so the page panel + * re-reads its own fail2ban state. + */ + public function unbanIp(string $jail, string $ip, Fail2banService $fail2ban): void + { + $server = Server::find($this->serverId); + if (! $server) { + $this->error = __('common.server_not_found'); + + return; + } + + try { + $res = $fail2ban->unban($server, $jail, $ip); + } catch (Throwable $e) { + $this->dispatch('notify', message: __('servers.notify_unban_failed', ['error' => Str::limit($e->getMessage(), 90)])); + + return; + } + if (! $res['ok']) { + $this->dispatch('notify', message: __('servers.notify_unban_failed', ['error' => Str::limit($res['output'] ?: __('servers.error_unknown'), 90)])); + + return; + } + + AuditEvent::create([ + 'user_id' => Auth::id(), + 'server_id' => $server->id, + 'actor' => Auth::user()?->name ?? 'system', + 'action' => 'fail2ban.unban', + 'target' => $ip.' · '.$jail.' · '.$server->name, + 'ip' => request()->ip(), + ]); + + $this->dispatch('notify', message: __('servers.notify_ip_unbanned', ['ip' => $ip])); + + // Re-read this modal's list, then tell the page panel to re-pull its own state. + $this->loadJails($fail2ban); + $this->dispatch('fail2banChanged'); + } + + public function render() + { + return view('livewire.modals.fail2ban-bans'); + } +} diff --git a/lang/de/servers.php b/lang/de/servers.php index 2ff282b..b155d7d 100644 --- a/lang/de/servers.php +++ b/lang/de/servers.php @@ -121,6 +121,10 @@ return [ 'fail2ban_banned' => 'gebannt: :count', 'fail2ban_failed' => 'Fehlversuche: :count', 'fail2ban_no_banned' => 'Keine gebannten IPs.', + 'fail2ban_jails_count' => ':count Jails', + 'fail2ban_view_bans' => 'Gesperrte IPs ansehen', + 'fail2ban_bans_title' => 'Gesperrte IPs', + 'fail2ban_bans_subtitle' => 'Aktive fail2ban-Jails und ihre aktuell gesperrten IP-Adressen. Zum Entsperren einer IP auf „Entsperren" tippen.', 'whitelist_label' => 'Whitelist (ignoreip)', 'whitelist_placeholder' => 'IP oder CIDR', diff --git a/lang/en/servers.php b/lang/en/servers.php index e390bec..ac96528 100644 --- a/lang/en/servers.php +++ b/lang/en/servers.php @@ -121,6 +121,10 @@ return [ 'fail2ban_banned' => 'banned: :count', 'fail2ban_failed' => 'failed attempts: :count', 'fail2ban_no_banned' => 'No banned IPs.', + 'fail2ban_jails_count' => ':count jails', + 'fail2ban_view_bans' => 'View banned IPs', + 'fail2ban_bans_title' => 'Banned IPs', + 'fail2ban_bans_subtitle' => 'Active fail2ban jails and their currently banned IP addresses. Tap “Unlock” to unban an IP.', 'whitelist_label' => 'Whitelist (ignoreip)', 'whitelist_placeholder' => 'IP or CIDR', diff --git a/resources/views/livewire/modals/fail2ban-bans.blade.php b/resources/views/livewire/modals/fail2ban-bans.blade.php new file mode 100644 index 0000000..3fd892e --- /dev/null +++ b/resources/views/livewire/modals/fail2ban-bans.blade.php @@ -0,0 +1,55 @@ +
+
+ + + +
+

{{ __('servers.fail2ban_bans_title') }}

+

{{ __('servers.fail2ban_bans_subtitle') }}

+
+
+ + @if ($error) +
+ +

{{ $error }}

+
+ @endif + +
+ @forelse ($jails as $jail) +
+
+

{{ $jail['name'] }}

+

+ ($jail['currentlyBanned'] ?? 0) > 0])>{{ __('servers.fail2ban_banned', ['count' => $jail['currentlyBanned'] ?? 0]) }} + · {{ __('servers.fail2ban_failed', ['count' => $jail['currentlyFailed'] ?? 0]) }} +

+
+ @if (count($jail['bannedIps'] ?? [])) +
+ @foreach ($jail['bannedIps'] as $ip) +
+ {{ $ip }} + {{ __('common.unlock') }} +
+ @endforeach +
+ @else +

{{ __('servers.fail2ban_no_banned') }}

+ @endif +
+ @empty +
+

{{ $loaded ? __('servers.fail2ban_no_banned') : __('servers.loading') }}

+
+ @endforelse +
+ +
+ {{ __('common.close') }} +
+
diff --git a/resources/views/livewire/servers/show.blade.php b/resources/views/livewire/servers/show.blade.php index 67281c6..bd24244 100644 --- a/resources/views/livewire/servers/show.blade.php +++ b/resources/views/livewire/servers/show.blade.php @@ -404,30 +404,18 @@ @else -
- @foreach ($f2bJails as $jail) -
-
-

{{ $jail['name'] }}

-

- ($jail['currentlyBanned'] ?? 0) > 0])>{{ __('servers.fail2ban_banned', ['count' => $jail['currentlyBanned'] ?? 0]) }} - · {{ __('servers.fail2ban_failed', ['count' => $jail['currentlyFailed'] ?? 0]) }} -

-
- @if (count($jail['bannedIps'] ?? [])) -
- @foreach ($jail['bannedIps'] as $ip) -
- {{ $ip }} - {{ __('common.unlock') }} -
- @endforeach -
- @else -

{{ __('servers.fail2ban_no_banned') }}

- @endif -
- @endforeach + {{-- Compact summary; the full jail/banned-IP list (which can grow very + long) lives in a scrollable modal, opened via the button below. --}} +
+

+ {{ __('common.active') }} + · $f2bBanned > 0])>{{ __('servers.fail2ban_banned', ['count' => $f2bBanned]) }} + · {{ __('servers.fail2ban_jails_count', ['count' => count($f2bJails)]) }} +

+ + {{ __('servers.fail2ban_view_bans') }} +
{{-- Whitelist (ignoreip) --}}