Fix: Mailbox Stats über Dovecot mit config/mailpool.php

main v1.0.123
boban 2025-10-31 16:41:54 +01:00
parent dcf9a8d3e9
commit d3783e1717
21 changed files with 764 additions and 10 deletions

0
,
View File

0
0
View File

View File

@ -1,9 +0,0 @@
= App\Models\Setting {#6409
id: 13,
group: "woltguard",
key: "services",
value: "{"ts":1761504019,"rows":[{"name":"postfix","ok":true},{"name":"dovecot","ok":true},{"name":"rspamd","ok":true},{"name":"clamav","ok":true},{"name":"db","ok":true},{"name":"redis","ok":true},{"name":"php-fpm","ok":true},{"name":"nginx","ok":true},{"name":"mw-queue","ok":true},{"name":"mw-schedule","ok":true},{"name":"mw-ws","ok":true},{"name":"fail2ban","ok":true},{"name":"journal","ok":true}]}",
created_at: "2025-10-26 19:40:19",
updated_at: "2025-10-26 19:40:19",
}

View File

View File

View File

@ -0,0 +1,124 @@
<?php
namespace App\Livewire\Ui\Security;
use Livewire\Attributes\On;
use Livewire\Component;
use App\Models\Fail2banSetting;
use App\Models\Fail2banIpList;
class Fail2banSettings extends Component
{
// Formfelder
public int $bantime;
public int $max_bantime;
public bool $bantime_increment;
public float $bantime_factor;
public int $max_retry;
public int $findtime;
public int $cidr_v4;
public int $cidr_v6;
public bool $external_mode;
public array $whitelist = [];
public array $blacklist = [];
public Fail2banSetting $settings;
#[On('f2b:refresh')]
public function refreshLists(): void
{
$this->whitelist = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray();
$this->blacklist = Fail2banIpList::where('type', 'blacklist')->pluck('ip')->toArray();
}
public function mount(): void
{
// Setting holen oder mit Defaults anlegen
$this->settings = Fail2banSetting::first() ?? Fail2banSetting::create([
'bantime' => 3600, 'max_bantime' => 43200, 'bantime_increment' => true,
'bantime_factor' => 1.5, 'max_retry' => 3, 'findtime' => 600,
'cidr_v4' => 32, 'cidr_v6' => 128, 'external_mode' => false,
]);
// Properties füllen (KEINE Mixed-Objekte in Inputs binden)
$this->fill([
'bantime' => (int)$this->settings->bantime,
'max_bantime' => (int)$this->settings->max_bantime,
'bantime_increment' => (bool)$this->settings->bantime_increment,
'bantime_factor' => (float)$this->settings->bantime_factor,
'max_retry' => (int)$this->settings->max_retry,
'findtime' => (int)$this->settings->findtime,
'cidr_v4' => (int)$this->settings->cidr_v4,
'cidr_v6' => (int)$this->settings->cidr_v6,
'external_mode' => (bool)$this->settings->external_mode,
]);
$this->whitelist = Fail2banIpList::where('type','whitelist')->pluck('ip')->toArray();
$this->blacklist = Fail2banIpList::where('type','blacklist')->pluck('ip')->toArray();
}
public function save(): void
{
$this->validate([
'bantime' => 'required|integer|min:60',
'max_bantime' => 'required|integer|min:60',
'bantime_factor' => 'required|numeric|min:1',
'max_retry' => 'required|integer|min:1',
'findtime' => 'required|integer|min:60',
'cidr_v4' => 'required|integer|min:8|max:32',
'cidr_v6' => 'required|integer|min:8|max:128',
]);
$this->settings->update([
'bantime' => $this->bantime,
'max_bantime' => $this->max_bantime,
'bantime_increment' => $this->bantime_increment,
'bantime_factor' => $this->bantime_factor,
'max_retry' => $this->max_retry,
'findtime' => $this->findtime,
'cidr_v4' => $this->cidr_v4,
'cidr_v6' => $this->cidr_v6,
'external_mode' => $this->external_mode,
]);
$this->writeDefaultsConfig();
$this->writeWhitelistConfig();
@shell_exec('sudo fail2ban-client reload');
$this->dispatch('notify', message: 'Gespeichert & Fail2Ban neu geladen.');
}
protected function writeDefaultsConfig(): void
{
$s = $this->settings;
$content = <<<CONF
[DEFAULT]
bantime = {$s->bantime}
findtime = {$s->findtime}
maxretry = {$s->max_retry}
bantime.increment = {$this->boolToStr($s->bantime_increment)}
bantime.factor = {$s->bantime_factor}
bantime.maxtime = {$s->max_bantime}
CONF;
file_put_contents('/etc/fail2ban/jail.d/00-mailwolt-defaults.local', $content);
}
protected function writeWhitelistConfig(): void
{
$ips = Fail2banIpList::where('type','whitelist')->pluck('ip')->toArray();
$ignore = implode(' ', array_unique(array_filter($ips)));
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
file_put_contents('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
}
private function boolToStr(bool $v): string
{
return $v ? 'true' : 'false';
}
public function render()
{
return view('livewire.ui.security.fail2ban-settings');
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace App\Livewire\Ui\Security\Modal;
use LivewireUI\Modal\ModalComponent;
use App\Models\Fail2banIpList;
use Illuminate\Validation\ValidationException;
class Fail2banIpModal extends ModalComponent
{
/** 'whitelist' | 'blacklist' */
public string $type = 'whitelist';
/** 'add' | 'remove' */
public string $mode = 'add';
/** IP/CIDR im Formular */
public string $ip = '';
/** Für "remove" vorbefüllt */
public ?string $prefill = null;
public static function modalMaxWidth(): string { return 'lg'; }
public function mount(string $type = 'whitelist', string $mode = 'add', ?string $ip = null): void
{
$type = strtolower($type);
$mode = strtolower($mode);
if (!in_array($type, ['whitelist', 'blacklist'], true)) {
throw new \InvalidArgumentException('Invalid type');
}
if (!in_array($mode, ['add', 'remove'], true)) {
throw new \InvalidArgumentException('Invalid mode');
}
$this->type = $type;
$this->mode = $mode;
$this->ip = $ip ?? '';
$this->prefill = $ip;
}
public function render()
{
return view('livewire.ui.security.modal.fail2ban-ip-modal');
}
/* ---------------- actions ---------------- */
public function save(): void
{
$this->assertAddMode();
$ip = trim($this->ip);
if (!$this->isValidIpOrCidr($ip)) {
throw ValidationException::withMessages(['ip' => 'Ungültige IP oder CIDR.']);
}
// DB schreiben
Fail2banIpList::firstOrCreate(['ip' => $ip, 'type' => $this->type]);
if ($this->type === 'whitelist') {
$this->writeWhitelistConfig();
$this->reloadFail2ban();
} else {
// Blacklist = sofort bannen im dedizierten Jail
$this->banIp($ip);
}
$this->dispatch('f2b:refresh');
$this->dispatch('notify', message: ucfirst($this->type).' aktualisiert.');
$this->closeModal();
$this->dispatch('f2b:refresh'); // falls du eine Liste neu laden willst
}
public function remove(): void
{
$this->assertRemoveMode();
$ip = trim($this->prefill ?? $this->ip);
if ($ip === '') return;
Fail2banIpList::where('type', $this->type)->where('ip', $ip)->delete();
if ($this->type === 'whitelist') {
$this->writeWhitelistConfig();
$this->reloadFail2ban();
} else {
// aus Blacklist-Jail entbannen, falls noch aktiv
$this->unbanIp($ip);
}
$this->dispatch('f2b:refresh');
$this->dispatch('notify', message: ucfirst($this->type).' Eintrag entfernt.');
$this->closeModal();
$this->dispatch('f2b:refresh');
}
/* ---------------- helper ---------------- */
private function assertAddMode(): void
{
if ($this->mode !== 'add') throw new \LogicException('Wrong mode');
}
private function assertRemoveMode(): void
{
if ($this->mode !== 'remove') throw new \LogicException('Wrong mode');
}
private function isValidIpOrCidr(string $s): bool
{
// IP
if (filter_var($s, FILTER_VALIDATE_IP)) return true;
// CIDR
if (strpos($s, '/') !== false) {
[$ip, $mask] = explode('/', $s, 2);
if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;
if (strpos($ip, ':') !== false) {
// IPv6
return ctype_digit($mask) && (int)$mask >= 8 && (int)$mask <= 128;
}
// IPv4
return ctype_digit($mask) && (int)$mask >= 8 && (int)$mask <= 32;
}
return false;
}
private function writeWhitelistConfig(): void
{
$ips = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray();
$ignore = implode(' ', array_unique(array_filter($ips)));
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
$file = '/etc/fail2ban/jail.d/mailwolt-whitelist.local';
$tmp = $file.'.tmp';
@file_put_contents($tmp, $content, LOCK_EX);
@chmod($tmp, 0644);
@rename($tmp, $file);
}
private function reloadFail2ban(): void
{
@shell_exec('sudo fail2ban-client reload 2>&1');
}
private function banIp(string $ip): void
{
$ipEsc = escapeshellarg($ip);
@shell_exec("sudo fail2ban-client set mailwolt-blacklist banip {$ipEsc} 2>&1");
// optional: in DB zusätzlich behalten, damit UI konsistent ist (bereits oben getan)
}
private function unbanIp(string $ip): void
{
$ipEsc = escapeshellarg($ip);
@shell_exec("sudo fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1");
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Fail2banIpList extends Model
{
protected $table = 'fail2ban_ip_lists';
protected $fillable = [
'ip',
'type',
];
protected $casts = [
'ip' => 'string',
'type' => 'string',
];
const TYPE_WHITELIST = 'whitelist';
const TYPE_BLACKLIST = 'blacklist';
/**
* Scopes
*/
public function scopeWhitelist($query)
{
return $query->where('type', self::TYPE_WHITELIST);
}
public function scopeBlacklist($query)
{
return $query->where('type', self::TYPE_BLACKLIST);
}
/**
* Validiert grob die IP.
*/
public function isValidIp(): bool
{
return filter_var($this->ip, FILTER_VALIDATE_IP) !== false;
}
/**
* Gibt Liste aller Whitelist-IPs als Array zurück.
*/
public static function whitelistArray(): array
{
return static::where('type', self::TYPE_WHITELIST)->pluck('ip')->all();
}
/**
* Gibt Liste aller Blacklist-IPs als Array zurück.
*/
public static function blacklistArray(): array
{
return static::where('type', self::TYPE_BLACKLIST)->pluck('ip')->all();
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Fail2banSetting extends Model
{
protected $fillable = [
'bantime','max_bantime','bantime_increment','bantime_factor',
'max_retry','findtime','cidr_v4','cidr_v6','external_mode',
];
protected $casts = [
'bantime' => 'integer',
'max_bantime' => 'integer',
'bantime_increment' => 'boolean',
'bantime_factor' => 'float',
'max_retry' => 'integer',
'findtime' => 'integer',
'cidr_v4' => 'integer',
'cidr_v6' => 'integer',
'external_mode' => 'boolean',
];
// /**
// * Gibt die erste Konfiguration oder Default-Werte zurück.
// */
// public static function current(): self
// {
// return static::first() ?? new static([
// 'bantime' => 3600,
// 'max_bantime' => 43200,
// 'bantime_increment' => true,
// 'bantime_factor' => 1.5,
// 'max_retry' => 5,
// 'findtime' => 600,
// 'cidr_v4' => 32,
// 'cidr_v6' => 128,
// 'external_mode' => false,
// ]);
// }
/**
* Konvertiert bool zu "true"/"false" (für Config-Dateien).
*/
public function boolToString(bool $val): string
{
return $val ? 'true' : 'false';
}
}

View File

View File

View File

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('fail2ban_settings', function (Blueprint $table) {
$table->id();
$table->integer('bantime')->default(3600);
$table->integer('max_bantime')->default(43200);
$table->boolean('bantime_increment')->default(true);
$table->float('bantime_factor')->default(1.5);
$table->integer('max_retry')->default(3);
$table->integer('findtime')->default(600);
$table->integer('cidr_v4')->default(32);
$table->integer('cidr_v6')->default(128);
$table->boolean('external_mode')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('fail2ban_settings');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('fail2ban_ip_lists', function (Blueprint $table) {
$table->id();
$table->string('ip');
$table->enum('type', ['whitelist', 'blacklist']);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('fail2ban_ip_lists');
}
};

View File

@ -0,0 +1,109 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Fail2banSetting;
use App\Models\Fail2banIpList;
use Illuminate\Support\Facades\Log;
class Fail2banSeeder extends Seeder
{
public function run(): void
{
$this->command->info('⚙️ Fail2ban Defaults werden initialisiert …');
// -----------------------------------------------------------
// 1) Standardwerte für Fail2ban Settings
// -----------------------------------------------------------
$settings = Fail2banSetting::firstOrCreate([], [
'bantime' => 3600, // 1h
'max_bantime' => 43200, // 12h
'bantime_increment' => true,
'bantime_factor' => 1.5,
'max_retry' => 5,
'findtime' => 600, // 10m
'cidr_v4' => 32,
'cidr_v6' => 128,
'external_mode' => false,
]);
// -----------------------------------------------------------
// 2) Standard-IPs für Whitelist
// -----------------------------------------------------------
$defaultWhitelist = [
'127.0.0.1/8',
'::1',
];
foreach ($defaultWhitelist as $ip) {
Fail2banIpList::firstOrCreate([
'ip' => $ip,
'type' => Fail2banIpList::TYPE_WHITELIST,
]);
}
// -----------------------------------------------------------
// 3) Fail2ban Config-Dateien erzeugen
// -----------------------------------------------------------
$this->writeDefaultsConfig($settings);
$this->writeWhitelistConfig();
// -----------------------------------------------------------
// 4) Fail2ban reload (optional, falls Dienst läuft)
// -----------------------------------------------------------
$out = shell_exec('sudo -n fail2ban-client reload 2>&1') ?? '';
if (stripos($out, 'OK') === false && stripos($out, 'Reloaded') === false) {
Log::warning('Fail2ban reload output', ['out' => $out]);
$this->command->warn('⚠️ Fail2ban reload möglicherweise nicht erfolgreich.');
} else {
$this->command->info('✅ Fail2ban reload erfolgreich.');
}
}
// -----------------------------------------------------------
// interne Hilfsfunktionen
// -----------------------------------------------------------
private function writeDefaultsConfig(Fail2banSetting $s): void
{
$content = <<<CONF
[DEFAULT]
bantime = {$s->bantime}
findtime = {$s->findtime}
maxretry = {$s->max_retry}
bantime.increment = {$this->boolToString($s->bantime_increment)}
bantime.factor = {$s->bantime_factor}
bantime.maxtime = {$s->max_bantime}
CONF;
$this->atomicWrite('/etc/fail2ban/jail.d/00-mailwolt-defaults.local', $content);
}
private function writeWhitelistConfig(): void
{
$ips = Fail2banIpList::where('type', Fail2banIpList::TYPE_WHITELIST)
->pluck('ip')
->toArray();
$ignore = implode(' ', array_unique(array_filter($ips)));
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
$this->atomicWrite('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
}
private function atomicWrite(string $path, string $content): void
{
$tmp = $path . '.tmp';
file_put_contents($tmp, $content);
rename($tmp, $path);
@chown($path, 'root');
@chgrp($path, 'root');
@chmod($path, 0644);
}
private function boolToString(bool $v): string
{
return $v ? 'true' : 'false';
}
}

View File

View File

View File

@ -0,0 +1,130 @@
<div class="grid grid-cols-1 xl:grid-cols-3 gap-5">
{{-- LEFT 2/3 --}}
<div class="xl:col-span-2 space-y-5">
<div class="glass-card p-5">
<div class="flex items-center justify-between mb-4">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-shield text-white/70 text-[13px]"></i>
<span class="text-[11px] uppercase tracking-wide text-white/70">Fail2Ban Konfiguration</span>
</div>
<button wire:click="save"
class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-1.5 text-white/80 hover:text-white hover:border-white/20">
<i class="ph ph-floppy-disk text-[14px]"></i> Speichern & Reload
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-white/60 text-sm mb-1">Bantime (Sekunden)</label>
<input type="number" wire:model.defer="bantime"
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
<p class="mt-1 text-xs text-white/45">Standard-Sperrzeit.</p>
</div>
<div>
<label class="block text-white/60 text-sm mb-1">Max. Bantime (Sekunden)</label>
<input type="number" wire:model.defer="max_bantime"
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
<p class="mt-1 text-xs text-white/45">Obergrenze bei dynamischer Erhöhung.</p>
</div>
<div>
<label class="block text-white/60 text-sm mb-1">Findtime (Sekunden)</label>
<input type="number" wire:model.defer="findtime"
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
<p class="mt-1 text-xs text-white/45">Zeitraum für Wiederholungen.</p>
</div>
<div>
<label class="block text-white/60 text-sm mb-1">Max. Retry</label>
<input type="number" wire:model.defer="max_retry"
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
<p class="mt-1 text-xs text-white/45">Fehlversuche bis Bann.</p>
</div>
<div class="md:col-span-2">
<label class="inline-flex items-center gap-2 cursor-pointer select-none group">
<input type="checkbox" wire:model.defer="bantime_increment" class="peer sr-only">
<span class="w-5 h-5 flex items-center justify-center rounded-md border border-white/15 bg-white/5 peer-checked:bg-emerald-500/20 peer-checked:border-emerald-400/40">
<i class="ph ph-check text-[12px] text-emerald-300 opacity-0 peer-checked:opacity-100"></i>
</span>
<span class="text-white/80 text-sm">Bantime dynamisch erhöhen (increment)</span>
</label>
</div>
<div class="md:col-span-2">
<label class="block text-white/60 text-sm mb-1">Erhöhungs-Faktor</label>
<input type="number" step="0.1" wire:model.defer="bantime_factor"
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
<p class="mt-1 text-xs text-white/45">Multiplikator (z. B. 1.5).</p>
</div>
</div>
</div>
<div class="glass-card p-5">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1 mb-4">
<i class="ph ph-info text-white/70 text-[13px]"></i>
<span class="text-[11px] uppercase tracking-wide text-white/70">Hinweise</span>
</div>
<ul class="list-disc list-inside text-sm text-white/60 space-y-1">
<li><strong>bantime.increment</strong> = true bedeutet, dass sich die Sperrzeit bei wiederholten
Angriffen erhöht (z. B. 1 h 1.5 h 2.25 h ).
</li>
<li>Die SQLite-Datenbank befindet sich unter <code>/var/lib/fail2ban/fail2ban.sqlite3</code>.</li>
<li>Alle Änderungen hier werden nach Klick auf <em>„Speichern & Reload“</em> sofort aktiv.</li>
</ul>
</div>
</div>
{{-- RIGHT 1/3 --}}
<div class="space-y-5">
<div class="glass-card p-5">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1 mb-3">
<i class="ph ph-list text-white/70 text-[13px]"></i>
<span class="text-[11px] uppercase tracking-wide text-white/70">Whitelist</span>
</div>
@forelse($whitelist as $ip)
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 mb-2">
<span class="text-white/80 text-sm">{{ $ip }}</span>
<button class="text-[12px] px-2 py-0.5 rounded border border-white/10 hover:border-white/20"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{mode:'remove',type:'whitelist',ip:'{{ $ip }}'}})">
Entfernen
</button>
</div>
@empty
<div class="text-sm text-white/50">Keine Einträge.</div>
@endforelse
<button class="primary-btn w-full justify-center mt-2"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{type:'whitelist'}})">
IP hinzufügen
</button>
</div>
<div class="glass-card p-5">
<div class="inline-flex items-center gap-2 rounded-full bg-rose-500/10 border border-rose-400/30 px-2.5 py-1 mb-3">
<i class="ph ph-hand text-rose-300 text-[13px]"></i>
<span class="text-[11px] uppercase tracking-wide text-rose-300">Blacklist</span>
</div>
@forelse($blacklist as $ip)
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 mb-2">
<span class="text-white/80 text-sm">{{ $ip }}</span>
<button class="text-[12px] px-2 py-0.5 rounded border border-white/10 hover:border-white/20"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{mode:'remove',type:'blacklist',ip:'{{ $ip }}'}})">
Entfernen
</button>
</div>
@empty
<div class="text-sm text-white/50">Keine Einträge.</div>
@endforelse
<button class="text-[13px] w-full px-3 py-2 rounded-xl border border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50"
wire:click="$dispatch('openModal',{component:'ui.security.modal.fail2ban-ip-modal',arguments:{type:'blacklist'}})">
IP hinzufügen
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,55 @@
<div class="p-5">
{{-- Header --}}
<div class="flex items-center justify-between mb-4">
<div class="inline-flex items-center gap-2 rounded-full
{{ $type === 'blacklist' ? 'bg-rose-500/10 border border-rose-400/30' : 'bg-white/5 border border-white/10' }}
px-2.5 py-1">
<i class="ph {{ $type === 'blacklist' ? 'ph-hand text-rose-300' : 'ph-list text-white/70' }} text-[13px]"></i>
<span class="text-[11px] uppercase tracking-wide
{{ $type === 'blacklist' ? 'text-rose-300' : 'text-white/70' }}">
{{ strtoupper($type) }} {{ $mode === 'add' ? 'hinzufügen' : 'entfernen' }}
</span>
</div>
<button type="button" wire:click="$dispatch('closeModal')"
class="rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-white/70 hover:text-white">
Schließen
</button>
</div>
{{-- Body --}}
<div class="space-y-3">
@if($mode === 'add')
<div>
<label class="block text-white/60 text-sm mb-1">IP oder CIDR</label>
<input type="text" wire:model.defer="ip" placeholder="z. B. 203.0.113.4 oder 203.0.113.0/24 oder 2001:db8::/32"
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
@error('ip') <p class="text-sm text-rose-400 mt-1">{{ $message }}</p> @enderror
</div>
<button wire:click="save"
class="primary-btn w-full justify-center">
{{ $type === 'blacklist' ? 'Zur Blacklist hinzufügen & bannen' : 'Zur Whitelist hinzufügen' }}
</button>
@if($type === 'blacklist')
<p class="text-xs text-white/50 mt-2">
Wird sofort im Jail <code>mailwolt-blacklist</code> gebannt (bantime = permanent).
</p>
@endif
@else
<div class="rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2">
<div class="text-white/80 text-sm">IP: {{ $prefill ?? $ip }}</div>
<div class="text-white/50 text-xs">Wird aus der {{ $type }} entfernt
@if($type === 'blacklist') und im Blacklist-Jail entbannt @endif.
</div>
</div>
<button wire:click="remove"
class="text-[13px] w-full px-3 py-2 rounded-xl border
{{ $type === 'blacklist'
? 'border-rose-400/40 bg-rose-500/10 text-rose-200 hover:border-rose-400/70'
: 'border-white/20 bg-white/5 text-white/80 hover:border-white/40' }}">
Entfernen
</button>
@endif
</div>
</div>

View File

@ -1 +1,10 @@
<?php @extends('layouts.app')
@section('title', 'Fail2Ban')
@section('header_title', 'Fail2Ban')
@section('content')
<livewire:ui.security.fail2ban-settings />
@endsection

View File