clusev/app/Livewire/Modals/Fail2banBans.php

121 lines
3.9 KiB
PHP

<?php
namespace App\Livewire\Modals;
use App\Models\AuditEvent;
use App\Models\Server;
use App\Services\Fail2banService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Livewire\Attributes\Locked;
use LivewireUI\Modal\ModalComponent;
use Throwable;
/**
* Scrollable list of fail2ban jails + their currently-banned IPs, lifted out of the
* servers/show panel so a long ban list never stretches the page. mount() only stores
* the serverId so the modal opens INSTANTLY; load() (fired by wire:init, behind a
* skeleton) does the live jail-status read via Fail2banService (the SAME service the
* page uses). unban() calls Fail2banService::unban (the SAME path as Show::unbanIp),
* audits the success, refreshes this modal's list, and dispatches `fail2banChanged`
* so the page panel re-reads too.
*/
class Fail2banBans extends ModalComponent
{
#[Locked]
public int $serverId;
/** Jail status rows (name/currentlyBanned/currentlyFailed/bannedIps) for the list. */
public array $jails = [];
/** True once the live status was read (distinguishes "no bans" from "not loaded"). */
public bool $loaded = false;
public ?string $error = null;
public function mount(int $serverId): void
{
// Store the id ONLY so the modal opens instantly; the SSH read happens in load().
$this->serverId = $serverId;
}
public static function modalMaxWidth(): string
{
return 'lg';
}
/** Lazy: the SSH read runs after the modal renders (wire:init), behind a skeleton. */
public function load(Fail2banService $fail2ban): void
{
$this->loadJails($fail2ban);
}
/** 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');
}
}