Compare commits
12 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
af369acbf6 | |
|
|
d81c3bc07c | |
|
|
821a2bde33 | |
|
|
8e68051fde | |
|
|
afb8d09db3 | |
|
|
fc04ef44d0 | |
|
|
a7d84899fb | |
|
|
e3dc81ef73 | |
|
|
9acea7b89b | |
|
|
6c3cde5f65 | |
|
|
77f22518c8 | |
|
|
9aa9475387 |
|
|
@ -31,7 +31,6 @@ if (!function_exists('webmail_host')) {
|
|||
if (!function_exists('mta_host')) {
|
||||
function mta_host(?int $domainId = null): string
|
||||
{
|
||||
// 1️⃣ Vorrang: Datenbankwert (z. B. aus der domains-Tabelle)
|
||||
if ($domainId) {
|
||||
try {
|
||||
$domain = \App\Models\Domain::find($domainId);
|
||||
|
|
@ -39,17 +38,25 @@ if (!function_exists('mta_host')) {
|
|||
return $domain->mta_host;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// DB evtl. noch nicht migriert — fallback auf env
|
||||
// DB evtl. noch nicht migriert — fallback auf env
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ ENV-Variante (z. B. MTA_SUB=mail01)
|
||||
$sub = env('MTA_SUB');
|
||||
if ($sub) {
|
||||
return domain_host($sub);
|
||||
}
|
||||
|
||||
// 3️⃣ Notfall: statischer Fallback
|
||||
return domain_host('mx');
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('countryFlag')) {
|
||||
function countryFlag(string $code): string
|
||||
{
|
||||
$code = strtoupper($code);
|
||||
return implode('', array_map(
|
||||
fn($char) => mb_chr(ord($char) + 127397, 'UTF-8'),
|
||||
str_split($code)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -92,7 +92,7 @@ class MailboxCreateModal extends ModalComponent
|
|||
{
|
||||
// alle Nicht-System-Domains in Select
|
||||
$this->domains = Domain::query()
|
||||
->where('is_system', false)
|
||||
->where('is_system', false)->where('is_server', false)
|
||||
->orderBy('domain')->get(['id', 'domain'])->toArray();
|
||||
|
||||
// vorselektieren falls mitgegeben, sonst 1. Domain (falls vorhanden)
|
||||
|
|
@ -291,251 +291,3 @@ class MailboxCreateModal extends ModalComponent
|
|||
return view('livewire.ui.mail.modal.mailbox-create-modal');
|
||||
}
|
||||
}
|
||||
|
||||
//namespace App\Livewire\Ui\Mail\Modal;
|
||||
//
|
||||
//use App\Models\Domain;
|
||||
//use App\Models\MailUser;
|
||||
//use Illuminate\Database\QueryException;
|
||||
//use Illuminate\Support\Facades\Hash;
|
||||
//use Illuminate\Validation\Rule;
|
||||
//use Livewire\Attributes\On;
|
||||
//use LivewireUI\Modal\ModalComponent;
|
||||
//
|
||||
//class MailboxCreateModal extends ModalComponent
|
||||
//{
|
||||
// // optional vorselektierte Domain
|
||||
// public ?int $domain_id = null;
|
||||
//
|
||||
// // Anzeige
|
||||
// public string $domain_name = '';
|
||||
// /** @var array<int,array{id:int,domain:string}> */
|
||||
// public array $domains = [];
|
||||
// public string $email_preview = '';
|
||||
//
|
||||
// public string $localpart = '';
|
||||
// public ?string $display_name = null;
|
||||
// public ?string $password = null;
|
||||
// public int $quota_mb = 0;
|
||||
// public ?int $rate_limit_per_hour = null;
|
||||
// public bool $is_active = true;
|
||||
// public bool $must_change_pw = true;
|
||||
//
|
||||
// // Limits / Status
|
||||
// public ?int $limit_max_mailboxes = null;
|
||||
// public ?int $limit_default_quota_mb = null;
|
||||
// public ?int $limit_max_quota_per_mb = null;
|
||||
// public ?int $limit_total_quota_mb = null; // 0 = unlimitiert
|
||||
// public ?int $limit_domain_rate_per_hour = null;
|
||||
// public bool $allow_rate_limit_override = false;
|
||||
//
|
||||
// public int $mailbox_count_used = 0;
|
||||
// public int $domain_storage_used_mb = 0;
|
||||
//
|
||||
// // Hints/Flags
|
||||
// public string $quota_hint = '';
|
||||
// public bool $rate_limit_readonly = false;
|
||||
// public bool $no_mailbox_slots = false;
|
||||
// public bool $no_storage_left = false;
|
||||
// public bool $can_create = true;
|
||||
// public string $block_reason = '';
|
||||
//
|
||||
// /* ---------- Validation ---------- */
|
||||
// protected function rules(): array
|
||||
// {
|
||||
// $maxPerMailbox = $this->limit_max_quota_per_mb ?? PHP_INT_MAX;
|
||||
// $remainingByTotal = (is_null($this->limit_total_quota_mb) || (int)$this->limit_total_quota_mb === 0)
|
||||
// ? PHP_INT_MAX
|
||||
// : max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb);
|
||||
// $cap = min($maxPerMailbox, $remainingByTotal);
|
||||
//
|
||||
// return [
|
||||
// 'domain_id' => ['required', Rule::exists('domains', 'id')],
|
||||
// 'localpart' => [
|
||||
// 'required', 'max:191', 'regex:/^[A-Za-z0-9._%+-]+$/',
|
||||
// Rule::unique('mail_users', 'localpart')->where(fn($q) => $q->where('domain_id', $this->domain_id)),
|
||||
// ],
|
||||
// 'display_name' => ['nullable', 'max:191'],
|
||||
// 'password' => ['nullable', 'min:8'],
|
||||
// 'quota_mb' => ['required', 'integer', 'min:0', 'max:' . $cap],
|
||||
// 'rate_limit_per_hour' => ['nullable', 'integer', 'min:1'],
|
||||
// 'is_active' => ['boolean'],
|
||||
// 'must_change_pw' => ['boolean'],
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// /* ---------- Lifecycle ---------- */
|
||||
// public function mount(?int $domainId = null): void
|
||||
// {
|
||||
// // alle Nicht-System-Domains in Select
|
||||
// $this->domains = Domain::query()
|
||||
// ->where('is_system', false)
|
||||
// ->orderBy('domain')->get(['id', 'domain'])->toArray();
|
||||
//
|
||||
// // vorselektieren falls mitgegeben, sonst 1. Domain (falls vorhanden)
|
||||
// $this->domain_id = $domainId ?: ($this->domains[0]['id'] ?? null);
|
||||
//
|
||||
// // Limits + Anzeige laden
|
||||
// $this->syncDomainContext();
|
||||
// }
|
||||
//
|
||||
// public function updatedDomainId(): void
|
||||
// {
|
||||
// $this->resetErrorBag(); // scoped unique etc.
|
||||
// $this->syncDomainContext();
|
||||
// }
|
||||
//
|
||||
// public function updatedLocalpart(): void
|
||||
// {
|
||||
// $this->localpart = strtolower(trim($this->localpart));
|
||||
// $this->rebuildEmailPreview();
|
||||
// }
|
||||
//
|
||||
// public function updatedQuotaMb(): void
|
||||
// {
|
||||
// $this->recomputeQuotaHints();
|
||||
// $this->recomputeBlockers();
|
||||
// }
|
||||
//
|
||||
// /* ---------- Helpers ---------- */
|
||||
// private function syncDomainContext(): void
|
||||
// {
|
||||
// if (!$this->domain_id) return;
|
||||
//
|
||||
// $d = Domain::query()
|
||||
// ->withCount('mailUsers')
|
||||
// ->withSum('mailUsers as used_storage_mb', 'quota_mb')
|
||||
// ->findOrFail($this->domain_id);
|
||||
//
|
||||
// $this->domain_name = $d->domain;
|
||||
// $this->limit_max_mailboxes = (int)$d->max_mailboxes;
|
||||
// $this->limit_default_quota_mb = (int)$d->default_quota_mb;
|
||||
// $this->limit_max_quota_per_mb = $d->max_quota_per_mailbox_mb !== null ? (int)$d->max_quota_per_mailbox_mb : null;
|
||||
// $this->limit_total_quota_mb = (int)$d->total_quota_mb; // 0 = unlimitiert
|
||||
// $this->limit_domain_rate_per_hour = $d->rate_limit_per_hour !== null ? (int)$d->rate_limit_per_hour : null;
|
||||
// $this->allow_rate_limit_override = (bool)$d->rate_limit_override;
|
||||
//
|
||||
// $this->mailbox_count_used = (int)$d->mail_users_count;
|
||||
// $this->domain_storage_used_mb = (int)($d->used_storage_mb ?? 0);
|
||||
//
|
||||
// // Defaults
|
||||
// $this->quota_mb = $this->limit_default_quota_mb ?? 0;
|
||||
// if (!$this->allow_rate_limit_override) {
|
||||
// $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour;
|
||||
// $this->rate_limit_readonly = true;
|
||||
// } else {
|
||||
// $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour;
|
||||
// $this->rate_limit_readonly = false;
|
||||
// }
|
||||
//
|
||||
// $this->rebuildEmailPreview();
|
||||
// $this->recomputeQuotaHints();
|
||||
// $this->recomputeBlockers();
|
||||
// }
|
||||
//
|
||||
// private function rebuildEmailPreview(): void
|
||||
// {
|
||||
// $this->email_preview = $this->localpart && $this->domain_name
|
||||
// ? ($this->localpart . '@' . $this->domain_name) : '';
|
||||
// }
|
||||
//
|
||||
// private function recomputeQuotaHints(): void
|
||||
// {
|
||||
// $parts = [];
|
||||
//
|
||||
// if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) {
|
||||
// $remainingNow = max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb);
|
||||
// $remainingAfter = max(0, $remainingNow - max(0, (int)$this->quota_mb));
|
||||
// $parts[] = "Verbleibend jetzt: {$remainingNow} MiB";
|
||||
// $parts[] = "nach Speichern: {$remainingAfter} MiB";
|
||||
// }
|
||||
// if (!is_null($this->limit_max_quota_per_mb)) $parts[] = "Max {$this->limit_max_quota_per_mb} MiB pro Postfach";
|
||||
// if (!is_null($this->limit_default_quota_mb)) $parts[] = "Standard: {$this->limit_default_quota_mb} MiB";
|
||||
//
|
||||
// $this->quota_hint = implode(' · ', $parts);
|
||||
// }
|
||||
//
|
||||
// private function recomputeBlockers(): void
|
||||
// {
|
||||
// // Slots
|
||||
// $this->no_mailbox_slots = false;
|
||||
// if (!is_null($this->limit_max_mailboxes)) {
|
||||
// $free = (int)$this->limit_max_mailboxes - (int)$this->mailbox_count_used;
|
||||
// if ($free <= 0) $this->no_mailbox_slots = true;
|
||||
// }
|
||||
//
|
||||
// // Speicher
|
||||
// $this->no_storage_left = false;
|
||||
// if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) {
|
||||
// $remaining = (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb;
|
||||
// if ($remaining <= 0) $this->no_storage_left = true;
|
||||
// }
|
||||
//
|
||||
// $reasons = [];
|
||||
// if ($this->no_mailbox_slots) $reasons[] = 'Keine freien Postfach-Slots in dieser Domain.';
|
||||
// if ($this->no_storage_left) $reasons[] = 'Kein Domain-Speicher mehr verfügbar.';
|
||||
// $this->block_reason = implode(' ', $reasons);
|
||||
// $this->can_create = !($this->no_mailbox_slots || $this->no_storage_left);
|
||||
// }
|
||||
//
|
||||
// /* ---------- Save ---------- */
|
||||
// #[On('mailbox:create')]
|
||||
// public function save(): void
|
||||
// {
|
||||
// $this->recomputeBlockers();
|
||||
// if (!$this->can_create) {
|
||||
// $this->addError('domain_id', $this->block_reason ?: 'Erstellung aktuell nicht möglich.');
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// $data = $this->validate();
|
||||
// $email = $data['localpart'] . '@' . $this->domain_name;
|
||||
//
|
||||
// try {
|
||||
// $u = new MailUser();
|
||||
// $u->domain_id = $data['domain_id'];
|
||||
// $u->localpart = $data['localpart'];
|
||||
// $u->email = $email;
|
||||
// $u->display_name = $this->display_name ?: null;
|
||||
// $u->password_hash = $this->password ? Hash::make($this->password) : null;
|
||||
// $u->is_system = false;
|
||||
// $u->is_active = (bool)$data['is_active'];
|
||||
// $u->must_change_pw = (bool)$data['must_change_pw'];
|
||||
// $u->quota_mb = (int)$data['quota_mb'];
|
||||
// $u->rate_limit_per_hour = $data['rate_limit_per_hour'];
|
||||
// $u->save();
|
||||
// } catch (QueryException $e) {
|
||||
// $msg = strtolower($e->getMessage());
|
||||
// if (str_contains($msg, 'mail_users_domain_localpart_unique')) {
|
||||
// $this->addError('localpart', 'Dieses Postfach existiert in dieser Domain bereits.');
|
||||
// return;
|
||||
// }
|
||||
// if (str_contains($msg, 'mail_users_email_unique')) {
|
||||
// $this->addError('localpart', 'Diese E-Mail-Adresse ist bereits vergeben.');
|
||||
// return;
|
||||
// }
|
||||
// throw $e;
|
||||
// }
|
||||
//
|
||||
// $this->dispatch('mailbox:created');
|
||||
// $this->dispatch('closeModal');
|
||||
// $this->dispatch('toast',
|
||||
// type: 'done',
|
||||
// badge: 'Postfach',
|
||||
// title: 'Postfach angelegt',
|
||||
// text: 'Das Postfach <b>' . e($email) . '</b> wurde erfolgreich angelegt.',
|
||||
// duration: 6000
|
||||
// );
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public static function modalMaxWidth(): string
|
||||
// {
|
||||
// return '3xl';
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.mail.modal.mailbox-create-modal');
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\Security;
|
||||
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Fail2banBanlist extends Component
|
||||
{
|
||||
/**
|
||||
* null oder '*' => alle Jails
|
||||
* 'recidive' => nur dieses Jail
|
||||
* 'mailwolt-blacklist' etc.
|
||||
*/
|
||||
public ?string $jail = null;
|
||||
|
||||
/**
|
||||
* Struktur für Blade (reine Ausgabe, keine Logik im Blade):
|
||||
* [
|
||||
* [
|
||||
* 'ip' => '1.2.3.4',
|
||||
* 'jail' => 'recidive',
|
||||
* 'permanent' => false,
|
||||
* 'label' => 'Temporär', // oder 'Permanent'
|
||||
* 'box' => 'border-amber-400/20 bg-white/3', // Kartenstil
|
||||
* 'badge' => 'border-amber-400/30 bg-amber-500/10 text-amber-200',
|
||||
* 'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50',
|
||||
* ],
|
||||
* ...
|
||||
* ]
|
||||
*
|
||||
* @var array<int,array{
|
||||
* ip:string,jail:string,permanent:bool,label:string,box:string,badge:string,btn:string
|
||||
* }>
|
||||
*/
|
||||
public array $rows = [];
|
||||
|
||||
#[On('f2b:refresh')]
|
||||
public function refreshList(): void
|
||||
{
|
||||
$this->loadBanned();
|
||||
}
|
||||
|
||||
public function mount(?string $jail = null): void
|
||||
{
|
||||
$this->jail = $jail;
|
||||
$this->loadBanned();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.security.fail2ban-banlist');
|
||||
}
|
||||
|
||||
/* ================= core ================= */
|
||||
|
||||
private function loadBanned(): void
|
||||
{
|
||||
$jails = $this->jailList();
|
||||
|
||||
// ggf. nur ein bestimmtes Jail
|
||||
if (is_string($this->jail) && $this->jail !== '' && $this->jail !== '*') {
|
||||
$jails = in_array($this->jail, $jails, true) ? [$this->jail] : [];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($jails as $j) {
|
||||
$out = $this->f2b("status " . escapeshellarg($j));
|
||||
if (!preg_match('/IP list:\s*(.+)$/mi', $out, $m)) {
|
||||
continue;
|
||||
}
|
||||
$ips = preg_split('/\s+/', trim($m[1])) ?: [];
|
||||
foreach ($ips as $ip) {
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$permanent = $this->isPermanent($j, $ip);
|
||||
|
||||
if ($permanent) {
|
||||
$box = 'border-rose-400/30 bg-rose-500/5';
|
||||
$badge = 'border-rose-400/30 bg-rose-500/10 text-rose-200';
|
||||
$label = 'Permanent';
|
||||
$style = 'permanent';
|
||||
$dot = 'bg-rose-500';
|
||||
} else {
|
||||
$box = 'border-amber-400/20 bg-white/3';
|
||||
$badge = 'border-amber-400/30 bg-amber-500/10 text-amber-200';
|
||||
$label = 'Temporär';
|
||||
$style = 'temporary';
|
||||
$dot = 'bg-amber-400';
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'ip' => $ip,
|
||||
'jail' => $j,
|
||||
'permanent' => $permanent,
|
||||
'style' => $style,
|
||||
'label' => $label,
|
||||
'box' => $box,
|
||||
'badge' => $badge,
|
||||
'dot' => $dot,
|
||||
'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sortierung: permanent oben, dann nach Jail, dann IP
|
||||
usort($rows, function ($a, $b) {
|
||||
if ($a['permanent'] !== $b['permanent']) return $a['permanent'] ? -1 : 1;
|
||||
if ($a['jail'] !== $b['jail']) return strcmp($a['jail'], $b['jail']);
|
||||
return strcmp($a['ip'], $b['ip']);
|
||||
});
|
||||
|
||||
$this->rows = $rows;
|
||||
}
|
||||
|
||||
/** Entbannt eine IP **im angegebenen Jail** (Button gibt Jail mit) */
|
||||
public function unban(string $ip, string $jail): void
|
||||
{
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) return;
|
||||
|
||||
$cmd = sprintf(
|
||||
'sudo -n /usr/bin/fail2ban-client set %s unbanip %s 2>&1',
|
||||
escapeshellarg($jail),
|
||||
escapeshellarg($ip)
|
||||
);
|
||||
@shell_exec($cmd);
|
||||
|
||||
$this->loadBanned();
|
||||
|
||||
$this->dispatch('toast',
|
||||
type: 'done',
|
||||
badge: 'Fail2Ban',
|
||||
title: 'IP entbannt',
|
||||
text: "IP {$ip} in Jail „{$jail}“ entbannt.",
|
||||
duration: 5000,
|
||||
);
|
||||
}
|
||||
|
||||
/* ================= helpers ================= */
|
||||
|
||||
/** Prüft via SQLite, ob der **letzte** Ban für (jail, ip) permanent ist (bantime < 0). */
|
||||
private function isPermanent(string $jail, string $ip): bool
|
||||
{
|
||||
$db = $this->getDbFile();
|
||||
if ($db === '' || !is_readable($db)) {
|
||||
// Fallback: Blacklist-Jail ist per Design permanent
|
||||
return $jail === 'mailwolt-blacklist';
|
||||
}
|
||||
|
||||
$q = <<<SQL
|
||||
WITH last AS (
|
||||
SELECT MAX(timeofban) AS t
|
||||
FROM bans
|
||||
WHERE jail = '$jail' AND ip = '$ip'
|
||||
)
|
||||
SELECT bantime
|
||||
FROM bans, last
|
||||
WHERE jail = '$jail' AND ip = '$ip' AND timeofban = last.t
|
||||
LIMIT 1;
|
||||
SQL;
|
||||
|
||||
$cmd = sprintf(
|
||||
'sudo -n /usr/bin/sqlite3 -readonly %s %s 2>&1',
|
||||
escapeshellarg($db),
|
||||
escapeshellarg($q)
|
||||
);
|
||||
$out = trim((string)@shell_exec($cmd));
|
||||
if ($out === '') return ($jail === 'mailwolt-blacklist'); // Fallback
|
||||
return ((int)$out) < 0;
|
||||
}
|
||||
|
||||
/** Liste aller Jails */
|
||||
private function jailList(): array
|
||||
{
|
||||
$out = $this->f2b('status');
|
||||
if (preg_match('/Jail list:\s*(.+)$/mi', $out, $m)) {
|
||||
$jails = array_map('trim', preg_split('/\s*,\s*/', trim($m[1])));
|
||||
return array_values(array_filter($jails, fn($v) => $v !== ''));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** fail2ban-client über sudo aufrufen */
|
||||
private function f2b(string $args): string
|
||||
{
|
||||
return (string) @shell_exec('sudo -n /usr/bin/fail2ban-client '.$args.' 2>&1');
|
||||
}
|
||||
|
||||
/** Pfad zur Fail2Ban-SQLite-DB holen */
|
||||
private function getDbFile(): string
|
||||
{
|
||||
$out = $this->f2b('get dbfile');
|
||||
$lines = array_values(array_filter(array_map('trim', preg_split('/\r?\n/', $out))));
|
||||
$path = end($lines) ?: '';
|
||||
$path = preg_replace('/^`?-?\s*/', '', $path);
|
||||
return $path ?: '/var/lib/fail2ban/fail2ban.sqlite3';
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ use Livewire\Attributes\On;
|
|||
use Livewire\Component;
|
||||
use App\Models\Fail2banSetting;
|
||||
use App\Models\Fail2banIpList;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class Fail2banSettings extends Component
|
||||
{
|
||||
|
|
@ -31,13 +32,10 @@ class Fail2banSettings extends Component
|
|||
{
|
||||
$this->whitelist = Fail2banIpList::visibleWhitelist()->pluck('ip')->toArray();
|
||||
$this->blacklist = Fail2banIpList::visibleBlacklist()->pluck('ip')->toArray();
|
||||
// $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 Defaults anlegen
|
||||
$this->settings = Fail2banSetting::first() ?? Fail2banSetting::create([
|
||||
'bantime' => 3600,
|
||||
'max_bantime' => 43200,
|
||||
|
|
@ -50,7 +48,6 @@ class Fail2banSettings extends Component
|
|||
'external_mode' => false,
|
||||
]);
|
||||
|
||||
// Properties befüllen
|
||||
$this->fill([
|
||||
'bantime' => (int)$this->settings->bantime,
|
||||
'max_bantime' => (int)$this->settings->max_bantime,
|
||||
|
|
@ -78,6 +75,7 @@ class Fail2banSettings extends Component
|
|||
'cidr_v6' => 'required|integer|min:8|max:128',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Einstellungen speichern
|
||||
$this->settings->update([
|
||||
'bantime' => $this->bantime,
|
||||
|
|
@ -98,8 +96,26 @@ class Fail2banSettings extends Component
|
|||
// Fail2Ban reload
|
||||
$this->runCommand('sudo -n /usr/bin/fail2ban-client reload');
|
||||
|
||||
$this->dispatch('notify', message: 'Gespeichert & Fail2Ban neu geladen.');
|
||||
$this->dispatch('toast',
|
||||
type: 'success',
|
||||
badge: 'Fail2Ban',
|
||||
title: 'Einstellungen gespeichert',
|
||||
text: 'Die Fail2Ban-Konfiguration wurde erfolgreich übernommen und ist jetzt aktiv.',
|
||||
duration: 6000,
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->dispatch('toast',
|
||||
type: 'error',
|
||||
badge: 'Fail2Ban',
|
||||
title: 'Fehler beim Anwenden',
|
||||
text: 'Die neuen Einstellungen konnten nicht angewendet werden: ' . $e->getMessage(),
|
||||
duration: 8000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- Config-Dateien ---------------- */
|
||||
|
||||
protected function writeDefaultsConfig(): void
|
||||
{
|
||||
|
|
@ -120,7 +136,8 @@ CONF;
|
|||
|
||||
protected function writeWhitelistConfig(): void
|
||||
{
|
||||
$ips = Fail2banIpList::where('type', 'whitelist')->pluck('ip')->toArray();
|
||||
// zieht System + User-Whitelist
|
||||
$ips = Fail2banIpList::allWhitelistForConfig();
|
||||
$ignore = implode(' ', array_unique(array_filter($ips)));
|
||||
|
||||
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||
|
|
@ -128,9 +145,8 @@ CONF;
|
|||
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt Root-Dateien sicher via `sudo tee`
|
||||
*/
|
||||
/* ---------------- Helper ---------------- */
|
||||
|
||||
private function writeRootFileViaTee(string $target, string $content): void
|
||||
{
|
||||
if (!preg_match('#^/etc/fail2ban/jail\.d/[A-Za-z0-9._-]+\.local$#', $target)) {
|
||||
|
|
@ -138,32 +154,28 @@ CONF;
|
|||
}
|
||||
|
||||
$cmd = sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target));
|
||||
|
||||
$descriptorspec = [
|
||||
$desc = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
|
||||
$proc = proc_open($cmd, $descriptorspec, $pipes, null, null);
|
||||
$proc = proc_open($cmd, $desc, $pipes);
|
||||
if (!is_resource($proc)) {
|
||||
throw new \RuntimeException('Failed to start tee');
|
||||
throw new \RuntimeException('tee start fehlgeschlagen');
|
||||
}
|
||||
|
||||
fwrite($pipes[0], $content);
|
||||
fclose($pipes[0]);
|
||||
stream_get_contents($pipes[1]);
|
||||
stream_get_contents($pipes[2]);
|
||||
$exitCode = proc_close($proc);
|
||||
$code = proc_close($proc);
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
if ($code !== 0) {
|
||||
throw new \RuntimeException("tee failed writing to {$target}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt Systembefehle aus und wirft Exception bei Fehlern
|
||||
*/
|
||||
private function runCommand(string $cmd): void
|
||||
{
|
||||
$output = [];
|
||||
|
|
@ -186,6 +198,195 @@ CONF;
|
|||
}
|
||||
}
|
||||
|
||||
//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::visibleWhitelist()->pluck('ip')->toArray();
|
||||
// $this->blacklist = Fail2banIpList::visibleBlacklist()->pluck('ip')->toArray();
|
||||
// }
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// // Setting holen oder 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 befüllen
|
||||
// $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->refreshLists();
|
||||
// }
|
||||
//
|
||||
// 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',
|
||||
// ]);
|
||||
//
|
||||
// // Einstellungen speichern
|
||||
// $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,
|
||||
// ]);
|
||||
//
|
||||
// // Config-Dateien schreiben
|
||||
// $this->writeDefaultsConfig();
|
||||
// $this->writeWhitelistConfig();
|
||||
//
|
||||
// // Fail2Ban reload
|
||||
// $this->runCommand('sudo -n /usr/bin/fail2ban-client reload');
|
||||
//
|
||||
// $this->dispatch('toast',
|
||||
// type: 'done',
|
||||
// badge: 'Fail2Ban',
|
||||
// title: 'Einstellungen gespeichert',
|
||||
// text: 'Die Fail2Ban-Konfiguration wurde erfolgreich übernommen und ist jetzt aktiv.',
|
||||
// duration: 6000,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// 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;
|
||||
//
|
||||
// $this->writeRootFileViaTee('/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";
|
||||
//
|
||||
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Schreibt Root-Dateien sicher via `sudo tee`
|
||||
// */
|
||||
// private function writeRootFileViaTee(string $target, string $content): void
|
||||
// {
|
||||
// if (!preg_match('#^/etc/fail2ban/jail\.d/[A-Za-z0-9._-]+\.local$#', $target)) {
|
||||
// throw new \RuntimeException("Illegal path: $target");
|
||||
// }
|
||||
//
|
||||
// $cmd = sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target));
|
||||
//
|
||||
// $descriptorspec = [
|
||||
// 0 => ['pipe', 'r'],
|
||||
// 1 => ['pipe', 'w'],
|
||||
// 2 => ['pipe', 'w'],
|
||||
// ];
|
||||
//
|
||||
// $proc = proc_open($cmd, $descriptorspec, $pipes, null, null);
|
||||
// if (!is_resource($proc)) {
|
||||
// throw new \RuntimeException('Failed to start tee');
|
||||
// }
|
||||
//
|
||||
// fwrite($pipes[0], $content);
|
||||
// fclose($pipes[0]);
|
||||
// stream_get_contents($pipes[1]);
|
||||
// stream_get_contents($pipes[2]);
|
||||
// $exitCode = proc_close($proc);
|
||||
//
|
||||
// if ($exitCode !== 0) {
|
||||
// throw new \RuntimeException("tee failed writing to {$target}");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Führt Systembefehle aus und wirft Exception bei Fehlern
|
||||
// */
|
||||
// private function runCommand(string $cmd): void
|
||||
// {
|
||||
// $output = [];
|
||||
// $return = 0;
|
||||
// exec($cmd . ' 2>&1', $output, $return);
|
||||
//
|
||||
// if ($return !== 0) {
|
||||
// throw new \RuntimeException("Command failed ($return): {$cmd}\n" . implode("\n", $output));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private function boolToStr(bool $v): string
|
||||
// {
|
||||
// return $v ? 'true' : 'false';
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.fail2ban-settings');
|
||||
// }
|
||||
//}
|
||||
|
||||
//
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
|
|
|
|||
|
|
@ -74,50 +74,102 @@ class Fail2BanJailModal extends ModalComponent
|
|||
//
|
||||
// $this->rows = $rows;
|
||||
// logger()->debug('rows='.json_encode($rows, JSON_UNESCAPED_SLASHES));
|
||||
// }
|
||||
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// $jail = $this->jail;
|
||||
//
|
||||
// // 1) Primär: DB
|
||||
// $rows = $this->activeBansFromDb($jail);
|
||||
//
|
||||
// // 2) Fallback: status parsen, wenn DB leer (z. B. sehr alte F2B-Setups)
|
||||
// if (empty($rows)) {
|
||||
// [, $s] = $this->f2b('status '.escapeshellarg($jail));
|
||||
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||
// $defaultBantime = $this->getBantime($jail);
|
||||
//
|
||||
// foreach ($ips as $ip) {
|
||||
// $banAt = $this->lastBanTimestamp($jail, $ip);
|
||||
// $remaining = $banAt !== null ? max(0, $defaultBantime - (time() - $banAt)) : -2;
|
||||
// $until = ($remaining > 0 && $banAt) ? $banAt + $defaultBantime : null;
|
||||
// $rows[] = [
|
||||
// 'ip' => $ip,
|
||||
// 'bantime' => $defaultBantime,
|
||||
// 'banned_at' => $banAt,
|
||||
// 'remaining' => $remaining,
|
||||
// 'until' => $until,
|
||||
// ];
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Präsentationswerte bauen (einheitlich)
|
||||
// $presented = [];
|
||||
// foreach ($rows as $r) {
|
||||
// [$timeText, $metaText, $boxClass] = $this->present($r['remaining'], $r['banned_at'], $r['until']);
|
||||
// $presented[] = $r + [
|
||||
// 'time_text' => $timeText,
|
||||
// 'meta_text' => $metaText,
|
||||
// 'box_class' => $boxClass,
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// $this->rows = $presented;
|
||||
// logger()->debug('rows='.json_encode($presented, JSON_UNESCAPED_SLASHES));
|
||||
// }
|
||||
|
||||
protected function load(bool $force = false): void
|
||||
{
|
||||
$jail = $this->jail;
|
||||
|
||||
// 1) Primär: DB
|
||||
$rows = $this->activeBansFromDb($jail);
|
||||
|
||||
// 2) Fallback: status parsen, wenn DB leer (z. B. sehr alte F2B-Setups)
|
||||
if (empty($rows)) {
|
||||
// 1) IP-Liste IMMER aus "status <jail>" holen (Quelle der Wahrheit)
|
||||
[, $s] = $this->f2b('status '.escapeshellarg($jail));
|
||||
$ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
$ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||
|
||||
$defaultBantime = $this->getBantime($jail);
|
||||
|
||||
$rows = [];
|
||||
foreach ($ips as $ip) {
|
||||
$banAt = $this->lastBanTimestamp($jail, $ip);
|
||||
$remaining = $banAt !== null ? max(0, $defaultBantime - (time() - $banAt)) : -2;
|
||||
$until = ($remaining > 0 && $banAt) ? $banAt + $defaultBantime : null;
|
||||
$banAt = null; $until = null; $remaining = null;
|
||||
|
||||
// 1) Primär: DB
|
||||
if ($info = $this->banInfoFromDb($jail, $ip)) {
|
||||
$banAt = $info['banned_at'];
|
||||
if ((int)$info['expire'] === -1) {
|
||||
$remaining = -1; // permanent
|
||||
} elseif ((int)$info['expire'] > 0) {
|
||||
$until = (int)$info['expire'];
|
||||
$remaining = max(0, $until - time());
|
||||
} else {
|
||||
// kein expire in DB → Fallback unten
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback: KEIN fixes Datum, nur "~ Bantime"
|
||||
if ($remaining === null) {
|
||||
$remaining = -2; // "~ ca. {$defaultBantime}s"
|
||||
}
|
||||
|
||||
[$timeText, $metaText, $boxClass] = $this->present($remaining, $banAt, $until);
|
||||
|
||||
$rows[] = [
|
||||
'ip' => $ip,
|
||||
'bantime' => $defaultBantime,
|
||||
'banned_at' => $banAt,
|
||||
'remaining' => $remaining,
|
||||
'until' => $until,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Präsentationswerte bauen (einheitlich)
|
||||
$presented = [];
|
||||
foreach ($rows as $r) {
|
||||
[$timeText, $metaText, $boxClass] = $this->present($r['remaining'], $r['banned_at'], $r['until']);
|
||||
$presented[] = $r + [
|
||||
'time_text' => $timeText,
|
||||
'meta_text' => $metaText,
|
||||
'box_class' => $boxClass,
|
||||
];
|
||||
}
|
||||
|
||||
$this->rows = $presented;
|
||||
logger()->debug('rows='.json_encode($presented, JSON_UNESCAPED_SLASHES));
|
||||
$this->rows = $rows;
|
||||
logger()->debug('rows='.json_encode($rows, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
/** ROBUST: findet Binaries automatisch */
|
||||
private function bin(string $name): string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class Fail2banIpModal extends ModalComponent
|
|||
throw ValidationException::withMessages(['ip' => 'Loopback/localhost ist bereits systemseitig erlaubt und kann nicht geändert werden.']);
|
||||
}
|
||||
|
||||
// Duplikate abfangen (es gibt einen Unique-Index ip+type; trotzdem user-freundlich)
|
||||
// Duplikate abfangen
|
||||
$exists = Fail2banIpList::where('ip', $ip)->where('type', $this->type)->exists();
|
||||
if ($exists) {
|
||||
throw ValidationException::withMessages(['ip' => ucfirst($this->type) . ' enthält diese IP bereits.']);
|
||||
|
|
@ -75,17 +75,36 @@ class Fail2banIpModal extends ModalComponent
|
|||
Fail2banIpList::create(['ip' => $ip, 'type' => $this->type]);
|
||||
|
||||
if ($this->type === 'whitelist') {
|
||||
$this->writeWhitelistConfig(); // schreibt /etc/fail2ban/jail.d/mailwolt-whitelist.local
|
||||
$this->reloadFail2ban(); // f2b neu laden
|
||||
// Whitelist-Datei aktualisieren + Fail2Ban reload
|
||||
$this->writeWhitelistConfig();
|
||||
$this->reloadFail2ban();
|
||||
|
||||
// UI aktualisieren & Toast
|
||||
$this->dispatch('f2b:refresh');
|
||||
$this->dispatch('toast',
|
||||
type: 'success',
|
||||
badge: 'Fail2Ban',
|
||||
title: 'Whitelist aktualisiert',
|
||||
text: 'Die IP wurde erfolgreich zur Whitelist hinzugefügt und ist nun freigegeben.',
|
||||
duration: 6000,
|
||||
);
|
||||
} else {
|
||||
// Blacklist = sofort bannen im dedizierten Jail
|
||||
// Blacklist = sofort bannen
|
||||
$this->banIp($ip);
|
||||
|
||||
// UI aktualisieren & Toast
|
||||
$this->dispatch('f2b:refresh');
|
||||
$this->dispatch('toast',
|
||||
type: 'warning',
|
||||
badge: 'Fail2Ban',
|
||||
title: 'Blacklist aktualisiert',
|
||||
text: 'Die IP wurde zur Blacklist hinzugefügt und umgehend blockiert.',
|
||||
duration: 6000,
|
||||
);
|
||||
}
|
||||
|
||||
$this->dispatch('f2b:refresh');
|
||||
$this->dispatch('notify', message: ucfirst($this->type) . ' aktualisiert.');
|
||||
// Modal bewusst am Ende schließen (Toast bleibt sichtbar)
|
||||
$this->closeModal();
|
||||
$this->dispatch('f2b:refresh');
|
||||
}
|
||||
|
||||
public function remove(): void
|
||||
|
|
@ -105,14 +124,29 @@ class Fail2banIpModal extends ModalComponent
|
|||
if ($this->type === 'whitelist') {
|
||||
$this->writeWhitelistConfig();
|
||||
$this->reloadFail2ban();
|
||||
} else {
|
||||
$this->unbanIp($ip);
|
||||
}
|
||||
|
||||
$this->dispatch('f2b:refresh');
|
||||
$this->dispatch('notify', message: ucfirst($this->type) . ' Eintrag entfernt.');
|
||||
$this->closeModal();
|
||||
$this->dispatch('toast',
|
||||
type: 'info',
|
||||
badge: 'Fail2Ban',
|
||||
title: 'Whitelist geändert',
|
||||
text: 'Die IP wurde aus der Whitelist entfernt.',
|
||||
duration: 6000,
|
||||
);
|
||||
} else {
|
||||
$this->unbanIp($ip);
|
||||
|
||||
$this->dispatch('f2b:refresh');
|
||||
$this->dispatch('toast',
|
||||
type: 'info',
|
||||
badge: 'Fail2Ban',
|
||||
title: 'Blacklist geändert',
|
||||
text: 'Die IP wurde aus der Blacklist entfernt und ist wieder freigegeben.',
|
||||
duration: 6000,
|
||||
);
|
||||
}
|
||||
|
||||
$this->closeModal();
|
||||
}
|
||||
|
||||
/* ---------------- helper ---------------- */
|
||||
|
|
@ -129,12 +163,11 @@ class Fail2banIpModal extends ModalComponent
|
|||
|
||||
private function writeWhitelistConfig(): void
|
||||
{
|
||||
// WICHTIG: inkl. System-IPs
|
||||
// WICHTIG: inkl. System-IPs (unsichtbar in der UI)
|
||||
$ips = Fail2banIpList::allWhitelistForConfig();
|
||||
$ignore = implode(' ', array_unique(array_filter($ips)));
|
||||
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||
|
||||
// sicher in Root-Pfad schreiben (sudo tee)
|
||||
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
||||
}
|
||||
|
||||
|
|
@ -154,12 +187,14 @@ class Fail2banIpModal extends ModalComponent
|
|||
if (!is_resource($proc)) {
|
||||
throw new \RuntimeException('tee start fehlgeschlagen');
|
||||
}
|
||||
|
||||
fwrite($pipes[0], $content);
|
||||
fclose($pipes[0]);
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[2]);
|
||||
|
||||
$code = proc_close($proc);
|
||||
if ($code !== 0) {
|
||||
throw new \RuntimeException("tee failed (code $code): $stderr $stdout");
|
||||
|
|
@ -183,6 +218,194 @@ class Fail2banIpModal extends ModalComponent
|
|||
@shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1");
|
||||
}
|
||||
}
|
||||
|
||||
//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 (!Fail2banIpList::isValidIpOrCidr($ip)) {
|
||||
// throw ValidationException::withMessages(['ip' => 'Ungültige IP oder CIDR.']);
|
||||
// }
|
||||
//
|
||||
// // Schutz: System-/Loopback-IPs darf der User nicht manuell pflegen
|
||||
// if (Fail2banIpList::isLoopback($ip)) {
|
||||
// throw ValidationException::withMessages(['ip' => 'Loopback/localhost ist bereits systemseitig erlaubt und kann nicht geändert werden.']);
|
||||
// }
|
||||
//
|
||||
// // Duplikate abfangen (es gibt einen Unique-Index ip+type; trotzdem user-freundlich)
|
||||
// $exists = Fail2banIpList::where('ip', $ip)->where('type', $this->type)->exists();
|
||||
// if ($exists) {
|
||||
// throw ValidationException::withMessages(['ip' => ucfirst($this->type) . ' enthält diese IP bereits.']);
|
||||
// }
|
||||
//
|
||||
// // DB schreiben
|
||||
// Fail2banIpList::create(['ip' => $ip, 'type' => $this->type]);
|
||||
//
|
||||
// if ($this->type === 'whitelist') {
|
||||
// $this->writeWhitelistConfig(); // schreibt /etc/fail2ban/jail.d/mailwolt-whitelist.local
|
||||
// $this->reloadFail2ban(); // f2b neu laden
|
||||
// } else {
|
||||
// // Blacklist = sofort bannen im dedizierten Jail
|
||||
// $this->banIp($ip);
|
||||
// }
|
||||
//
|
||||
// $this->closeModal();
|
||||
// $this->dispatch('f2b:refresh');
|
||||
// }
|
||||
//
|
||||
// public function remove(): void
|
||||
// {
|
||||
// $this->assertRemoveMode();
|
||||
// $ip = trim($this->prefill ?? $this->ip);
|
||||
// if ($ip === '') return;
|
||||
//
|
||||
// // System-Whitelist darf nicht entfernt werden
|
||||
// $row = Fail2banIpList::where('type', $this->type)->where('ip', $ip)->first();
|
||||
// if ($row && $row->is_system) {
|
||||
// throw ValidationException::withMessages(['ip' => 'Systemeintrag kann nicht entfernt werden.']);
|
||||
// }
|
||||
//
|
||||
// Fail2banIpList::where('type', $this->type)->where('ip', $ip)->delete();
|
||||
//
|
||||
// if ($this->type === 'whitelist') {
|
||||
// $this->writeWhitelistConfig();
|
||||
// $this->reloadFail2ban();
|
||||
// } else {
|
||||
// $this->unbanIp($ip);
|
||||
// }
|
||||
//
|
||||
// $this->closeModal();
|
||||
// $this->dispatch('f2b:refresh');
|
||||
// $this->dispatch('toast',
|
||||
// type: 'done',
|
||||
// badge: 'Fail2Ban',
|
||||
// title: 'Einstellungen gespeichert',
|
||||
// text: 'Die Fail2Ban-Konfiguration wurde erfolgreich übernommen und ist jetzt aktiv.',
|
||||
// duration: 6000,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// /* ---------------- 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 writeWhitelistConfig(): void
|
||||
// {
|
||||
// // WICHTIG: inkl. System-IPs
|
||||
// $ips = Fail2banIpList::allWhitelistForConfig();
|
||||
// $ignore = implode(' ', array_unique(array_filter($ips)));
|
||||
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||
//
|
||||
// // sicher in Root-Pfad schreiben (sudo tee)
|
||||
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
||||
// }
|
||||
//
|
||||
// private function writeRootFileViaTee(string $target, string $content): void
|
||||
// {
|
||||
// if (!preg_match('#^/etc/fail2ban/jail\.d/[A-Za-z0-9._-]+\.local$#', $target)) {
|
||||
// throw new \RuntimeException("Illegal path: $target");
|
||||
// }
|
||||
//
|
||||
// $cmd = sprintf('sudo -n /usr/bin/tee %s >/dev/null', escapeshellarg($target));
|
||||
// $desc = [
|
||||
// 0 => ['pipe', 'r'],
|
||||
// 1 => ['pipe', 'w'],
|
||||
// 2 => ['pipe', 'w'],
|
||||
// ];
|
||||
// $proc = proc_open($cmd, $desc, $pipes);
|
||||
// if (!is_resource($proc)) {
|
||||
// throw new \RuntimeException('tee start fehlgeschlagen');
|
||||
// }
|
||||
// fwrite($pipes[0], $content);
|
||||
// fclose($pipes[0]);
|
||||
// $stdout = stream_get_contents($pipes[1]);
|
||||
// fclose($pipes[1]);
|
||||
// $stderr = stream_get_contents($pipes[2]);
|
||||
// fclose($pipes[2]);
|
||||
// $code = proc_close($proc);
|
||||
// if ($code !== 0) {
|
||||
// throw new \RuntimeException("tee failed (code $code): $stderr $stdout");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private function reloadFail2ban(): void
|
||||
// {
|
||||
// @shell_exec('sudo -n /usr/bin/fail2ban-client reload 2>&1');
|
||||
// }
|
||||
//
|
||||
// private function banIp(string $ip): void
|
||||
// {
|
||||
// $ipEsc = escapeshellarg($ip);
|
||||
// @shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist banip {$ipEsc} 2>&1");
|
||||
// }
|
||||
//
|
||||
// private function unbanIp(string $ip): void
|
||||
// {
|
||||
// $ipEsc = escapeshellarg($ip);
|
||||
// @shell_exec("sudo -n /usr/bin/fail2ban-client set mailwolt-blacklist unbanip {$ipEsc} 2>&1");
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//
|
||||
//namespace App\Livewire\Ui\Security\Modal;
|
||||
//
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System\Form;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Livewire\Component;
|
||||
|
||||
class DomainsSslForm extends Component
|
||||
{
|
||||
// fix / readonly aus ENV oder config
|
||||
public string $mail_domain_readonly = '';
|
||||
|
||||
// editierbar
|
||||
public string $ui_domain = '';
|
||||
public string $webmail_domain = '';
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'ui_domain' => 'nullable|string|max:190',
|
||||
'webmail_domain' => 'nullable|string|max:190',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->mail_domain_readonly = (string) config('mailwolt.domain.mail', 'mx');
|
||||
$this->ui_domain = Setting::get('ui_domain', $this->ui_domain);
|
||||
$this->webmail_domain = Setting::get('webmail_domain', $this->webmail_domain);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
Setting::put('ui_domain', $this->ui_domain);
|
||||
Setting::put('webmail_domain', $this->webmail_domain);
|
||||
|
||||
$this->dispatch('toast',
|
||||
type: 'done',
|
||||
badge: 'System',
|
||||
title: 'Domains gespeichert',
|
||||
text: 'UI- und Webmail-Domain wurden übernommen.',
|
||||
duration: 5000,
|
||||
);
|
||||
}
|
||||
|
||||
public function render() { return view('livewire.ui.system.form.domains-ssl-form'); }
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System\Form;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Livewire\Component;
|
||||
|
||||
class GeneralForm extends Component
|
||||
{
|
||||
public string $locale = 'de';
|
||||
public string $timezone = 'Europe/Berlin';
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'locale' => 'required|string|max:10',
|
||||
'timezone' => 'required|string|max:64',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
// Defaults aus ENV nur für den allerersten Seed in Settings (Redis/DB)
|
||||
$envLocale = env('APP_LOCALE') ?? env('APP_FALLBACK_LOCALE') ?? $this->locale;
|
||||
$envTimezone = env('APP_TIMEZONE') ?? $this->timezone;
|
||||
|
||||
// Wenn (noch) nichts in Settings liegt, einmalig mit ENV-Werten befüllen
|
||||
if (Setting::get('locale', null) === null) {
|
||||
Setting::set('locale', $envLocale);
|
||||
}
|
||||
if (Setting::get('timezone', null) === null) {
|
||||
Setting::set('timezone', $envTimezone);
|
||||
}
|
||||
|
||||
// Ab hier ausschließlich aus Settings lesen (Redis → DB Fallback)
|
||||
$this->locale = (string) Setting::get('locale', $envLocale);
|
||||
$this->timezone = (string) Setting::get('timezone', $envTimezone);
|
||||
|
||||
// Sofort für die aktuelle Request anwenden
|
||||
app()->setLocale($this->locale);
|
||||
@date_default_timezone_set($this->timezone);
|
||||
config([
|
||||
'app.locale' => $this->locale,
|
||||
'app.fallback_locale' => $this->locale,
|
||||
'app.timezone' => $this->timezone,
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
// Persistieren: DB → Redis (siehe Setting::set)
|
||||
Setting::set('locale', $this->locale);
|
||||
Setting::set('timezone', $this->timezone);
|
||||
|
||||
// Direkt in der laufenden Request aktivieren
|
||||
app()->setLocale($this->locale);
|
||||
@date_default_timezone_set($this->timezone);
|
||||
config([
|
||||
'app.locale' => $this->locale,
|
||||
'app.fallback_locale' => $this->locale, // optional
|
||||
'app.timezone' => $this->timezone,
|
||||
]);
|
||||
|
||||
$this->dispatch('toast',
|
||||
type: 'done',
|
||||
badge: 'System',
|
||||
title: 'Allgemein gespeichert',
|
||||
text: 'Sprache und Zeitzone wurden übernommen.',
|
||||
duration: 5000,
|
||||
);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.form.general-form');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System\Form;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Livewire\Component;
|
||||
|
||||
class SecurityForm extends Component
|
||||
{
|
||||
public bool $twofa_enabled = false;
|
||||
public ?int $rate_limit = 5;
|
||||
public ?int $password_min = 10;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'twofa_enabled' => 'boolean',
|
||||
'rate_limit' => 'nullable|integer|min:1|max:100',
|
||||
'password_min' => 'nullable|integer|min:6|max:128',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->twofa_enabled = (bool) Setting::get('twofa_enabled', $this->twofa_enabled);
|
||||
$this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit);
|
||||
$this->password_min = (int) Setting::get('password_min', $this->password_min);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
Setting::put('twofa_enabled', $this->twofa_enabled);
|
||||
Setting::put('rate_limit', $this->rate_limit);
|
||||
Setting::put('password_min', $this->password_min);
|
||||
|
||||
$this->dispatch('toast',
|
||||
type: 'done',
|
||||
badge: 'Sicherheit',
|
||||
title: 'Sicherheit gespeichert',
|
||||
text: '2FA/Rate-Limits/Passwortregeln wurden übernommen.',
|
||||
duration: 5000,
|
||||
);
|
||||
}
|
||||
|
||||
public function render() { return view('livewire.ui.system.form.security-form'); }
|
||||
}
|
||||
|
|
@ -1,6 +1,30 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'domain' => [
|
||||
'base' => env('BASE_DOMAIN'),
|
||||
'mail' => env('MTA_SUB'),
|
||||
'ui' => env('UI_SUB'),
|
||||
'webmail' => env('WEBMAIL_SUB'),
|
||||
],
|
||||
|
||||
'language' => [
|
||||
|
||||
'de' => [
|
||||
'label' => 'Deutsch',
|
||||
'locale' => 'de',
|
||||
'fallback_locale' => 'de',
|
||||
'flag' => 'de',
|
||||
],
|
||||
|
||||
'en' => [
|
||||
'label' => 'English',
|
||||
'locale' => 'en',
|
||||
'fallback_locale' => 'en',
|
||||
'flag' => 'gb',
|
||||
],
|
||||
],
|
||||
|
||||
'units' => [
|
||||
['name' => 'nginx', 'action' => 'reload'],
|
||||
['name' => 'postfix', 'action' => 'try-reload-or-restart'],
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ return [
|
|||
'label' => 'Mail', 'icon' => 'ph-envelope', 'items' => [
|
||||
['label' => 'Postfächer', 'route' => 'ui.mail.mailboxes.index'],
|
||||
['label' => 'Aliasse', 'route' => 'ui.mail.aliases.index'],
|
||||
['label' => 'Gruppen', 'route' => 'ui.mail.groups.index'],
|
||||
['label' => 'Filter', 'route' => 'ui.mail.filters.index'],
|
||||
// ['label' => 'Gruppen', 'route' => 'ui.mail.groups.index'],
|
||||
// ['label' => 'Filter', 'route' => 'ui.mail.filters.index'],
|
||||
['label' => 'Quarantäne', 'route' => 'ui.mail.quarantine.index'],
|
||||
['label' => 'Queues', 'route' => 'ui.mail.queues.index'],
|
||||
],
|
||||
|
|
@ -19,45 +19,59 @@ return [
|
|||
[
|
||||
'label' => 'Domains', 'icon' => 'ph-globe', 'items' => [
|
||||
['label' => 'Übersicht', 'route' => 'ui.domains.index'],
|
||||
['label' => 'DNS-Assistent', 'route' => 'ui.domains.dns'],
|
||||
['label' => 'Zertifikate', 'route' => 'ui.domains.certificates'],
|
||||
// ['label' => 'DNS-Assistent', 'route' => 'ui.domains.dns'],
|
||||
// ['label' => 'Zertifikate', 'route' => 'ui.domains.certificates'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => 'Webmail', 'icon' => 'ph-browser', 'items' => [
|
||||
['label' => 'Allgemein', 'route' => 'ui.webmail.settings'],
|
||||
['label' => 'Plugins', 'route' => 'ui.webmail.plugins'],
|
||||
['label' => 'Allgemein', 'route' => 'ui.logout'],
|
||||
['label' => 'Plugins', 'route' => 'ui.logout'],
|
||||
// ['label' => 'Allgemein', 'route' => 'ui.webmail.settings'],
|
||||
// ['label' => 'Plugins', 'route' => 'ui.webmail.plugins'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => 'Benutzer', 'icon' => 'ph-users', 'items' => [
|
||||
['label' => 'Benutzer', 'route' => 'ui.users.index'],
|
||||
['label' => 'Rollen & Rechte', 'route' => 'ui.users.roles'],
|
||||
['label' => 'Anmeldesicherheit', 'route' => 'ui.users.security'],
|
||||
['label' => 'Benutzer', 'route' => 'ui.logout'],
|
||||
['label' => 'Rollen & Rechte', 'route' => 'ui.logout'],
|
||||
['label' => 'Anmeldesicherheit', 'route' => 'ui.logout'],
|
||||
// ['label' => 'Benutzer', 'route' => 'ui.users.index'],
|
||||
// ['label' => 'Rollen & Rechte', 'route' => 'ui.users.roles'],
|
||||
// ['label' => 'Anmeldesicherheit', 'route' => 'ui.users.security'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => 'Sicherheit', 'icon' => 'ph-shield', 'items' => [
|
||||
['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
|
||||
['label' => 'Ratelimits', 'route' => 'ui.security.abuse'],
|
||||
['label' => 'Fail2Ban', 'route' => 'ui.security.fail2ban'],
|
||||
['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
|
||||
['label' => 'Rspamd', 'route' => 'ui.security.rspamd'],
|
||||
['label' => 'SSL', 'route' => 'ui.security.ssl'],
|
||||
// ['label' => 'Ratelimits', 'route' => 'ui.security.audit'],
|
||||
// ['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
|
||||
// ['label' => 'Ratelimits', 'route' => 'ui.security.abuse'],
|
||||
// ['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => 'System', 'icon' => 'ph-gear-six', 'items' => [
|
||||
['label' => 'Einstellungen', 'route' => 'ui.system.settings'],
|
||||
['label' => 'Dienste & Status', 'route' => 'ui.system.services'],
|
||||
['label' => 'Jobs & Queues', 'route' => 'ui.system.jobs'],
|
||||
['label' => 'Logs', 'route' => 'ui.system.logs'],
|
||||
['label' => 'Speicher', 'route' => 'ui.system.storage'],
|
||||
['label' => 'Über', 'route' => 'ui.system.about'],
|
||||
// ['label' => 'Dienste & Status', 'route' => 'ui.system.services'],
|
||||
// ['label' => 'Jobs & Queues', 'route' => 'ui.system.jobs'],
|
||||
// ['label' => 'Logs', 'route' => 'ui.system.logs'],
|
||||
// ['label' => 'Speicher', 'route' => 'ui.system.storage'],
|
||||
// ['label' => 'Über', 'route' => 'ui.system.about'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'label' => 'Entwickler', 'icon' => 'ph-brackets-curly', 'items' => [
|
||||
['label' => 'API-Schlüssel', 'route' => 'ui.dev.tokens'],
|
||||
['label' => 'Webhooks', 'route' => 'ui.dev.webhooks'],
|
||||
['label' => 'Sandbox', 'route' => 'ui.dev.sandbox'],
|
||||
['label' => 'API-Schlüssel', 'route' => 'ui.logout'],
|
||||
['label' => 'Webhooks', 'route' => 'ui.logout'],
|
||||
['label' => 'Sandbox', 'route' => 'ui.logout'],
|
||||
// ['label' => 'API-Schlüssel', 'route' => 'ui.dev.tokens'],
|
||||
// ['label' => 'Webhooks', 'route' => 'ui.dev.webhooks'],
|
||||
// ['label' => 'Sandbox', 'route' => 'ui.dev.sandbox'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -62,8 +62,8 @@
|
|||
$itemIcon = $item['icon'] ?? 'ph-dot-outline';
|
||||
@endphp
|
||||
<li>
|
||||
{{-- <a href="{{ isset($item['route']) ? route($item['route']) : '#' }}"--}}
|
||||
<a href="#"
|
||||
<a href="{{ isset($item['route']) ? route($item['route']) : '#' }}"
|
||||
{{-- <a href="#"--}}
|
||||
class="sidebar-link group relative flex items-center gap-2 px-2 py-2 rounded-lg
|
||||
border border-transparent transition-colors
|
||||
hover:bg-gradient-to-t hover:from-white/10 hover:to-transparent
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@
|
|||
|
||||
{{-- Row 1: Domain + Typ --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
|
||||
{{-- DOMAIN (TailwindPlus Elements) --}}
|
||||
<div class="relative" wire:ignore id="domain-select-{{ $this->getId() }}">
|
||||
<label class="block text-xs text-white/60 mb-1">Domain</label>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-white/90">Aktuell gebannte IPs</h3>
|
||||
<button wire:click="refreshList"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-xs text-white/80 hover:text-white hover:border-white/20">
|
||||
<i class="ph ph-arrows-counter-clockwise text-[14px]"></i>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (empty($rows))
|
||||
<div class="text-white/50 text-sm">Keine aktiven Banns vorhanden.</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach ($rows as $r)
|
||||
<div class="flex items-center justify-between rounded-2xl border px-4 py-2.5 {{ $r['box'] }}">
|
||||
<div class="flex items-center gap-3">
|
||||
{{-- Statuspunkt: rot=permanent, gelb=temporär --}}
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full {{ $r['dot'] }}"></span>
|
||||
|
||||
{{-- IP klein + monospace, ohne Jail-Text --}}
|
||||
<span class="font-mono text-[13px] md:text-[14px] text-white/85 tracking-normal">
|
||||
{{ $r['ip'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
wire:click="unban('{{ $r['ip'] }}','{{ $r['jail'] }}')"
|
||||
class="text-[12px] px-3 py-1.5 rounded-xl border {{ $r['btn'] }}">
|
||||
Entbannen
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<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">
|
||||
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-xs text-white/80 hover:text-white hover:border-white/20">
|
||||
<i class="ph ph-floppy-disk text-[14px]"></i> Speichern & Reload
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -45,7 +45,8 @@
|
|||
<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">
|
||||
<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>
|
||||
|
|
@ -61,6 +62,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card p-5">
|
||||
<livewire:ui.security.fail2ban-banlist/>
|
||||
</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>
|
||||
|
|
@ -75,6 +81,7 @@
|
|||
<li>Alle Änderungen hier werden nach Klick auf <em>„Speichern & Reload“</em> sofort aktiv.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- RIGHT 1/3 --}}
|
||||
|
|
@ -86,7 +93,8 @@
|
|||
</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">
|
||||
<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 }}'}})">
|
||||
|
|
@ -104,13 +112,15 @@
|
|||
</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">
|
||||
<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">
|
||||
<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 }}'}})">
|
||||
|
|
@ -121,7 +131,8 @@
|
|||
<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"
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"
|
||||
wire:poll.2s="refresh">
|
||||
@if($running) wire:poll.2s="refresh" @endif>
|
||||
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-white/60 text-sm mb-1">Mailserver-Domain (fix)</label>
|
||||
<input type="text" value="{{ $mail_domain_readonly }}" disabled
|
||||
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.06] px-3 text-white/60 cursor-not-allowed">
|
||||
<p class="mt-1 text-xs text-white/45">Wird aus ENV/Config gelesen und ist nicht änderbar.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/60 text-sm mb-1">UI-Domain</label>
|
||||
<input type="text" wire:model.defer="ui_domain" placeholder="z. B. ui.deinedomain.tld"
|
||||
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||
@error('ui_domain') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/60 text-sm mb-1">Webmail-Domain</label>
|
||||
<input type="text" wire:model.defer="webmail_domain" placeholder="z. B. mail.deinedomain.tld"
|
||||
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||
@error('webmail_domain') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<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">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-white/45">
|
||||
TLS/Redirect ist systemweit immer erzwungen (HTTPS). ACME/Zertifikate haben ihren eigenen Reiter.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{{-- Sprache --}}
|
||||
<div>
|
||||
<label class="block text-white/60 text-sm mb-1">Sprache</label>
|
||||
<select wire:model.defer="locale"
|
||||
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||
@foreach (config('mailwolt.language') as $key => $lang)
|
||||
<option value="{{ $lang['locale'] }}">{{ $lang['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('locale') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Zeitzone --}}
|
||||
<div>
|
||||
<label class="block text-white/60 text-sm mb-1">Zeitzone</label>
|
||||
<select wire:model.defer="timezone"
|
||||
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||
@foreach (DateTimeZone::listIdentifiers() as $tz)
|
||||
<option value="{{ $tz }}">{{ $tz }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('timezone') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Actions: immer unten rechts, volle Breite, rechts ausgerichtet --}}
|
||||
<div class="md:col-span-2 flex justify-end">
|
||||
<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">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<div class="space-y-4">
|
||||
<label class="flex items-center gap-3">
|
||||
<input type="checkbox" wire:model.defer="twofa_enabled" class="h-4 w-4">
|
||||
<span class="text-white/80">Zwei-Faktor-Authentifizierung aktivieren</span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/60 text-sm mb-1">Login-Rate-Limit (Versuche/Minute)</label>
|
||||
<input type="number" min="1" max="100" wire:model.defer="rate_limit"
|
||||
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||
@error('rate_limit') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/60 text-sm mb-1">Minimale Passwortlänge</label>
|
||||
<input type="number" min="6" max="128" wire:model.defer="password_min"
|
||||
class="w-full h-11 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-white/90">
|
||||
@error('password_min') <p class="text-xs text-rose-400 mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<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">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,14 +1,3 @@
|
|||
{{-- resources/views/ui/system/settings.blade.php --}}
|
||||
{{--@extends('layouts.app')--}}
|
||||
|
||||
{{--@section('title', 'System · Einstellungen')--}}
|
||||
{{--@section('header_title', 'System · Einstellungen')--}}
|
||||
|
||||
{{--@section('content')--}}
|
||||
{{-- <div class="glass-card p-5">--}}
|
||||
{{-- <livewire:ui.system.settings-form />--}}
|
||||
{{-- </div>--}}
|
||||
{{--@endsection--}}
|
||||
{{-- resources/views/ui/system/settings/index.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
|
||||
|
|
@ -55,7 +44,7 @@
|
|||
</div>
|
||||
|
||||
{{-- Livewire-Form (Allgemein) --}}
|
||||
<livewire:ui.system.general-form />
|
||||
<livewire:ui.system.form.general-form />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -37,14 +37,16 @@ Route::middleware('auth.user')->name('ui.')->group(function () {
|
|||
});
|
||||
|
||||
#DOMAIN ROUTES
|
||||
Route::name('domain.')->group(function () {
|
||||
Route::name('domains.')->group(function () {
|
||||
Route::get('/domains', [DomainDnsController::class, 'index'])->name('index');
|
||||
});
|
||||
|
||||
#MAIL ROUTES
|
||||
Route::name('mail.')->group(function () {
|
||||
Route::get('/mailboxes', [MailboxController::class, 'index'])->name('mailbox.index');
|
||||
Route::get('/mailboxes', [MailboxController::class, 'index'])->name('mailboxes.index');
|
||||
Route::get('/aliases', [AliasController::class, 'index'])->name('aliases.index');
|
||||
Route::get('/quarantine', function () {return 'Quarantäne';})->name('quarantine.index');
|
||||
Route::get('/queues', function () {return 'Queues';})->name('queues.index');
|
||||
});
|
||||
|
||||
#LOGOUT ROUTE
|
||||
|
|
|
|||
Loading…
Reference in New Issue