Compare commits
89 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
af369acbf6 | |
|
|
d81c3bc07c | |
|
|
821a2bde33 | |
|
|
8e68051fde | |
|
|
afb8d09db3 | |
|
|
fc04ef44d0 | |
|
|
a7d84899fb | |
|
|
e3dc81ef73 | |
|
|
9acea7b89b | |
|
|
6c3cde5f65 | |
|
|
77f22518c8 | |
|
|
9aa9475387 | |
|
|
d4255b08fa | |
|
|
94aec78d4c | |
|
|
d3783e1717 | |
|
|
dcf9a8d3e9 | |
|
|
595828c5f6 | |
|
|
834f173bb9 | |
|
|
a3a4ec4d06 | |
|
|
2f390af9ed | |
|
|
530faf6b45 | |
|
|
8058f9b814 | |
|
|
c4b906223c | |
|
|
81860d1851 | |
|
|
792f0e3528 | |
|
|
46591669d6 | |
|
|
8690067d9c | |
|
|
beb2f863a3 | |
|
|
e833033074 | |
|
|
02e558bf4b | |
|
|
d9867db546 | |
|
|
e77d9f64bb | |
|
|
ee44ff3def | |
|
|
4d1fd64158 | |
|
|
6b0dd7d176 | |
|
|
67b6e1fa02 | |
|
|
8b4f2d9fe8 | |
|
|
e3c7e8de33 | |
|
|
385b67c3c5 | |
|
|
251f2d9c8f | |
|
|
aaae226c8d | |
|
|
3c1093311c | |
|
|
0cb7212d4b | |
|
|
ab13bab984 | |
|
|
47bca4c8de | |
|
|
9074904683 | |
|
|
659f3cb7ae | |
|
|
c8cae445c5 | |
|
|
2ef27b5e8f | |
|
|
d200e3e73f | |
|
|
e23713a5c6 | |
|
|
56e7453f8d | |
|
|
8790cffeb4 | |
|
|
2ed1d1cd36 | |
|
|
10b4872a04 | |
|
|
4645b168f7 | |
|
|
3152dc94e2 | |
|
|
59c495af84 | |
|
|
a5d3ac08c6 | |
|
|
4197b61905 | |
|
|
dd645aed68 | |
|
|
703843a9c2 | |
|
|
3108d521a5 | |
|
|
dd3f413e6a | |
|
|
09117fe1e9 | |
|
|
ddd96eb9f2 | |
|
|
da30b80056 | |
|
|
074d2da4ec | |
|
|
d76ea0b703 | |
|
|
adea3c5275 | |
|
|
9d3ca94b87 | |
|
|
520617d9b3 | |
|
|
d6f0c5d7cb | |
|
|
3b816e2198 | |
|
|
8e35c617b8 | |
|
|
d65aaf9a5d | |
|
|
a5e745ca4a | |
|
|
cdb16fc4a5 | |
|
|
988de01e82 | |
|
|
3bf7db585a | |
|
|
93c87b8d89 | |
|
|
18447dbf21 | |
|
|
c11d330c38 | |
|
|
a943b42fec | |
|
|
e997d5374d | |
|
|
ecbe123088 | |
|
|
7a636fb496 | |
|
|
f99790b1a5 | |
|
|
1bf41063ae |
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Setting;
|
||||
|
||||
class ProbeRbl extends Command
|
||||
{
|
||||
protected $signature = 'rbl:probe {--force : Ignoriert Intervalle und prüft sofort}';
|
||||
protected $description = 'Prüft öffentliche RBLs und speichert das Ergebnis in settings:health.rbl';
|
||||
|
||||
// Intervalle
|
||||
private int $minIntervalDays = 7; // frühestens alle 7 Tage neu prüfen
|
||||
private int $ttlDays = 14; // Ergebnis 14 Tage gültig
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$now = now();
|
||||
|
||||
$existing = (array) Setting::get('health.rbl', []) ?: [];
|
||||
$lastAt = isset($existing['checked_at']) ? \Illuminate\Support\Carbon::parse($existing['checked_at']) : null;
|
||||
$nextDue = $lastAt ? $lastAt->copy()->addDays($this->minIntervalDays) : null;
|
||||
|
||||
if (!$this->option('force') && $nextDue && $now->lt($nextDue)) {
|
||||
$this->info("Übersprungen: nächste Prüfung erst ab {$nextDue->toIso8601String()} (force mit --force).");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// IPs ermitteln (Installer-ENV bevorzugt)
|
||||
[$ipv4, $ipv6] = $this->resolvePublicIpsFromInstallerEnv();
|
||||
$ipv4 = $ipv4 ?: trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: null;
|
||||
$ipv6 = $ipv6 ?: trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: null;
|
||||
|
||||
// Kandidat für RBL (nur IPv4)
|
||||
$ip = $this->validIPv4($ipv4) ? $ipv4 : null;
|
||||
if (!$ip) {
|
||||
$file = trim((string) @file_get_contents('/etc/mailwolt/public_ip'));
|
||||
if ($this->validIPv4($file)) $ip = $file;
|
||||
}
|
||||
if (!$ip) {
|
||||
$curl = trim((string) @shell_exec('curl -fsS --max-time 2 ifconfig.me 2>/dev/null'));
|
||||
if ($this->validIPv4($curl)) $ip = $curl;
|
||||
}
|
||||
if (!$ip) $ip = '0.0.0.0';
|
||||
|
||||
// Abfragen (DNS)
|
||||
[$lists, $meta] = $this->queryRblLists($ip);
|
||||
|
||||
$payload = [
|
||||
'ip' => $ip,
|
||||
'ipv4' => $ipv4,
|
||||
'ipv6' => $ipv6,
|
||||
'hits' => count($lists),
|
||||
'lists' => array_values($lists), // nur die tatsächlich gelisteten Zonen
|
||||
'meta' => $meta, // {zone:{status, txt?}}
|
||||
'checked_at' => $now->toIso8601String(),
|
||||
'valid_until' => $now->copy()->addDays($this->ttlDays)->toIso8601String(),
|
||||
'min_next' => $now->copy()->addDays($this->minIntervalDays)->toIso8601String(),
|
||||
];
|
||||
|
||||
// Persistieren (DB) + in Redis spiegeln
|
||||
Setting::set('health.rbl', $payload);
|
||||
Cache::put('health.rbl', $payload, now()->addDays($this->ttlDays));
|
||||
|
||||
$this->info(sprintf(
|
||||
'RBL: ip=%s hits=%d lists=[%s]',
|
||||
$payload['ip'], $payload['hits'], implode(',', $payload['lists'])
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/* ---------- Helpers ---------- */
|
||||
|
||||
private function resolvePublicIpsFromInstallerEnv(): array
|
||||
{
|
||||
$file = '/etc/mailwolt/installer.env';
|
||||
if (!is_readable($file)) return [null, null];
|
||||
|
||||
$ipv4 = $ipv6 = null;
|
||||
foreach (@file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
|
||||
if ($line === '' || $line[0] === '#') continue;
|
||||
if (!str_contains($line, '=')) continue;
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$v = trim($v, " \t\n\r\0\x0B\"'");
|
||||
if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) $ipv4 = $v;
|
||||
if ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) $ipv6 = $v;
|
||||
}
|
||||
return [ $ipv4, $ipv6 ];
|
||||
}
|
||||
|
||||
private function validIPv4(?string $ip): bool
|
||||
{
|
||||
return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
}
|
||||
private function validIPv6(?string $ip): bool
|
||||
{
|
||||
return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt [listedZones, meta] zurück.
|
||||
* meta[zone] = ['status'=>'listed|clean|blocked|nx', 'txt'=>?string]
|
||||
*/
|
||||
private function queryRblLists(string $ip): array
|
||||
{
|
||||
if (!$this->validIPv4($ip)) return [[], []];
|
||||
|
||||
$rev = implode('.', array_reverse(explode('.', $ip)));
|
||||
|
||||
// Kuratierte, erreichbare Zonen (ohne kaputte Subdomains)
|
||||
$zones = [
|
||||
// Spamhaus ZEN – rate-limitiert, liefert „blocked“ bei Open Resolver
|
||||
'zen.spamhaus.org',
|
||||
// PSBL
|
||||
'psbl.surriel.com',
|
||||
// UCEPROTECT Level 1
|
||||
'dnsbl-1.uceprotect.net',
|
||||
// s5h
|
||||
'bl.s5h.net',
|
||||
];
|
||||
|
||||
$listed = [];
|
||||
$meta = [];
|
||||
|
||||
foreach ($zones as $zone) {
|
||||
$q = "{$rev}.{$zone}.";
|
||||
|
||||
$txt = @dns_get_record($q, DNS_TXT) ?: [];
|
||||
$a = @dns_get_record($q, DNS_A) ?: [];
|
||||
|
||||
// Spamhaus „blocked“ Heuristik
|
||||
$blocked = false;
|
||||
if ($zone === 'zen.spamhaus.org') {
|
||||
foreach ($a as $rec) {
|
||||
if (!empty($rec['ip']) && in_array($rec['ip'], ['127.255.255.254','127.255.255.255'], true)) {
|
||||
$blocked = true; break;
|
||||
}
|
||||
}
|
||||
if (!$blocked) {
|
||||
foreach ($txt as $rec) {
|
||||
$t = implode('', $rec['txt'] ?? []);
|
||||
if (stripos($t, 'open resolver') !== false) { $blocked = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($blocked) {
|
||||
$meta[$zone] = ['status' => 'blocked', 'txt' => 'Spamhaus blockt – nutze privaten Resolver'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasA = !empty($a);
|
||||
$hasTXT = !empty($txt);
|
||||
if ($hasA || $hasTXT) {
|
||||
$listed[] = $zone;
|
||||
$meta[$zone] = ['status' => 'listed', 'txt' => $hasTXT ? ($txt[0]['txt'][0] ?? null) : null];
|
||||
} else {
|
||||
// NXDOMAIN / sauber
|
||||
$meta[$zone] = ['status' => 'clean', 'txt' => null];
|
||||
}
|
||||
}
|
||||
|
||||
return [$listed, $meta];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SpamAvCollectCommand extends Command
|
||||
{
|
||||
protected $signature = 'spamav:collect';
|
||||
protected $description = 'Collect Rspamd/ClamAV metrics and persist to Settings (DB→Redis)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Collecting Spam/AV metrics…');
|
||||
|
||||
$rpC = '/usr/bin/rspamc';
|
||||
$rpA = '/usr/bin/rspamadm';
|
||||
|
||||
// --- RSPAMD: erst 'stat' (sicher), sonst 'counters' als Fallback ---
|
||||
$ham = $spam = $reject = 0;
|
||||
|
||||
$stat = trim(@shell_exec("$rpC -h 127.0.0.1:11334 stat 2>/dev/null") ?? '');
|
||||
if ($stat !== '') {
|
||||
$ham = preg_match('/Messages treated as ham:\s*(\d+)/i', $stat, $m) ? (int)$m[1] : 0;
|
||||
$spam = preg_match('/Messages treated as spam:\s*(\d+)/i', $stat, $m) ? (int)$m[1] : 0;
|
||||
$reject = preg_match('/Messages with action reject:\s*(\d+)/i', $stat, $m) ? (int)$m[1] : 0;
|
||||
} else {
|
||||
$cnt = trim(@shell_exec("$rpC -h 127.0.0.1:11334 counters 2>/dev/null") ?? '');
|
||||
$ham = preg_match('/\bham\s*:\s*(\d+)/i', $cnt, $m) ? (int)$m[1] : 0;
|
||||
$spam = preg_match('/\bspam\s*:\s*(\d+)/i', $cnt, $m) ? (int)$m[1] : 0;
|
||||
$reject = preg_match('/\breject\s*:\s*(\d+)/i', $cnt, $m) ? (int)$m[1] : 0;
|
||||
}
|
||||
|
||||
// Rspamd Version
|
||||
$rspamdVer = trim(@shell_exec("$rpA --version 2>/dev/null") ?? '');
|
||||
if ($rspamdVer === '') {
|
||||
$rspamdVer = trim(@shell_exec("dpkg-query -W -f='\${Version}\n' rspamd 2>/dev/null") ?? '–');
|
||||
}
|
||||
|
||||
// $clamLine = trim(@shell_exec('clamd --version 2>/dev/null || clamscan --version 2>/dev/null') ?? '') ?: '–';
|
||||
// $clamVer = $clamLine;
|
||||
//
|
||||
// // Versuch, Datum aus der Versionzeile zu ziehen (kein Zugriff auf freshclam.log nötig)
|
||||
// $sigUpdated = null;
|
||||
// if (preg_match('#/([^/]+ [0-9]{2} [0-9:]{8} [0-9]{4})$#', $clamLine, $m)) {
|
||||
// $sigUpdated = $m[1]; // z.B. "Sun Oct 26 09:42:43 2025"
|
||||
// }
|
||||
|
||||
// --- CLAMAV: Versionzeile inkl. Signaturdatum ausgeben ---
|
||||
$clamLine = trim(@shell_exec('clamd --version 2>/dev/null || clamscan --version 2>/dev/null') ?? '') ?: '–';
|
||||
|
||||
$parts = explode('/', $clamLine, 3);
|
||||
$clamVer = $parts[0] ?? 'ClamAV';
|
||||
$sigUpdated = null;
|
||||
if (isset($parts[2])) {
|
||||
$sigUpdated = trim($parts[2]);
|
||||
} else {
|
||||
// Fallback: journalctl (falls Gruppe adm)
|
||||
$jl = trim(@shell_exec('journalctl -u freshclam -n 50 --no-pager 2>/dev/null | grep -i "Database updated" | tail -n1') ?? '');
|
||||
if ($jl) $sigUpdated = $jl;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'ts' => time(),
|
||||
'ham' => $ham,
|
||||
'spam' => $spam,
|
||||
'reject' => $reject,
|
||||
'rspamdVer' => $rspamdVer ?: '–',
|
||||
'clamVer' => $clamVer,
|
||||
'sigUpdated' => $sigUpdated,
|
||||
];
|
||||
|
||||
Setting::set('spamav.metrics', $data);
|
||||
Cache::put('dash.spamav', $data, now()->addMinutes(10));
|
||||
|
||||
$this->info(sprintf(
|
||||
'ham=%d spam=%d reject=%d | rspamd=%s | clam=%s',
|
||||
$ham, $spam, $reject, $data['rspamdVer'], $clamVer
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
//
|
||||
//namespace App\Console\Commands;
|
||||
//
|
||||
//use Illuminate\Console\Command;
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//use App\Models\Setting;
|
||||
//
|
||||
//class SpamAvCollectCommand extends Command
|
||||
//{
|
||||
// protected $signature = 'spamav:collect';
|
||||
// protected $description = 'Collect Rspamd/ClamAV metrics and persist to Settings (DB→Redis)';
|
||||
//
|
||||
// public function handle(): int
|
||||
// {
|
||||
// $this->info('Collecting Spam/AV metrics…');
|
||||
//
|
||||
// // Rspamd counters (kein Root nötig)
|
||||
// $out = trim(@shell_exec('rspamc counters 2>/dev/null') ?? '');
|
||||
// $ham = preg_match('/\bham\s*:\s*(\d+)/i', $out, $m1) ? (int)$m1[1] : 0;
|
||||
// $spam = preg_match('/\bspam\s*:\s*(\d+)/i', $out, $m2) ? (int)$m2[1] : 0;
|
||||
// $reject = preg_match('/\breject\s*:\s*(\d+)/i', $out, $m3) ? (int)$m3[1] : 0;
|
||||
//
|
||||
// // ClamAV Version + Signatur-Datum robust ohne Datei-Zugriff
|
||||
// $clamLine = trim((string) @shell_exec('clamd --version 2>/dev/null || clamscan --version 2>/dev/null'));
|
||||
// $clamVer = $clamLine !== '' ? $clamLine : '–';
|
||||
//
|
||||
// // Aus clamd/clamscan-Output das Datum am Ende herausziehen (Format: ".../Sun Oct 26 09:42:43 2025")
|
||||
// $sigUpdated = null;
|
||||
// if ($clamLine && preg_match('#/([^/]+\d{4})$#', $clamLine, $m)) {
|
||||
// // $m[1] ist z.B. "Sun Oct 26 09:42:43 2025"
|
||||
// $sigUpdated = $m[1];
|
||||
// }
|
||||
//
|
||||
// $data = [
|
||||
// 'ts' => time(),
|
||||
// 'ham' => $ham,
|
||||
// 'spam' => $spam,
|
||||
// 'reject' => $reject,
|
||||
// 'rspamdVer' => trim((string) @shell_exec('rspamadm version 2>/dev/null')) ?: '–',
|
||||
// 'clamVer' => $clamVer,
|
||||
// 'sigUpdated' => $sigUpdated,
|
||||
// ];
|
||||
//
|
||||
// // Persistieren (DB→Redis) + kurzer UI-Cache
|
||||
// Setting::set('spamav.metrics', $data);
|
||||
// Cache::put('dash.spamav', $data, 60);
|
||||
//
|
||||
// $this->info(sprintf(
|
||||
// 'ham=%d spam=%d reject=%d | rspamd=%s | clam=%s',
|
||||
// $data['ham'], $data['spam'], $data['reject'], $data['rspamdVer'], $data['clamVer']
|
||||
// ));
|
||||
//
|
||||
// return self::SUCCESS;
|
||||
// }
|
||||
//}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
|
@ -8,25 +9,41 @@ use App\Models\Setting;
|
|||
class StorageProbe extends Command
|
||||
{
|
||||
protected $signature = 'health:probe-disk {target=/}';
|
||||
protected $description = 'Speichert Storage-Werte (inkl. Frei+5%) in settings:health.disk';
|
||||
protected $description = 'Speichert Storage-Werte (inkl. Breakdown) in settings:health.disk';
|
||||
|
||||
// Quelle für vorberechnete Mail-Summen (kommt aus mail:update-stats)
|
||||
private const MAILBOX_TOTALS_KEY = 'mailbox.totals';
|
||||
// Wie lange dürfen Mail-Summen alt sein, bevor wir auf du-Fallback gehen (Sekunden)
|
||||
private const MAILBOX_TOTALS_STALE = 900; // 15 Min
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$target = $this->argument('target') ?: '/';
|
||||
$data = $this->probe($target);
|
||||
$data = $this->probe($target);
|
||||
|
||||
// Persistiert (DB + Redis) über dein Settings-Model
|
||||
Setting::set('health.disk', $data);
|
||||
Setting::set('health.disk_updated_at', now()->toIso8601String());
|
||||
|
||||
// hübsche Konsole
|
||||
$hb = function (int $bytes): string {
|
||||
$b = max(0, $bytes);
|
||||
if ($b >= 1024 ** 3) return number_format($b / 1024 ** 3, 1) . ' GB';
|
||||
if ($b >= 1024 ** 2) return number_format($b / 1024 ** 2, 2) . ' MiB';
|
||||
if ($b >= 1024) return number_format($b / 1024, 0) . ' KiB';
|
||||
return $b . ' B';
|
||||
};
|
||||
|
||||
$bd = $data['breakdown_bytes'] ?? ['system' => 0, 'mails' => 0, 'backup' => 0];
|
||||
|
||||
$this->info(sprintf(
|
||||
'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d)',
|
||||
'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s',
|
||||
$data['mount'],
|
||||
$data['total_gb'],
|
||||
$data['used_gb'],
|
||||
$data['free_gb'],
|
||||
$data['free_plus_reserve_gb'],
|
||||
$data['percent_used_total'],
|
||||
$hb((int)$bd['system']), $hb((int)$bd['mails']), $hb((int)$bd['backup'])
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
|
|
@ -34,43 +51,360 @@ class StorageProbe extends Command
|
|||
|
||||
protected function probe(string $target): array
|
||||
{
|
||||
$line = trim((string) @shell_exec('df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
|
||||
// ── 1) df lesen (Gesamt/Frei inkl. Reserve) ──────────────────────────
|
||||
$line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
|
||||
|
||||
$device = $mount = '';
|
||||
$totalKb = $usedKb = $availKb = 0;
|
||||
|
||||
if ($line !== '') {
|
||||
$p = preg_split('/\s+/', $line);
|
||||
if (count($p) >= 6) {
|
||||
$device = $p[0];
|
||||
$totalKb = (int) $p[1]; // TOTAL (inkl. Reserve)
|
||||
$usedKb = (int) $p[2]; // Used
|
||||
$availKb = (int) $p[3]; // Avail (User-sicht)
|
||||
$mount = $p[5];
|
||||
$device = $p[0];
|
||||
$totalKb = (int)$p[1]; // TOTAL (inkl. Reserve)
|
||||
$usedKb = (int)$p[2]; // Used
|
||||
$availKb = (int)$p[3]; // Avail (User-sicht)
|
||||
$mount = $p[5];
|
||||
}
|
||||
}
|
||||
|
||||
$toGiB = static fn($kb) => (int) round(max(0, (int)$kb) / (1024*1024));
|
||||
|
||||
$totalGb = $toGiB($totalKb);
|
||||
$freeGb = $toGiB($availKb); // user-verfügbar
|
||||
$usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
|
||||
$res5Gb = (int) round($totalGb * 0.05); // 5% von Gesamt
|
||||
$toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // Ganzzahl für Kopfzahlen
|
||||
$totalGb = $toGiB_i($totalKb);
|
||||
$freeGb = $toGiB_i($availKb);
|
||||
$usedGb = max(0, $totalGb - $freeGb);
|
||||
$res5Gb = (int)round($totalGb * 0.05);
|
||||
$freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
|
||||
$percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0;
|
||||
|
||||
$percentUsed = $totalGb > 0 ? (int) round($usedGb * 100 / $totalGb) : 0;
|
||||
// Bytes für Breakdown rechnen
|
||||
$totalBytes = (int)$totalKb * 1024;
|
||||
$freeBytes = (int)$availKb * 1024;
|
||||
$usedBytes = max(0, $totalBytes - $freeBytes);
|
||||
|
||||
// ── 2) Mails: bevorzugt aus Cache (mail:update-stats) ────────────────
|
||||
[$mailUsersBytes, $mailSystemBytes] = $this->readMailTotals();
|
||||
|
||||
// ── 3) Backups schnell messen ────────────────────────────────────────
|
||||
$bytesBackup = $this->duBytesDir('/var/backups/mailwolt');
|
||||
|
||||
// ── 4) Breakdown auflösen und konsistent machen ─────────────────────
|
||||
// alles, was nicht Mails/Backups ist, dem System zuordnen
|
||||
$bytesMails = max(0, (int)$mailUsersBytes); // nur user_mail in "Mails"
|
||||
$bytesSystem = max(0, $usedBytes - ($bytesMails + $bytesBackup));
|
||||
|
||||
// Wenn Vorberechnung system_mail existiert, zähle sie explizit zum System.
|
||||
$bytesSystem += max(0, (int)$mailSystemBytes);
|
||||
|
||||
// negativ verhindern (Messrauschen)
|
||||
if ($bytesSystem < 0) {
|
||||
$bytesSystem = 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'device' => $device ?: 'unknown',
|
||||
'mount' => $mount ?: $target,
|
||||
'device' => $device ?: 'unknown',
|
||||
'mount' => $mount ?: $target,
|
||||
|
||||
'total_gb' => $totalGb,
|
||||
'used_gb' => $usedGb, // inkl. Reserve
|
||||
'free_gb' => $freeGb, // User-sicht
|
||||
'reserve5_gb' => $res5Gb, // Info
|
||||
'free_plus_reserve_gb' => $freePlusReserveGb, // ← das willst du anzeigen
|
||||
'total_gb' => $totalGb,
|
||||
'used_gb' => $usedGb,
|
||||
'free_gb' => $freeGb,
|
||||
'reserve5_gb' => $res5Gb,
|
||||
'free_plus_reserve_gb' => $freePlusReserveGb,
|
||||
|
||||
'percent_used_total' => $percentUsed, // fürs Donut (~15%)
|
||||
'percent_used_total' => $percentUsed,
|
||||
|
||||
'breakdown_bytes' => [
|
||||
'system' => $bytesSystem,
|
||||
'mails' => $bytesMails,
|
||||
'backup' => $bytesBackup,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest vorberechnete Mail-Summen aus settings: mail.totals.
|
||||
* Fallback: wenn nicht vorhanden/zu alt, ermittelt Mails per du (nur dann).
|
||||
*
|
||||
* @return array{0:int,1:int} [users_bytes, system_bytes]
|
||||
*/
|
||||
private function readMailTotals(): array
|
||||
{
|
||||
$totals = (array)(Setting::get(self::MAILBOX_TOTALS_KEY, []) ?: []);
|
||||
$ts = isset($totals['updated_at']) ? strtotime((string)$totals['updated_at']) : null;
|
||||
$fresh = $ts && (time() - $ts) <= self::MAILBOX_TOTALS_STALE;
|
||||
|
||||
if ($fresh) {
|
||||
$users = (int)($totals['users_bytes'] ?? 0);
|
||||
$system = (int)($totals['system_bytes'] ?? 0);
|
||||
return [max(0, $users), max(0, $system)];
|
||||
}
|
||||
|
||||
// Fallback EINMAL: grob mails via detectMailRoot() messen
|
||||
$root = $this->detectMailRoot();
|
||||
$usersBytes = $root ? $this->duBytesDir($root) : 0;
|
||||
|
||||
return [max(0, $usersBytes), 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Versucht, das Wurzelverzeichnis der Maildaten zu finden (Dovecot/Postfix),
|
||||
* ohne auf konkrete Setups festgenagelt zu sein.
|
||||
*/
|
||||
private function detectMailRoot(): ?string
|
||||
{
|
||||
// Dovecot bevorzugt
|
||||
$ml = trim((string)@shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\''));
|
||||
if ($ml !== '') {
|
||||
if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) {
|
||||
$root = rtrim($m[1]);
|
||||
foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) {
|
||||
if (str_ends_with($root, $suffix)) {
|
||||
$root = dirname($root);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_dir($root)) return $root;
|
||||
}
|
||||
}
|
||||
|
||||
// Postfix-Konfiguration
|
||||
$vmb = trim((string)@shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\''));
|
||||
if ($vmb !== '' && is_dir($vmb)) return $vmb;
|
||||
|
||||
// Fallbacks
|
||||
foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) {
|
||||
if (is_dir($cand)) return $cand;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Summe in Bytes für ein Verzeichnisbaum; robust & schnell genug. */
|
||||
private function duBytesDir(string $path): int
|
||||
{
|
||||
if (!is_dir($path)) return 0;
|
||||
$out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1');
|
||||
return max(0, (int)trim((string)$out));
|
||||
}
|
||||
}
|
||||
|
||||
//namespace App\Console\Commands;
|
||||
//
|
||||
//use Illuminate\Console\Command;
|
||||
//use App\Models\Setting;
|
||||
//
|
||||
//class StorageProbe extends Command
|
||||
//{
|
||||
// protected $signature = 'health:probe-disk {target=/}';
|
||||
// protected $description = 'Speichert Storage-Werte (inkl. Breakdown) in settings:health.disk';
|
||||
//
|
||||
// public function handle(): int
|
||||
// {
|
||||
// $target = $this->argument('target') ?: '/';
|
||||
// $data = $this->probe($target);
|
||||
//
|
||||
// Setting::set('health.disk', $data);
|
||||
// Setting::set('health.disk_updated_at', now()->toIso8601String());
|
||||
//
|
||||
// $hb = function (int $bytes): string {
|
||||
// $b = max(0, $bytes);
|
||||
// if ($b >= 1024**3) return number_format($b / 1024**3, 1).' GB';
|
||||
// if ($b >= 1024**2) return number_format($b / 1024**2, 2).' MiB';
|
||||
// if ($b >= 1024) return number_format($b / 1024, 0).' KiB';
|
||||
// return $b.' B';
|
||||
// };
|
||||
//
|
||||
// $this->info(sprintf(
|
||||
// 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s',
|
||||
// $data['mount'],
|
||||
// $data['total_gb'],
|
||||
// $data['used_gb'],
|
||||
// $data['free_gb'],
|
||||
// $data['free_plus_reserve_gb'],
|
||||
// $data['percent_used_total'],
|
||||
// $hb((int)$data['breakdown_bytes']['system']),
|
||||
// $hb((int)$data['breakdown_bytes']['mails']),
|
||||
// $hb((int)$data['breakdown_bytes']['backup']),
|
||||
// ));
|
||||
// return self::SUCCESS;
|
||||
// }
|
||||
//
|
||||
// private function detectMailRoot(): ?string
|
||||
// {
|
||||
// // Dovecot
|
||||
// $ml = trim((string) @shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\''));
|
||||
// if ($ml !== '') {
|
||||
// if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) {
|
||||
// $root = rtrim($m[1]);
|
||||
// foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) {
|
||||
// if (str_ends_with($root, $suffix)) { $root = dirname($root); break; }
|
||||
// }
|
||||
// if (is_dir($root)) return $root;
|
||||
// }
|
||||
// }
|
||||
// // Postfix
|
||||
// $vmb = trim((string) @shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\''));
|
||||
// if ($vmb !== '' && is_dir($vmb)) return $vmb;
|
||||
//
|
||||
// // Fallbacks
|
||||
// foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) {
|
||||
// if (is_dir($cand)) return $cand;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// private function duBytesDir(string $path): int
|
||||
// {
|
||||
// if (!is_dir($path)) return 0;
|
||||
// $out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1');
|
||||
// return max(0, (int) trim((string) $out));
|
||||
// }
|
||||
//
|
||||
// protected function probe(string $target): array
|
||||
// {
|
||||
// // --- df: Gesamtdaten des Filesystems (inkl. Reserve) -----------------
|
||||
// $line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
|
||||
//
|
||||
// $device = $mount = '';
|
||||
// $totalKb = $usedKb = $availKb = 0;
|
||||
//
|
||||
// if ($line !== '') {
|
||||
// $p = preg_split('/\s+/', $line);
|
||||
// if (count($p) >= 6) {
|
||||
// $device = $p[0];
|
||||
// $totalKb = (int)$p[1]; // TOTAL (inkl. Reserve)
|
||||
// $usedKb = (int)$p[2]; // Used
|
||||
// $availKb = (int)$p[3]; // Avail (User-sicht)
|
||||
// $mount = $p[5];
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // ganzzahlig (UI: Gesamt/Genutzt/Frei)
|
||||
// $toGiB_f = static fn($kb) => round(max(0, (int)$kb) / (1024 * 1024), 1); // eine Nachkommastelle (Breakdown/Legende)
|
||||
//
|
||||
// $totalGb = $toGiB_i($totalKb);
|
||||
// $freeGb = $toGiB_i($availKb); // user-verfügbar
|
||||
// $usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
|
||||
// $res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt
|
||||
// $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
|
||||
// $percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0;
|
||||
//
|
||||
// $duBytes = function (string $path): int {
|
||||
// if (!is_dir($path)) return 0;
|
||||
// $b = (int) trim((string) @shell_exec(
|
||||
// 'LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'
|
||||
// ));
|
||||
// return max(0, $b);
|
||||
// };
|
||||
//
|
||||
// $mailRoot = $this->detectMailRoot();
|
||||
// $bytesMails = $mailRoot ? $this->duBytesDir($mailRoot) : 0;
|
||||
// $bytesBackup = $duBytes('/var/backups/mailwolt');
|
||||
//
|
||||
// $totalBytes = (int) $totalKb * 1024;
|
||||
// $freeBytes = (int) $availKb * 1024;
|
||||
// $usedBytes = max(0, $totalBytes - $freeBytes);
|
||||
//
|
||||
// $bytesSystem = max(0, $usedBytes - ($bytesMails + $bytesBackup));
|
||||
//
|
||||
// return [
|
||||
// 'device' => $device ?: 'unknown',
|
||||
// 'mount' => $mount ?: $target,
|
||||
//
|
||||
// 'total_gb' => $totalGb,
|
||||
// 'used_gb' => $usedGb, // inkl. Reserve
|
||||
// 'free_gb' => $freeGb, // User-sicht
|
||||
// 'reserve5_gb' => $res5Gb, // Info
|
||||
// 'free_plus_reserve_gb' => $freePlusReserveGb, // Anzeige „Frei“
|
||||
//
|
||||
// 'percent_used_total' => $percentUsed,
|
||||
//
|
||||
// // Reale Breakdown-Werte
|
||||
// 'breakdown_bytes' => [
|
||||
// 'system' => $bytesSystem,
|
||||
// 'mails' => $bytesMails,
|
||||
// 'backup' => $bytesBackup,
|
||||
// ],
|
||||
// ];
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
//
|
||||
//namespace App\Console\Commands;
|
||||
//
|
||||
//use Illuminate\Console\Command;
|
||||
//use App\Models\Setting;
|
||||
//
|
||||
//class StorageProbe extends Command
|
||||
//{
|
||||
// protected $signature = 'health:probe-disk {target=/}';
|
||||
// protected $description = 'Speichert Storage-Werte (inkl. Frei+5%) in settings:health.disk';
|
||||
//
|
||||
// public function handle(): int
|
||||
// {
|
||||
// $target = $this->argument('target') ?: '/';
|
||||
// $data = $this->probe($target);
|
||||
//
|
||||
// // Persistiert (DB + Redis) über dein Settings-Model
|
||||
// Setting::set('health.disk', $data);
|
||||
// Setting::set('health.disk_updated_at', now()->toIso8601String());
|
||||
//
|
||||
// $this->info(sprintf(
|
||||
// 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d)',
|
||||
// $data['mount'],
|
||||
// $data['total_gb'],
|
||||
// $data['used_gb'],
|
||||
// $data['free_gb'],
|
||||
// $data['free_plus_reserve_gb'],
|
||||
// $data['percent_used_total'],
|
||||
// ));
|
||||
//
|
||||
// return self::SUCCESS;
|
||||
// }
|
||||
//
|
||||
// protected function probe(string $target): array
|
||||
// {
|
||||
// $line = trim((string) @shell_exec('df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
|
||||
//
|
||||
// $device = $mount = '';
|
||||
// $totalKb = $usedKb = $availKb = 0;
|
||||
//
|
||||
// if ($line !== '') {
|
||||
// $p = preg_split('/\s+/', $line);
|
||||
// if (count($p) >= 6) {
|
||||
// $device = $p[0];
|
||||
// $totalKb = (int) $p[1]; // TOTAL (inkl. Reserve)
|
||||
// $usedKb = (int) $p[2]; // Used
|
||||
// $availKb = (int) $p[3]; // Avail (User-sicht)
|
||||
// $mount = $p[5];
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $toGiB = static fn($kb) => (int) round(max(0, (int)$kb) / (1024*1024));
|
||||
//
|
||||
// $totalGb = $toGiB($totalKb);
|
||||
// $freeGb = $toGiB($availKb); // user-verfügbar
|
||||
// $usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
|
||||
// $res5Gb = (int) round($totalGb * 0.05); // 5% von Gesamt
|
||||
// $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
|
||||
//
|
||||
// $percentUsed = $totalGb > 0 ? (int) round($usedGb * 100 / $totalGb) : 0;
|
||||
//
|
||||
// return [
|
||||
// 'device' => $device ?: 'unknown',
|
||||
// 'mount' => $mount ?: $target,
|
||||
//
|
||||
// 'total_gb' => $totalGb,
|
||||
// 'used_gb' => $usedGb, // inkl. Reserve
|
||||
// 'free_gb' => $freeGb, // User-sicht
|
||||
// 'reserve5_gb' => $res5Gb, // Info
|
||||
// 'free_plus_reserve_gb' => $freePlusReserveGb, // ← das willst du anzeigen
|
||||
//
|
||||
// 'percent_used_total' => $percentUsed, // fürs Donut (~15%)
|
||||
// 'breakdown' => [
|
||||
// 'system_gb' => 5.2, // OS, App, Logs …
|
||||
// 'mails_gb' => 2.8, // /var/mail/vhosts
|
||||
// 'backup_gb' => 1.0, // /var/backups/mailwolt (oder wohin du sicherst)
|
||||
// ],
|
||||
// ];
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -15,39 +15,119 @@ class UpdateMailboxStats extends Command
|
|||
protected $signature = 'mail:update-stats {--user=}';
|
||||
protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; ohne DB-Spalten).';
|
||||
|
||||
// public function handle(): int
|
||||
// {
|
||||
// $log = Log::channel('mailstats');
|
||||
// $onlyUser = trim((string)$this->option('user')) ?: null;
|
||||
// $t0 = microtime(true);
|
||||
//
|
||||
// // Basis-Query: nur aktive, keine System-Mailboxen und keine System-Domains
|
||||
// $base = MailUser::query()
|
||||
// ->select(['id', 'domain_id', 'localpart', 'email', 'is_active', 'is_system'])
|
||||
// ->with(['domain:id,domain,is_system'])
|
||||
// ->where('is_active', true)
|
||||
// ->where('is_system', false)
|
||||
// ->whereHas('domain', fn($d) => $d->where('is_system', false));
|
||||
//
|
||||
// if ($onlyUser) {
|
||||
// $base->where('email', $onlyUser);
|
||||
// }
|
||||
//
|
||||
// $checked = 0;
|
||||
// $changed = 0;
|
||||
//
|
||||
// $log->info('mail:update-stats START', ['only' => $onlyUser]);
|
||||
//
|
||||
// $base->orderBy('id')->chunkById(200, function ($users) use (&$checked, &$changed, $log) {
|
||||
// foreach ($users as $u) {
|
||||
// $checked++;
|
||||
//
|
||||
// // Email robust bestimmen (raw -> accessor -> zusammengesetzt)
|
||||
// $raw = (string)($u->getRawOriginal('email') ?? '');
|
||||
// $email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
|
||||
//
|
||||
// if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
|
||||
// // still – kein Log-Spam
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// [$local, $domain] = explode('@', $email, 2);
|
||||
// $maildir = "/var/mail/vhosts/{$domain}/{$local}";
|
||||
//
|
||||
// // Größe in Bytes (rekursiv)
|
||||
// $usedBytes = 0;
|
||||
// if (is_dir($maildir)) {
|
||||
// $it = new RecursiveIteratorIterator(
|
||||
// new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
||||
// );
|
||||
// foreach ($it as $f) {
|
||||
// if ($f->isFile()) $usedBytes += $f->getSize();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Message-Count
|
||||
// $messageCount = $this->countViaDoveadm($email);
|
||||
// if ($messageCount === null) {
|
||||
// $messageCount = $this->countViaFilesystem($maildir);
|
||||
// }
|
||||
//
|
||||
// $key = "mailbox.{$email}";
|
||||
// $prev = (array)(Setting::get($key, []) ?: []);
|
||||
// $new = [
|
||||
// 'used_bytes' => (int)$usedBytes,
|
||||
// 'message_count' => (int)$messageCount,
|
||||
// 'updated_at' => now()->toDateTimeString(),
|
||||
// ];
|
||||
//
|
||||
// if (($prev['used_bytes'] ?? null) !== $new['used_bytes']
|
||||
// || ($prev['message_count'] ?? null) !== $new['message_count']) {
|
||||
// Setting::set($key, $new);
|
||||
// $changed++;
|
||||
//
|
||||
// // kurze Ausgabe & Info-Log NUR bei Änderung
|
||||
// $this->line(sprintf("%-35s %7.1f MiB %5d msgs",
|
||||
// $email, $usedBytes / 1048576, $messageCount));
|
||||
// $log->info('updated', ['email' => $email, 'used_bytes' => $new['used_bytes'], 'message_count' => $new['message_count']]);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// $ms = (int)((microtime(true) - $t0) * 1000);
|
||||
// $log->info('mail:update-stats DONE', compact('checked', 'changed', 'ms'));
|
||||
// $this->info('Mailbox-Statistiken aktualisiert.');
|
||||
// return self::SUCCESS;
|
||||
// }
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$log = Log::channel('mailstats');
|
||||
$onlyUser = trim((string)$this->option('user')) ?: null;
|
||||
$t0 = microtime(true);
|
||||
|
||||
// Basis-Query: nur aktive, keine System-Mailboxen und keine System-Domains
|
||||
// Summen
|
||||
$sumUserBytes = 0;
|
||||
$sumSystemBytes = 0;
|
||||
|
||||
// aktiver Benutzerbestand (inkl. Domains, um system/non-system zu unterscheiden)
|
||||
$base = MailUser::query()
|
||||
->select(['id', 'domain_id', 'localpart', 'email', 'is_active', 'is_system'])
|
||||
->select(['id','domain_id','localpart','email','is_active','is_system'])
|
||||
->with(['domain:id,domain,is_system'])
|
||||
->where('is_active', true)
|
||||
->where('is_system', false)
|
||||
->whereHas('domain', fn($d) => $d->where('is_system', false));
|
||||
->where('is_active', true);
|
||||
|
||||
if ($onlyUser) {
|
||||
$base->where('email', $onlyUser);
|
||||
}
|
||||
|
||||
$checked = 0;
|
||||
$changed = 0;
|
||||
|
||||
$checked = 0; $changed = 0;
|
||||
$log->info('mail:update-stats START', ['only' => $onlyUser]);
|
||||
|
||||
$base->orderBy('id')->chunkById(200, function ($users) use (&$checked, &$changed, $log) {
|
||||
$base->orderBy('id')->chunkById(200, function ($users) use (&$checked,&$changed,&$sumUserBytes,&$sumSystemBytes,$log) {
|
||||
foreach ($users as $u) {
|
||||
$checked++;
|
||||
|
||||
// Email robust bestimmen (raw -> accessor -> zusammengesetzt)
|
||||
$raw = (string)($u->getRawOriginal('email') ?? '');
|
||||
$email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
|
||||
|
||||
if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
|
||||
// still – kein Log-Spam
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -57,47 +137,59 @@ class UpdateMailboxStats extends Command
|
|||
// Größe in Bytes (rekursiv)
|
||||
$usedBytes = 0;
|
||||
if (is_dir($maildir)) {
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
||||
$it = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($it as $f) {
|
||||
if ($f->isFile()) $usedBytes += $f->getSize();
|
||||
}
|
||||
foreach ($it as $f) if ($f->isFile()) $usedBytes += $f->getSize();
|
||||
}
|
||||
|
||||
$isSystemDomain = (bool)($u->domain->is_system ?? false);
|
||||
|
||||
if ($isSystemDomain || $u->is_system) {
|
||||
// systemische Mailboxen fließen nur in die System-Summe ein
|
||||
$sumSystemBytes += $usedBytes;
|
||||
continue; // KEIN per-user Setting
|
||||
}
|
||||
|
||||
// Message-Count
|
||||
$messageCount = $this->countViaDoveadm($email);
|
||||
if ($messageCount === null) {
|
||||
$messageCount = $this->countViaFilesystem($maildir);
|
||||
}
|
||||
if ($messageCount === null) $messageCount = $this->countViaFilesystem($maildir);
|
||||
|
||||
$key = "mailbox.{$email}";
|
||||
$sumUserBytes += $usedBytes;
|
||||
|
||||
$key = "mailbox.{$email}";
|
||||
$prev = (array)(Setting::get($key, []) ?: []);
|
||||
$new = [
|
||||
'used_bytes' => (int)$usedBytes,
|
||||
$new = [
|
||||
'used_bytes' => (int)$usedBytes,
|
||||
'message_count' => (int)$messageCount,
|
||||
'updated_at' => now()->toDateTimeString(),
|
||||
'updated_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
if (($prev['used_bytes'] ?? null) !== $new['used_bytes']
|
||||
|| ($prev['message_count'] ?? null) !== $new['message_count']) {
|
||||
Setting::set($key, $new);
|
||||
$changed++;
|
||||
|
||||
// kurze Ausgabe & Info-Log NUR bei Änderung
|
||||
$this->line(sprintf("%-35s %7.1f MiB %5d msgs",
|
||||
$this->line(sprintf("%-35s %7.2f MiB %5d msgs",
|
||||
$email, $usedBytes / 1048576, $messageCount));
|
||||
$log->info('updated', ['email' => $email, 'used_bytes' => $new['used_bytes'], 'message_count' => $new['message_count']]);
|
||||
$log->info('updated', ['email'=>$email]+$new);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$ms = (int)((microtime(true) - $t0) * 1000);
|
||||
$log->info('mail:update-stats DONE', compact('checked', 'changed', 'ms'));
|
||||
// Totals persistieren (Nutzen wir später im StorageProbe)
|
||||
Setting::set('mailbox.totals', [
|
||||
'users_bytes' => (int)$sumUserBytes, // alle nicht-systemischen Mailboxen
|
||||
'system_bytes' => (int)$sumSystemBytes, // systemische Mailboxen
|
||||
'updated_at' => now()->toIso8601String(),
|
||||
]);
|
||||
|
||||
$ms = (int)((microtime(true)-$t0)*1000);
|
||||
$log->info('mail:update-stats DONE', compact('checked','changed','ms','sumUserBytes','sumSystemBytes'));
|
||||
$this->info('Mailbox-Statistiken aktualisiert.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
private function countViaDoveadm(string $email): ?int
|
||||
{
|
||||
$cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u "
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
public function render($request, \Throwable $e)
|
||||
{
|
||||
if ($e instanceof TokenMismatchException) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => 'session_expired',
|
||||
'redirect' => route('login'),
|
||||
], 419);
|
||||
}
|
||||
return redirect()
|
||||
->route('login')
|
||||
->with('warning', 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.');
|
||||
}
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class GuestOnlyMiddleware
|
|||
{
|
||||
if (Auth::check()) {
|
||||
// Eingeloggt → z. B. Dashboard weiterleiten
|
||||
return redirect()->route('dashboard');
|
||||
return redirect()->route('ui.dashboard');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Setting as SettingsModel;
|
||||
use App\Support\CacheVer;
|
||||
use App\Support\WoltGuard\Probes;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
|
@ -16,7 +17,7 @@ class RunHealthChecks implements ShouldQueue
|
|||
{
|
||||
use Queueable, Probes;
|
||||
|
||||
public int $timeout = 10; // safety
|
||||
public int $timeout = 10;
|
||||
public int $tries = 1;
|
||||
|
||||
public function handle(): void
|
||||
|
|
@ -26,13 +27,20 @@ class RunHealthChecks implements ShouldQueue
|
|||
foreach ($cards as $key => $card) {
|
||||
$ok = false;
|
||||
foreach ($card['sources'] as $src) {
|
||||
if ($this->check($src)) { $ok = true; break; }
|
||||
if ($this->check($src)) {
|
||||
$ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$svcRows[] = ['name' => $key, 'ok' => $ok]; // labels brauchst du im UI
|
||||
}
|
||||
|
||||
Cache::put(CacheVer::k('health:services'), $svcRows, 60);
|
||||
$payload = ['ts' => time(), 'rows' => $svcRows];
|
||||
Cache::put(CacheVer::k('health:services'), $payload, 300);
|
||||
Log::info('WG: writing services', ['count'=>count($svcRows)]);
|
||||
SettingsModel::set('woltguard.services', $payload);
|
||||
Cache::forget('health:services');
|
||||
|
||||
}
|
||||
|
||||
/** Wraps a probe; logs and returns fallback on error */
|
||||
|
|
@ -68,9 +76,16 @@ class RunHealthChecks implements ShouldQueue
|
|||
|
||||
protected function queueWorkers(): array
|
||||
{
|
||||
$r = Process::run("systemctl is-active supervisor");
|
||||
$raw = trim($r->output() ?: $r->errorOutput());
|
||||
return ['name'=>'queue', 'ok'=>$raw === 'active', 'raw'=>$raw ?: 'unknown'];
|
||||
$okQueue = $this->probeSystemd('mailwolt-queue.service');
|
||||
$okSched = $this->probeSystemd('mailwolt-schedule.service');
|
||||
|
||||
$ok = $okQueue && $okSched;
|
||||
$raw = sprintf('queue:%s sched:%s', $okQueue ? 'active' : 'down', $okSched ? 'active' : 'down');
|
||||
|
||||
return ['name' => 'queue', 'ok' => $ok, 'raw' => $raw];
|
||||
// $r = Process::run("systemctl is-active supervisor");
|
||||
// $raw = trim($r->output() ?: $r->errorOutput());
|
||||
// return ['name'=>'queue', 'ok'=>$raw === 'active', 'raw'=>$raw ?: 'unknown'];
|
||||
}
|
||||
|
||||
protected function diskUsage(): array
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,39 +5,540 @@ namespace App\Livewire\Ui\Mail;
|
|||
use Livewire\Component;
|
||||
use App\Models\Domain;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DnsHealthCard extends Component
|
||||
{
|
||||
public array $rows = []; // [ ['domain'=>..., 'dkim'=>bool, 'dmarc'=>bool, 'tlsa'=>bool], ... ]
|
||||
public array $rows = []; // [{id,name,ok,missing:[...]}]
|
||||
public string $mtaHost = '';
|
||||
public bool $tlsa = false;
|
||||
|
||||
public function mount(): void { $this->load(); }
|
||||
public function render() { return view('livewire.ui.mail.dns-health-card'); }
|
||||
public function refresh(): void { $this->load(true); }
|
||||
|
||||
protected function load(bool $force=false): void
|
||||
public function mount(): void
|
||||
{
|
||||
$this->rows = Cache::remember('dash.dnshealth', $force ? 1 : 600, function () {
|
||||
$rows = [];
|
||||
$domains = Domain::query()->where('is_system', false)->where('is_active', true)->get(['domain']);
|
||||
foreach ($domains as $d) {
|
||||
$dom = $d->domain;
|
||||
$dkim = $this->hasTxt("_domainkey.$dom"); // rough: just any dkim TXT exists
|
||||
$dmarc = $this->hasTxt("_dmarc.$dom");
|
||||
$tlsa = $this->hasTlsa("_25._tcp.$dom") || $this->hasTlsa("_465._tcp.$dom") || $this->hasTlsa("_587._tcp.$dom");
|
||||
$rows[] = compact('dom','dkim','dmarc','tlsa');
|
||||
$this->load();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.mail.dns-health-card');
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->load();
|
||||
}
|
||||
|
||||
public function openDnsModal(int $domainId): void
|
||||
{
|
||||
$this->dispatch('openModal', component: 'ui.domain.modal.domain-dns-modal', arguments: ['domainId' => $domainId]);
|
||||
}
|
||||
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// [$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v2', $force ? 1 : 600, function () {
|
||||
//
|
||||
// $base = trim((string) env('BASE_DOMAIN', ''));
|
||||
// $mtaSub = trim((string) env('MTA_SUB', 'mx'));
|
||||
// $mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub; // z.B. mx.nexlab.at
|
||||
//
|
||||
// // ▼ gewünschter Filter:
|
||||
// $domains = Domain::query()
|
||||
// ->where('is_active', true)
|
||||
// ->where('is_server', false) // <<< Server-Domain sauber ausschließen
|
||||
// ->orderBy('domain')
|
||||
// ->get(['id', 'domain']);
|
||||
//
|
||||
// $rows = [];
|
||||
// foreach ($domains as $d) {
|
||||
// $dom = $d->domain;
|
||||
//
|
||||
// // DKIM-Selector ermitteln: .env > DB > Fallback null
|
||||
// $selector = trim((string) env('DKIM_SELECTOR', ''));
|
||||
// if ($selector === '') {
|
||||
// $selector = (string) DB::table('dkim_keys')
|
||||
// ->where('domain_id', $d->id)
|
||||
// ->where('is_active', 1)
|
||||
// ->orderByDesc('id')
|
||||
// ->value('selector') ?? '';
|
||||
// }
|
||||
//
|
||||
// $missing = [];
|
||||
//
|
||||
// if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX';
|
||||
// if (!$this->hasSpf($dom)) $missing[] = 'SPF';
|
||||
// if (!$this->hasDkim($dom, $selector)) $missing[] = 'DKIM';
|
||||
// if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
|
||||
//
|
||||
// $rows[] = [
|
||||
// 'id' => (int) $d->id,
|
||||
// 'name' => $dom,
|
||||
// 'ok' => empty($missing),
|
||||
// 'missing' => $missing,
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// // Hostweites TLSA (nur Hinweis)
|
||||
// $tlsa = $this->hasTlsa("_25._tcp.$mtaHost") || $this->hasTlsa("_465._tcp.$mtaHost") || $this->hasTlsa("_587._tcp.$mtaHost");
|
||||
//
|
||||
// return [$mtaHost, $tlsa, $rows];
|
||||
// });
|
||||
// }
|
||||
|
||||
protected function load(): void
|
||||
{
|
||||
$base = trim((string) env('BASE_DOMAIN', ''));
|
||||
$mtaSub = trim((string) env('MTA_SUB', 'mx'));
|
||||
$mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub; // z.B. mx.nexlab.at
|
||||
|
||||
// nur aktive, NICHT-Server-Domains (System + Custom, solange is_server = false)
|
||||
$domains = Domain::query()
|
||||
->where('is_active', true)
|
||||
->where('is_server', false)
|
||||
->orderBy('domain')
|
||||
->get(['id','domain']);
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($domains as $d) {
|
||||
$dom = $d->domain;
|
||||
|
||||
// DKIM-Selector: .env > DB > leer
|
||||
$selector = trim((string) env('DKIM_SELECTOR', ''));
|
||||
if ($selector === '') {
|
||||
$selector = (string) DB::table('dkim_keys')
|
||||
->where('domain_id', $d->id)
|
||||
->where('is_active', 1)
|
||||
->orderByDesc('id')
|
||||
->value('selector') ?? '';
|
||||
}
|
||||
return $rows;
|
||||
});
|
||||
|
||||
$missing = [];
|
||||
if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX';
|
||||
if (!$this->hasSpf($dom)) $missing[] = 'SPF';
|
||||
if (!$this->hasDkim($dom, $selector)) $missing[] = 'DKIM';
|
||||
if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
|
||||
|
||||
$rows[] = [
|
||||
'id' => (int) $d->id,
|
||||
'name' => $dom,
|
||||
'ok' => empty($missing),
|
||||
'missing' => $missing,
|
||||
];
|
||||
}
|
||||
|
||||
// Hostweites TLSA (nur Info)
|
||||
$tlsa = $this->hasTlsa("_25._tcp.$mtaHost")
|
||||
|| $this->hasTlsa("_465._tcp.$mtaHost")
|
||||
|| $this->hasTlsa("_587._tcp.$mtaHost");
|
||||
|
||||
$this->mtaHost = $mtaHost;
|
||||
$this->tlsa = $tlsa;
|
||||
$this->rows = $rows;
|
||||
}
|
||||
/* ── DNS Helpers ───────────────────────────────────────────────────── */
|
||||
|
||||
protected function digShort(string $type, string $name): string
|
||||
{
|
||||
$cmd = "timeout 2 dig +short " . escapeshellarg($name) . ' ' . escapeshellarg(strtoupper($type)) . " 2>/dev/null";
|
||||
return (string) @shell_exec($cmd) ?: '';
|
||||
}
|
||||
|
||||
protected function hasTxt(string $name): bool
|
||||
{
|
||||
$out = @shell_exec("dig +short TXT ".escapeshellarg($name)." 2>/dev/null");
|
||||
return is_string($out) && trim($out) !== '';
|
||||
return trim($this->digShort('TXT', $name)) !== '';
|
||||
}
|
||||
|
||||
protected function hasTlsa(string $name): bool
|
||||
{
|
||||
$out = @shell_exec("dig +short TLSA ".escapeshellarg($name)." 2>/dev/null");
|
||||
return is_string($out) && trim($out) !== '';
|
||||
return trim($this->digShort('TLSA', $name)) !== '';
|
||||
}
|
||||
|
||||
protected function hasSpf(string $domain): bool
|
||||
{
|
||||
$out = $this->digShort('TXT', $domain);
|
||||
foreach (preg_split('/\R+/', trim($out)) as $line) {
|
||||
if (stripos($line, 'v=spf1') !== false) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ▼ DKIM: bevorzugt konkreten Selector prüfen; wenn leer, versuche Policy (_domainkey)
|
||||
protected function hasDkim(string $domain, string $selector = ''): bool
|
||||
{
|
||||
if ($selector !== '' && $this->hasTxt("{$selector}._domainkey.$domain")) {
|
||||
return true;
|
||||
}
|
||||
// Fallback: irgendein _domainkey-TXT vorhanden
|
||||
return $this->hasTxt("_domainkey.$domain");
|
||||
}
|
||||
|
||||
// protected function hasDkim(string $domain, string $selector = ''): bool
|
||||
// {
|
||||
// if ($selector !== '') {
|
||||
// return $this->hasTxt("{$selector}._domainkey.$domain");
|
||||
// }
|
||||
// // Manche Betreiber veröffentlichen eine Policy auf _domainkey.<dom>
|
||||
// return $this->hasTxt("_domainkey.$domain");
|
||||
// }
|
||||
|
||||
protected function mxPointsTo(string $domain, array $allowedHosts): bool
|
||||
{
|
||||
$out = $this->digShort('MX', $domain);
|
||||
if ($out === '') return false;
|
||||
|
||||
$targets = [];
|
||||
foreach (preg_split('/\R+/', trim($out)) as $line) {
|
||||
if (preg_match('~\s+([A-Za-z0-9\.\-]+)\.?$~', trim($line), $m)) {
|
||||
$targets[] = strtolower($m[1]);
|
||||
}
|
||||
}
|
||||
if (!$targets) return false;
|
||||
|
||||
$allowed = array_map(fn ($h) => strtolower(rtrim($h, '.')), $allowedHosts);
|
||||
foreach ($targets as $t) {
|
||||
$t = rtrim($t, '.');
|
||||
if (in_array($t, $allowed, true)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//namespace App\Livewire\Ui\Mail;
|
||||
//
|
||||
//use Livewire\Attributes\On;
|
||||
//use Livewire\Component;
|
||||
//use App\Models\Domain;
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//
|
||||
//class DnsHealthCard extends Component
|
||||
//{
|
||||
// public array $rows = []; // [{id,name,ok,missing:[...]}]
|
||||
// public string $mtaHost = ''; // z.B. mx.nexlab.at
|
||||
// public bool $tlsa = false; // hostweit
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.mail.dns-health-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// public function openDnsModal(int $domainId): void
|
||||
// {
|
||||
// $this->dispatch('openModal', component: 'ui.domain.modal.domain-dns-modal', arguments: ['domainId' => $domainId]);
|
||||
// }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// [$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v1', $force ? 1 : 600, function () {
|
||||
//
|
||||
// $base = trim((string)env('BASE_DOMAIN', ''));
|
||||
// $mtaSub = trim((string)env('MTA_SUB', 'mx'));
|
||||
// $mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub;
|
||||
//
|
||||
// $rows = [];
|
||||
// $domains = Domain::query()
|
||||
// ->where('is_system', false)
|
||||
// ->where('is_active', true)
|
||||
// ->orderBy('domain')
|
||||
// ->get(['id', 'domain']);
|
||||
//
|
||||
// foreach ($domains as $d) {
|
||||
// $dom = $d->domain;
|
||||
//
|
||||
// $missing = [];
|
||||
//
|
||||
// // Pflicht-Checks
|
||||
// if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX';
|
||||
// if (!$this->hasSpf($dom)) $missing[] = 'SPF';
|
||||
// if (!$this->hasDkim($dom)) $missing[] = 'DKIM';
|
||||
// if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
|
||||
//
|
||||
// $rows[] = [
|
||||
// 'id' => (int)$d->id,
|
||||
// 'name' => $dom,
|
||||
// 'ok' => empty($missing),
|
||||
// 'missing' => $missing,
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// // TLSA (hostweit, nur Info)
|
||||
// $tlsa = $this->hasTlsa("_25._tcp.$mtaHost") || $this->hasTlsa("_465._tcp.$mtaHost") || $this->hasTlsa("_587._tcp.$mtaHost");
|
||||
//
|
||||
// return [$mtaHost, $tlsa, $rows];
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// /* ── DNS Helpers (mit Timeout, damit UI nicht hängt) ───────────────── */
|
||||
//
|
||||
// protected function digShort(string $type, string $name): string
|
||||
// {
|
||||
// $cmd = "timeout 2 dig +short " . escapeshellarg($name) . " " . escapeshellarg(strtoupper($type)) . " 2>/dev/null";
|
||||
// return (string)@shell_exec($cmd) ?: '';
|
||||
// }
|
||||
//
|
||||
// protected function hasTxt(string $name): bool
|
||||
// {
|
||||
// return trim($this->digShort('TXT', $name)) !== '';
|
||||
// }
|
||||
//
|
||||
// protected function hasTlsa(string $name): bool
|
||||
// {
|
||||
// return trim($this->digShort('TLSA', $name)) !== '';
|
||||
// }
|
||||
//
|
||||
// protected function hasSpf(string $domain): bool
|
||||
// {
|
||||
// $out = $this->digShort('TXT', $domain);
|
||||
// foreach (preg_split('/\R+/', trim($out)) as $line) {
|
||||
// if (stripos($line, 'v=spf1') !== false) return true;
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// // DKIM: wenn spezifischer Selector vorhanden → prüfe den, sonst akzeptiere _domainkey-Policy als “vorhanden”
|
||||
// protected function hasDkim(string $domain): bool
|
||||
// {
|
||||
// $sel = trim((string)env('DKIM_SELECTOR', ''));
|
||||
// if ($sel !== '' && $this->hasTxt("{$sel}._domainkey.$domain")) return true;
|
||||
// return $this->hasTxt("_domainkey.$domain");
|
||||
// }
|
||||
//
|
||||
// protected function mxPointsTo(string $domain, array $allowedHosts): bool
|
||||
// {
|
||||
// $out = $this->digShort('MX', $domain);
|
||||
// if ($out === '') return false;
|
||||
//
|
||||
// $targets = [];
|
||||
// foreach (preg_split('/\R+/', trim($out)) as $line) {
|
||||
// // Format: "10 mx.example.com."
|
||||
// if (preg_match('~\s+([A-Za-z0-9\.\-]+)\.?$~', trim($line), $m)) {
|
||||
// $targets[] = strtolower($m[1]);
|
||||
// }
|
||||
// }
|
||||
// if (!$targets) return false;
|
||||
//
|
||||
// $allowed = array_map('strtolower', $allowedHosts);
|
||||
// foreach ($targets as $t) {
|
||||
// if (in_array($t, $allowed, true)) return true;
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//namespace App\Livewire\Ui\Mail;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//use App\Models\Domain;
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//
|
||||
//class DnsHealthCard extends Component
|
||||
//{
|
||||
// public array $domains = []; // [['name'=>..., 'dkim'=>bool, 'dmarc'=>bool], ...]
|
||||
// public string $host = ''; // z.B. mx.nexlab.at
|
||||
// public bool $tlsa = false; // hostbasiert (einmalig)
|
||||
// public ?string $ipv4 = null;
|
||||
// public ?string $ipv6 = null;
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.mail.dns-health-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// [$this->host, $this->tlsa, $this->domains, $this->ipv4, $this->ipv6] =
|
||||
// Cache::remember('dash.dnshealth', $force ? 1 : 900, function () {
|
||||
//
|
||||
// // ── ENV lesen ────────────────────────────────────────────────
|
||||
// $base = trim((string)env('BASE_DOMAIN', ''));
|
||||
// $mtaSub = trim((string)env('MTA_SUB', 'mx'));
|
||||
// $host = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub;
|
||||
//
|
||||
// $ipv4 = trim((string)env('SERVER_PUBLIC_IPV4', '')) ?: null;
|
||||
// $ipv6 = trim((string)env('SERVER_PUBLIC_IPV6', '')) ?: null;
|
||||
//
|
||||
// // ── Domains laden (nur aktive, nicht-system) ────────────────
|
||||
// $rows = [];
|
||||
// $domains = Domain::query()
|
||||
// ->where('is_system', false)
|
||||
// ->where('is_active', true)
|
||||
// ->orderBy('domain')
|
||||
// ->get(['domain']);
|
||||
//
|
||||
// foreach ($domains as $d) {
|
||||
// $dom = $d->domain;
|
||||
// $rows[] = [
|
||||
// 'name' => $dom,
|
||||
// 'dkim' => $this->hasTxt("_domainkey.$dom"),
|
||||
// 'dmarc' => $this->hasTxt("_dmarc.$dom"),
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// // ── TLSA nur hostbasiert prüfen (25/465/587) ────────────────
|
||||
// $tlsa = $this->hasTlsa("_25._tcp.$host")
|
||||
// || $this->hasTlsa("_465._tcp.$host")
|
||||
// || $this->hasTlsa("_587._tcp.$host");
|
||||
//
|
||||
// return [$host, $tlsa, $rows, $ipv4, $ipv6];
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// /* ───────────────────────── DNS Helpers ───────────────────────── */
|
||||
//
|
||||
// protected function hasTxt(string $name): bool
|
||||
// {
|
||||
// $out = @shell_exec("timeout 2 dig +short TXT " . escapeshellarg($name) . " 2>/dev/null");
|
||||
// return is_string($out) && trim($out) !== '';
|
||||
// }
|
||||
//
|
||||
// protected function hasTlsa(string $name): bool
|
||||
// {
|
||||
// $out = @shell_exec("timeout 2 dig +short TLSA " . escapeshellarg($name) . " 2>/dev/null");
|
||||
// return is_string($out) && trim($out) !== '';
|
||||
// }
|
||||
//}
|
||||
|
||||
//namespace App\Livewire\Ui\Mail;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//use App\Models\Domain;
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//
|
||||
//class DnsHealthCard extends Component
|
||||
//{
|
||||
// public array $rows = []; // pro Domain: ['dom','dkim','dmarc']
|
||||
// public string $host = ''; // z.B. mx.nexlab.at
|
||||
// public bool $tlsa = false; // TLSA-Status für den Host
|
||||
// public ?string $ipv4 = null;
|
||||
// public ?string $ipv6 = null;
|
||||
//
|
||||
// public function mount(): void { $this->load(); }
|
||||
// public function render() { return view('livewire.ui.mail.dns-health-card'); }
|
||||
// public function refresh(): void { $this->load(true); }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// // Werte aus .env
|
||||
// $this->ipv4 = trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: null;
|
||||
// $this->ipv6 = trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: null;
|
||||
//
|
||||
// $base = trim((string) env('BASE_DOMAIN', ''));
|
||||
// $mta = trim((string) env('MTA_SUB', 'mx'));
|
||||
// $host = $base ? "{$mta}.{$base}" : $mta; // z.B. mx.nexlab.at
|
||||
// $this->host = $host;
|
||||
//
|
||||
// // Neuer Cache-Key, damit altes Format keinen Crash verursacht
|
||||
// [$calcHost, $calcTlsa, $calcRows] = Cache::remember(
|
||||
// 'dash.dnshealth.v2',
|
||||
// $force ? 1 : 900,
|
||||
// function () use ($host) {
|
||||
// // TLSA: nur 1× pro Host prüfen
|
||||
// $tlsa = $this->hasTlsa("_25._tcp.{$host}")
|
||||
// || $this->hasTlsa("_465._tcp.{$host}")
|
||||
// || $this->hasTlsa("_587._tcp.{$host}");
|
||||
//
|
||||
// // Domains: nur DKIM/DMARC
|
||||
// $rows = [];
|
||||
// $domains = Domain::query()
|
||||
// ->where('is_system', false)
|
||||
// ->where('is_active', true)
|
||||
// ->get(['domain']);
|
||||
//
|
||||
// foreach ($domains as $d) {
|
||||
// $dom = $d->domain;
|
||||
// $dkim = $this->hasTxt("_domainkey.{$dom}");
|
||||
// $dmarc = $this->hasTxt("_dmarc.{$dom}");
|
||||
// $rows[] = compact('dom','dkim','dmarc');
|
||||
// }
|
||||
//
|
||||
// return [$host, $tlsa, $rows];
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// // Defensive: falls mal falsches Datenformat im Cache war
|
||||
// if (!is_string($calcHost) || !is_bool($calcTlsa) || !is_array($calcRows)) {
|
||||
// Cache::forget('dash.dnshealth.v2');
|
||||
// $this->load(true);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// $this->host = $calcHost;
|
||||
// $this->tlsa = $calcTlsa;
|
||||
// $this->rows = $calcRows;
|
||||
// }
|
||||
//
|
||||
// protected function hasTxt(string $name): bool
|
||||
// {
|
||||
// $out = @shell_exec("dig +short TXT " . escapeshellarg($name) . " 2>/dev/null");
|
||||
// return is_string($out) && trim($out) !== '';
|
||||
// }
|
||||
//
|
||||
// protected function hasTlsa(string $name): bool
|
||||
// {
|
||||
// $out = @shell_exec("dig +short TLSA " . escapeshellarg($name) . " 2>/dev/null");
|
||||
// return is_string($out) && trim($out) !== '';
|
||||
// }
|
||||
//}
|
||||
|
||||
//
|
||||
//namespace App\Livewire\Ui\Mail;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//use App\Models\Domain;
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//
|
||||
//class DnsHealthCard extends Component
|
||||
//{
|
||||
// public array $rows = []; // [ ['domain'=>..., 'dkim'=>bool, 'dmarc'=>bool, 'tlsa'=>bool], ... ]
|
||||
//
|
||||
// public function mount(): void { $this->load(); }
|
||||
// public function render() { return view('livewire.ui.mail.dns-health-card'); }
|
||||
// public function refresh(): void { $this->load(true); }
|
||||
//
|
||||
// protected function load(bool $force=false): void
|
||||
// {
|
||||
// $this->rows = Cache::remember('dash.dnshealth', $force ? 1 : 600, function () {
|
||||
// $rows = [];
|
||||
// $domains = Domain::query()->where('is_system', false)->where('is_active', true)->get(['domain']);
|
||||
// foreach ($domains as $d) {
|
||||
// $dom = $d->domain;
|
||||
// $dkim = $this->hasTxt("_domainkey.$dom"); // rough: just any dkim TXT exists
|
||||
// $dmarc = $this->hasTxt("_dmarc.$dom");
|
||||
// $tlsa = $this->hasTlsa("_25._tcp.$dom") || $this->hasTlsa("_465._tcp.$dom") || $this->hasTlsa("_587._tcp.$dom");
|
||||
// $rows[] = compact('dom','dkim','dmarc','tlsa');
|
||||
// }
|
||||
// return $rows;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// protected function hasTxt(string $name): bool
|
||||
// {
|
||||
// $out = @shell_exec("dig +short TXT ".escapeshellarg($name)." 2>/dev/null");
|
||||
// return is_string($out) && trim($out) !== '';
|
||||
// }
|
||||
// protected function hasTlsa(string $name): bool
|
||||
// {
|
||||
// $out = @shell_exec("dig +short TLSA ".escapeshellarg($name)." 2>/dev/null");
|
||||
// return is_string($out) && trim($out) !== '';
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
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');
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,964 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Livewire\Ui\Security;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Fail2BanCard extends Component
|
||||
{
|
||||
public bool $available = true;
|
||||
public bool $permDenied = false;
|
||||
public bool $error = false;
|
||||
public int $activeBans = 0;
|
||||
public array $topIps = []; // [['ip'=>'1.2.3.4','count'=>12],...]
|
||||
public array $jails = [];
|
||||
|
||||
public function mount(): void { $this->load(); }
|
||||
public function render() { return view('livewire.ui.security.fail2-ban-card'); }
|
||||
public function refresh(): void { $this->load(true); }
|
||||
|
||||
protected function load(bool $force=false): void
|
||||
public function mount(): void
|
||||
{
|
||||
$status = @shell_exec('fail2ban-client status 2>/dev/null') ?? '';
|
||||
$bans = preg_match('/Currently banned:\s+(\d+)/i', $status, $m) ? (int)$m[1] : 0;
|
||||
$this->activeBans = $bans;
|
||||
$this->load();
|
||||
}
|
||||
|
||||
// quick & rough: last 1000 lines auth/mail logs → top IPs
|
||||
$log = @shell_exec('tail -n 1000 /var/log/auth.log /var/log/mail.log 2>/dev/null | grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" | sort | uniq -c | sort -nr | head -5');
|
||||
$rows = [];
|
||||
if ($log) {
|
||||
foreach (preg_split('/\R+/', trim($log)) as $l) {
|
||||
if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) {
|
||||
$rows[] = ['ip'=>$m[2],'count'=>(int)$m[1]];
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.security.fail2-ban-card');
|
||||
}
|
||||
|
||||
#[On('f2b:refresh-banlist')]
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->load(true);
|
||||
}
|
||||
|
||||
public function openDetails(string $jail): void
|
||||
{
|
||||
$this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
|
||||
}
|
||||
|
||||
/* ---------------- intern ---------------- */
|
||||
|
||||
protected function load(bool $force = false): void
|
||||
{
|
||||
$this->available = $this->permDenied = $this->error = false;
|
||||
$this->activeBans = 0;
|
||||
$this->jails = [];
|
||||
|
||||
$bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||
if ($bin === '') {
|
||||
$this->available = false;
|
||||
return;
|
||||
}
|
||||
$this->topIps = $rows;
|
||||
$this->available = true;
|
||||
|
||||
[, $ping] = $this->f2b('ping');
|
||||
if ($this->looksDenied($ping)) {
|
||||
$this->permDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
[, $status] = $this->f2b('status');
|
||||
if ($this->looksDenied($status)) {
|
||||
$this->permDenied = true;
|
||||
return;
|
||||
}
|
||||
if (!preg_match('/Jail list:\s*(.+)$/mi', $status, $mm)) {
|
||||
$this->error = true;
|
||||
Log::warning('Fail2BanCard: unexpected status output', ['status' => $status]);
|
||||
return;
|
||||
}
|
||||
|
||||
$jails = array_filter(array_map('trim', preg_split('/\s*,\s*/', $mm[1] ?? '')));
|
||||
$sum = 0;
|
||||
$rows = [];
|
||||
|
||||
foreach ($jails as $j) {
|
||||
$jEsc = escapeshellarg($j);
|
||||
[, $s] = $this->f2b("status {$jEsc}");
|
||||
if ($this->looksDenied($s)) {
|
||||
$this->permDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
$bantime = $this->getBantime($j);
|
||||
|
||||
$rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime, 'ips' => []];
|
||||
$sum += $banned;
|
||||
}
|
||||
|
||||
$this->activeBans = $sum;
|
||||
$this->jails = $rows;
|
||||
}
|
||||
|
||||
private function f2b(string $args): array
|
||||
{
|
||||
$sudo = $this->bin('sudo');
|
||||
$f2b = $this->bin('fail2ban-client');
|
||||
$cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
|
||||
$out = (string)@shell_exec($cmd);
|
||||
|
||||
$ok = stripos($out, 'Status') !== false
|
||||
|| stripos($out, 'Jail list') !== false
|
||||
|| stripos($out, 'pong') !== false;
|
||||
|
||||
return [$ok, $out];
|
||||
}
|
||||
|
||||
private function getBantime(string $jail): int
|
||||
{
|
||||
[, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||
if ($this->looksDenied($out)) {
|
||||
$this->permDenied = true;
|
||||
return 600;
|
||||
}
|
||||
if (preg_match('/-?\d+/', trim($out), $m)) return (int)$m[0];
|
||||
return 600;
|
||||
}
|
||||
|
||||
private function looksDenied(string $out): bool
|
||||
{
|
||||
return (bool)preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out);
|
||||
}
|
||||
|
||||
private function firstMatch(string $pattern, string $haystack): ?string
|
||||
{
|
||||
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
}
|
||||
|
||||
private function bin(string $name): string
|
||||
{
|
||||
$p = trim((string)@shell_exec("command -v " . escapeshellarg($name) . " 2>/dev/null"));
|
||||
return $p !== '' ? $p : $name;
|
||||
}
|
||||
}
|
||||
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Illuminate\Support\Facades\Log;
|
||||
//use Livewire\Attributes\On;
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class Fail2BanCard extends Component
|
||||
//{
|
||||
// public bool $available = true; // fail2ban-client vorhanden?
|
||||
// public bool $permDenied = false; // sudo / Socket-Rechte fehlen?
|
||||
// public bool $error = false; // anderer Fehler (Output unerwartet)
|
||||
// public int $activeBans = 0;
|
||||
// public array $jails = []; // [['name','banned','bantime','ips'=>[...]]]
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.fail2-ban-card');
|
||||
// }
|
||||
//
|
||||
// #[On('f2b:refresh-banlist')]
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// public function openDetails(string $jail): void
|
||||
// {
|
||||
// // wire-elements/modal (v2): Event-Namen + Component + Params
|
||||
// $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
|
||||
// }
|
||||
//
|
||||
// /* ------------------- intern ------------------- */
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// $this->available = true;
|
||||
// $this->permDenied = false;
|
||||
// $this->error = false;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
//
|
||||
// // existiert fail2ban-client?
|
||||
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||
// if ($bin === '') {
|
||||
// $this->available = false;
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Rechte / Erreichbarkeit
|
||||
// [, $ping] = $this->f2b('ping');
|
||||
// if ($this->looksDenied($ping)) {
|
||||
// $this->permDenied = true;
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Jails lesen
|
||||
// [, $status] = $this->f2b('status');
|
||||
// if ($this->looksDenied($status)) {
|
||||
// $this->permDenied = true;
|
||||
// return;
|
||||
// }
|
||||
// if (!preg_match('/Jail list:\s*(.+)$/mi', $status, $mm)) {
|
||||
// // etwas stimmt nicht – loggen und „error“ zeigen
|
||||
// $this->error = true;
|
||||
// Log::warning('Fail2BanCard: unexpected status output', ['status' => $status]);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// $jails = array_filter(array_map('trim', preg_split('/\s*,\s*/', $mm[1] ?? '')));
|
||||
// $sum = 0;
|
||||
// $rows = [];
|
||||
//
|
||||
// foreach ($jails as $j) {
|
||||
// $jEsc = escapeshellarg($j);
|
||||
// [, $s] = $this->f2b("status {$jEsc}");
|
||||
// if ($this->looksDenied($s)) {
|
||||
// $this->permDenied = true;
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
// $bantime = $this->getBantime($j);
|
||||
// $ipLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
// $ips = $ipLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipLine)))) : [];
|
||||
//
|
||||
// $rows[] = [
|
||||
// 'name' => $j,
|
||||
// 'banned' => $banned,
|
||||
// 'bantime' => $bantime,
|
||||
// // wir zeigen IPs NICHT mehr in der Card; Details sind im Modal
|
||||
// 'ips' => [],
|
||||
// ];
|
||||
// $sum += $banned;
|
||||
// }
|
||||
//
|
||||
// $this->activeBans = $sum;
|
||||
// $this->jails = $rows;
|
||||
// }
|
||||
//
|
||||
// private function f2b(string $args): array
|
||||
// {
|
||||
// $sudo = '/usr/bin/sudo';
|
||||
// $f2b = '/usr/bin/fail2ban-client';
|
||||
// $cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
|
||||
// $out = (string)@shell_exec($cmd);
|
||||
//
|
||||
// $ok = stripos($out, 'Status') !== false
|
||||
// || stripos($out, 'Jail list') !== false
|
||||
// || stripos($out, 'pong') !== false;
|
||||
//
|
||||
// return [$ok, $out];
|
||||
// }
|
||||
//
|
||||
// private function getBantime(string $jail): int
|
||||
// {
|
||||
// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||
// if ($this->looksDenied($out)) {
|
||||
// $this->permDenied = true;
|
||||
// return 600;
|
||||
// }
|
||||
// $val = trim($out);
|
||||
// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
|
||||
// return 600;
|
||||
// }
|
||||
//
|
||||
// private function looksDenied(string $out): bool
|
||||
// {
|
||||
// return preg_match('/(permission denied|not allowed to execute|a password is required)/i', $out) === 1;
|
||||
// }
|
||||
//
|
||||
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||
// {
|
||||
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
// }
|
||||
//}
|
||||
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Livewire\Attributes\On;
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class Fail2BanCard extends Component
|
||||
//{
|
||||
// public bool $available = true;
|
||||
// public bool $permDenied = false;
|
||||
// public int $activeBans = 0;
|
||||
// public array $jails = [];
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.fail2-ban-card');
|
||||
// }
|
||||
//
|
||||
// #[On('f2b:refresh-banlist')]
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// public function openDetails(string $jail): void
|
||||
// {
|
||||
// // KORREKTER DISPATCH für wire-elements/modal
|
||||
// $this->dispatch('openModal', component: 'ui.security.modal.fail2-ban-jail-modal', arguments: ['jail' => $jail]);
|
||||
// }
|
||||
//
|
||||
// /* ------------------- intern ------------------- */
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||
// if ($bin === '') {
|
||||
// $this->available = false;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Rechte prüfen
|
||||
// [$ok, $raw] = $this->f2b('ping');
|
||||
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||
// $this->available = true;
|
||||
// $this->permDenied = true;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Jail-Liste
|
||||
// [, $status] = $this->f2b('status');
|
||||
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||
//
|
||||
// $rows = [];
|
||||
// $sum = 0;
|
||||
//
|
||||
// foreach ($jails as $j) {
|
||||
// $jEsc = escapeshellarg($j);
|
||||
// [, $s] = $this->f2b("status {$jEsc}");
|
||||
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
// $bantime = $this->getBantime($j);
|
||||
// $ipListLine = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
// $ips = $ipListLine !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipListLine)))) : [];
|
||||
//
|
||||
// // Details inkl. Restzeit je IP
|
||||
// $ipDetails = $this->buildIpDetails($j, $ips, $bantime);
|
||||
//
|
||||
// $rows[] = [
|
||||
// 'name' => $j,
|
||||
// 'banned' => $banned,
|
||||
// 'bantime' => $bantime, // Sek. (-1 = permanent)
|
||||
// 'ips' => $ipDetails, // [['ip'=>..., 'remaining'=>..., 'until'=>...], ...]
|
||||
// ];
|
||||
// $sum += $banned;
|
||||
// }
|
||||
//
|
||||
// $this->available = true;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = $sum;
|
||||
// $this->jails = $rows;
|
||||
// }
|
||||
//
|
||||
// private function f2b(string $args): array
|
||||
// {
|
||||
// $sudo = '/usr/bin/sudo';
|
||||
// $f2b = '/usr/bin/fail2ban-client';
|
||||
// $cmd = "timeout 3 $sudo -n $f2b $args 2>&1";
|
||||
// $out = (string)@shell_exec($cmd);
|
||||
//
|
||||
// $ok = stripos($out, 'Status') !== false
|
||||
// || stripos($out, 'Jail list') !== false
|
||||
// || stripos($out, 'pong') !== false;
|
||||
//
|
||||
// return [$ok, $out];
|
||||
// }
|
||||
//
|
||||
// /** konfig. Bantime des Jails in Sekunden (-1 = permanent) */
|
||||
// private function getBantime(string $jail): int
|
||||
// {
|
||||
// [, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
|
||||
// $val = trim($out);
|
||||
// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
|
||||
// return 600; // konservativer Fallback
|
||||
// }
|
||||
//
|
||||
// /** Letzten Ban-Zeitpunkt (Unix-Timestamp) aus /var/log/fail2ban.log ermitteln. */
|
||||
// private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||
// {
|
||||
// $file = '/var/log/fail2ban.log';
|
||||
// if (!is_readable($file)) return null;
|
||||
//
|
||||
// // nur das Ende der Datei lesen (Performance, auch bei Rotation groß genug wählen)
|
||||
// $tailBytes = 400000; // 400 KB
|
||||
// $size = @filesize($file) ?: 0;
|
||||
// $seek = max(0, $size - $tailBytes);
|
||||
//
|
||||
// $fh = @fopen($file, 'rb');
|
||||
// if (!$fh) return null;
|
||||
// if ($seek > 0) fseek($fh, $seek);
|
||||
// $data = stream_get_contents($fh) ?: '';
|
||||
// fclose($fh);
|
||||
//
|
||||
// // Beispielzeile:
|
||||
// // 2025-10-30 22:34:20,797 fail2ban.actions [...] NOTICE [sshd] Ban 193.46.255.244
|
||||
// $j = preg_quote($jail, '/');
|
||||
// $p = preg_quote($ip, '/');
|
||||
// $pattern = '/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}),\d+.*\['.$j.'\]\s+Ban\s+'.$p.'\s*$/m';
|
||||
//
|
||||
// if (preg_match_all($pattern, $data, $m) && !empty($m[1])) {
|
||||
// $date = end($m[1]); // YYYY-MM-DD
|
||||
// $time = end($m[2]); // HH:MM:SS
|
||||
// $dt = \DateTime::createFromFormat('Y-m-d H:i:s', "$date $time", new \DateTimeZone(date_default_timezone_get()));
|
||||
// return $dt ? $dt->getTimestamp() : null;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// /** Baut Details inkl. Restzeit (Sekunden; -1 = permanent). */
|
||||
// private function buildIpDetails(string $jail, array $ips, int $bantime): array
|
||||
// {
|
||||
// $now = time();
|
||||
// $out = [];
|
||||
//
|
||||
// foreach ($ips as $ip) {
|
||||
// $banAt = $this->lastBanTimestamp($jail, $ip);
|
||||
// $remaining = null;
|
||||
// $until = null;
|
||||
//
|
||||
// if ($bantime === -1) {
|
||||
// $remaining = -1; // permanent
|
||||
// } elseif ($banAt !== null) {
|
||||
// $remaining = max(0, $bantime - ($now - $banAt));
|
||||
// $until = $remaining > 0 ? ($banAt + $bantime) : null;
|
||||
// }
|
||||
//
|
||||
// $out[] = [
|
||||
// 'ip' => $ip,
|
||||
// 'remaining' => $remaining, // -1 = permanent, null = Ban-Zeitpunkt nicht gefunden, >=0 = Sekunden
|
||||
// 'until' => $until, // Unix-Timestamp oder null
|
||||
// ];
|
||||
// }
|
||||
// return $out;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||
// {
|
||||
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class Fail2BanCard extends Component
|
||||
//{
|
||||
// public bool $available = true; // fail2ban-client vorhanden?
|
||||
// public bool $permDenied = false; // Socket/Root-Rechte fehlen?
|
||||
// public int $activeBans = 0; // Summe gebannter IPs
|
||||
// /** @var array<int,array{name:string,banned:int,bantime:int}> */
|
||||
// public array $jails = [];
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.fail2-ban-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// // Optional: öffnet später dein Detail-Modal/Tab
|
||||
// public function openDetails(string $jail): void
|
||||
// {
|
||||
// $this->dispatch('openModal', 'ui.security.modal.fail2-ban-jail-modal', ['jail' => $jail]);
|
||||
// }
|
||||
// /* ---------------- intern ---------------- */
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||
// if ($bin === '') {
|
||||
// $this->available = false;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Rechtecheck
|
||||
// [$ok, $raw] = $this->f2b('ping');
|
||||
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||
// $this->available = true;
|
||||
// $this->permDenied = true;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Jails laden
|
||||
// [, $status] = $this->f2b('status');
|
||||
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||
//
|
||||
// $rows = [];
|
||||
// $sum = 0;
|
||||
//
|
||||
// foreach ($jails as $j) {
|
||||
// [, $s] = $this->f2b('status ' . escapeshellarg($j));
|
||||
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
// $bantime = $this->getBantime($j); // Sek.; -1 = permanent
|
||||
// $rows[] = ['name' => $j, 'banned' => $banned, 'bantime' => $bantime];
|
||||
// $sum += $banned;
|
||||
// }
|
||||
//
|
||||
// $this->available = true;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = $sum;
|
||||
// $this->jails = $rows;
|
||||
// }
|
||||
//
|
||||
// /** sudo + fail2ban-client ausführen; [ok, output] */
|
||||
// private function f2b(string $args): array
|
||||
// {
|
||||
// $sudo = '/usr/bin/sudo';
|
||||
// $f2b = '/usr/bin/fail2ban-client';
|
||||
// $out = (string)@shell_exec("timeout 2 $sudo -n $f2b $args 2>&1");
|
||||
// $ok = stripos($out, 'Status') !== false
|
||||
// || stripos($out, 'Jail list') !== false
|
||||
// || stripos($out, 'pong') !== false;
|
||||
// return [$ok, $out];
|
||||
// }
|
||||
//
|
||||
// private function getBantime(string $jail): int
|
||||
// {
|
||||
// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||
// $val = trim($out);
|
||||
// if (preg_match('/-?\d+/', $val, $m)) return (int)$m[0];
|
||||
// return 600; // defensiver Default
|
||||
// }
|
||||
//
|
||||
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||
// {
|
||||
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class Fail2BanCard extends Component
|
||||
//{
|
||||
// public bool $available = true; // fail2ban-client vorhanden?
|
||||
// public bool $permDenied = false; // Socket/Root-Rechte fehlen?
|
||||
// public int $activeBans = 0; // Summe gebannter IPs über alle Jails
|
||||
// public array $jails = []; // [['name','banned','bantime','ips'=>[['ip','remaining','until'],...]],...]
|
||||
// public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...]
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.fail2-ban-card');
|
||||
// }
|
||||
//
|
||||
// /** Button „Neu prüfen“ */
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// /* --------------------- intern --------------------- */
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// // existiert fail2ban-client?
|
||||
// $bin = trim((string)@shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||
// if ($bin === '') {
|
||||
// $this->available = false;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// $this->topIps = [];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // ping → prüft zugleich Rechte (bei Permission-Fehler kommt Klartext)
|
||||
// [$ok, $raw] = $this->f2b('ping'); // ok == "pong" erkannt
|
||||
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||
// $this->available = true;
|
||||
// $this->permDenied = true;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// $this->topIps = $this->collectTopIps();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Jails auflisten
|
||||
// [, $status] = $this->f2b('status');
|
||||
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||
//
|
||||
// $total = 0;
|
||||
// $rows = [];
|
||||
//
|
||||
// foreach ($jails as $j) {
|
||||
// $bantimeSecs = $this->getBantime($j); // Sek., -1 = permanent
|
||||
//
|
||||
// [, $s] = $this->f2b('status ' . escapeshellarg($j));
|
||||
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||
//
|
||||
// // Restzeiten je IP bestimmen (aus /var/log/fail2ban.log)
|
||||
// $ipDetails = [];
|
||||
// foreach (array_slice($ips, 0, 50) as $ip) {
|
||||
// $banAt = $this->lastBanTimestamp($j, $ip); // Unix-Timestamp oder null
|
||||
// $remaining = null;
|
||||
// $until = null;
|
||||
//
|
||||
// if ($banAt !== null) {
|
||||
// if ((int)$bantimeSecs === -1) {
|
||||
// $remaining = -1; // permanent
|
||||
// } else {
|
||||
// $remaining = max(0, $bantimeSecs - (time() - $banAt));
|
||||
// $until = $remaining > 0 ? ($banAt + $bantimeSecs) : null;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $ipDetails[] = [
|
||||
// 'ip' => $ip,
|
||||
// 'remaining' => $remaining, // -1 = permanent, 0 = abgelaufen, >0 Sek.
|
||||
// 'until' => $until, // Unix-Timestamp oder null
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// $rows[] = [
|
||||
// 'name' => $j,
|
||||
// 'banned' => $banned,
|
||||
// 'ips' => $ipDetails,
|
||||
// 'bantime' => (int)$bantimeSecs,
|
||||
// ];
|
||||
// $total += $banned;
|
||||
// }
|
||||
//
|
||||
// $this->available = true;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = $total;
|
||||
// $this->jails = $rows;
|
||||
// $this->topIps = $this->collectTopIps();
|
||||
// }
|
||||
//
|
||||
// /** führt fail2ban-client via sudo aus; gibt [ok, output] zurück */
|
||||
// private function f2b(string $args): array
|
||||
// {
|
||||
// $sudo = '/usr/bin/sudo';
|
||||
// $f2b = '/usr/bin/fail2ban-client';
|
||||
// $cmd = "timeout 2 $sudo -n $f2b $args 2>&1";
|
||||
// $out = (string)@shell_exec($cmd);
|
||||
//
|
||||
// $ok = stripos($out, 'Status') !== false
|
||||
// || stripos($out, 'Jail list') !== false
|
||||
// || stripos($out, 'pong') !== false;
|
||||
//
|
||||
// return [$ok, $out];
|
||||
// }
|
||||
//
|
||||
// private function getBantime(string $jail): int
|
||||
// {
|
||||
// [, $out] = $this->f2b('get ' . escapeshellarg($jail) . ' bantime');
|
||||
// $val = trim($out);
|
||||
// if (preg_match('/-?\d+/', $val, $m)) {
|
||||
// return (int)$m[0];
|
||||
// }
|
||||
// return 600; // defensiver Default
|
||||
// }
|
||||
//
|
||||
// /** letzte Ban-Zeile aus /var/log/fail2ban.log → Unix-Timestamp */
|
||||
// private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||
// {
|
||||
// $cmd = "grep -F \"[{$jail}] Ban {$ip}\" /var/log/fail2ban.log 2>/dev/null | tail -n 1";
|
||||
// $line = trim((string)@shell_exec($cmd));
|
||||
// if ($line === '') return null;
|
||||
//
|
||||
// // "YYYY-MM-DD HH:MM:SS,mmm ..."
|
||||
// if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)) {
|
||||
// $ts = strtotime($m[1] . ' ' . $m[2]);
|
||||
// return $ts ?: null;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||
// {
|
||||
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
// }
|
||||
//
|
||||
// /** Top-IPs grob zählen (aus der aktuellen Jail-Liste; Fallback: Log) */
|
||||
// private function collectTopIps(): array
|
||||
// {
|
||||
// $map = [];
|
||||
// foreach ($this->jails as $jail) {
|
||||
// foreach ($jail['ips'] as $row) {
|
||||
// $ip = $row['ip'] ?? null;
|
||||
// if (!$ip) continue;
|
||||
// $map[$ip] = ($map[$ip] ?? 0) + 1;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (!empty($map)) {
|
||||
// arsort($map);
|
||||
// $out = [];
|
||||
// foreach (array_slice($map, 0, 5, true) as $ip => $count) {
|
||||
// $out[] = ['ip' => $ip, 'count' => $count];
|
||||
// }
|
||||
// return $out;
|
||||
// }
|
||||
//
|
||||
// // Fallback: aus fail2ban.log
|
||||
// $cmd = 'grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" /var/log/fail2ban.log 2>/dev/null'
|
||||
// . ' | sort | uniq -c | sort -nr | head -5';
|
||||
// $log = (string)@shell_exec($cmd);
|
||||
// $rows = [];
|
||||
// if ($log !== '') {
|
||||
// foreach (preg_split('/\R+/', trim($log)) as $l) {
|
||||
// if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) {
|
||||
// $rows[] = ['ip' => $m[2], 'count' => (int)$m[1]];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return $rows;
|
||||
// }
|
||||
//}
|
||||
|
||||
//
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class Fail2BanCard extends Component
|
||||
//{
|
||||
// public bool $available = true; // fail2ban-client vorhanden?
|
||||
// public bool $permDenied = false; // Socket/Root-Rechte fehlen?
|
||||
// public int $activeBans = 0; // Summe gebannter IPs über alle Jails
|
||||
// public array $jails = []; // [['name'=>'sshd','banned'=>2,'ips'=>['1.2.3.4',...]], ...]
|
||||
// public array $topIps = []; // [['ip'=>'x.x.x.x','count'=>N], ...]
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.fail2-ban-card');
|
||||
// }
|
||||
//
|
||||
// // Wird vom Button "Neu prüfen" genutzt
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// /* --------------------- intern --------------------- */
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// // existiert fail2ban-client?
|
||||
// $bin = trim((string) @shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
|
||||
// if ($bin === '') {
|
||||
// $this->available = false;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// $this->topIps = [];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // ping → prüft zugleich Rechte (bei Permission-Fehler kommt Klartext)
|
||||
// [$ok, $raw] = $this->f2b('ping'); // ok == "pong" erkannt
|
||||
// if (!$ok && stripos($raw, 'permission denied') !== false) {
|
||||
// $this->available = true;
|
||||
// $this->permDenied = true;
|
||||
// $this->activeBans = 0;
|
||||
// $this->jails = [];
|
||||
// $this->topIps = $this->collectTopIps();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Jails auflisten
|
||||
// [, $status] = $this->f2b('status');
|
||||
// $jailsLn = $this->firstMatch('/Jail list:\s*(.+)$/mi', $status);
|
||||
// $jails = $jailsLn ? array_filter(array_map('trim', preg_split('/\s*,\s*/', $jailsLn))) : [];
|
||||
//
|
||||
// $total = 0; $rows = [];
|
||||
//// ... in load() NACH dem Einlesen der Jail-Liste:
|
||||
// $rows = [];
|
||||
// foreach ($jails as $j) {
|
||||
// $bantimeSecs = $this->getBantime($j); // konfigurierter Wert (Sekunden, -1 = permanent)
|
||||
//
|
||||
// [, $s] = $this->f2b('status '.escapeshellarg($j));
|
||||
// $banned = (int)($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||
//
|
||||
// // Restzeiten je IP bestimmen (aus /var/log/fail2ban.log)
|
||||
// $ipDetails = [];
|
||||
// foreach (array_slice($ips, 0, 50) as $ip) {
|
||||
// $banAt = $this->lastBanTimestamp($j, $ip); // Unix-Timestamp oder null
|
||||
// $remaining = null;
|
||||
// $until = null;
|
||||
//
|
||||
// if ($banAt !== null) {
|
||||
// if ((int)$bantimeSecs === -1) {
|
||||
// $remaining = -1; // permanent
|
||||
// } else {
|
||||
// $remaining = max(0, $bantimeSecs - (time() - $banAt));
|
||||
// $until = $remaining > 0 ? ($banAt + $bantimeSecs) : null;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $ipDetails[] = [
|
||||
// 'ip' => $ip,
|
||||
// 'remaining' => $remaining, // -1 = permanent, 0 = abgelaufen, >0 Sek.
|
||||
// 'until' => $until, // Unix-Timestamp oder null
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// $rows[] = [
|
||||
// 'name' => $j,
|
||||
// 'banned' => $banned,
|
||||
// 'ips' => $ipDetails, // jetzt mit Details
|
||||
// 'bantime' => (int)$bantimeSecs,
|
||||
// ];
|
||||
// $total += $banned;
|
||||
// }
|
||||
//
|
||||
// // foreach ($jails as $j) {
|
||||
//// [, $s] = $this->f2b('status '.escapeshellarg($j));
|
||||
//// $banned = (int) ($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
|
||||
//// $ipList = $this->firstMatch('/Banned IP list:\s*(.+)$/mi', $s) ?: '';
|
||||
//// $ips = $ipList !== '' ? array_values(array_filter(array_map('trim', preg_split('/\s+/', $ipList)))) : [];
|
||||
//// $rows[] = ['name'=>$j,'banned'=>$banned,'ips'=>array_slice($ips, 0, 8)];
|
||||
//// $total += $banned;
|
||||
//// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// $this->available = true;
|
||||
// $this->permDenied = false;
|
||||
// $this->activeBans = $total;
|
||||
// $this->jails = $rows;
|
||||
// $this->topIps = $this->collectTopIps();
|
||||
// }
|
||||
//
|
||||
// /** führt fail2ban-client via sudo aus; gibt [ok, output] zurück */
|
||||
// private function f2b(string $args): array
|
||||
// {
|
||||
// $sudo = '/usr/bin/sudo';
|
||||
// $f2b = '/usr/bin/fail2ban-client';
|
||||
// $cmd = "timeout 2 $sudo -n $f2b $args 2>&1";
|
||||
// $out = (string) @shell_exec($cmd);
|
||||
//
|
||||
// $ok = stripos($out, 'Status') !== false
|
||||
// || stripos($out, 'Jail list') !== false
|
||||
// || stripos($out, 'pong') !== false;
|
||||
//
|
||||
// return [$ok, $out];
|
||||
// }
|
||||
//
|
||||
// private function getBantime(string $jail): int
|
||||
// {
|
||||
// [, $out] = $this->f2b('get '.escapeshellarg($jail).' bantime');
|
||||
// // fail2ban liefert Seconds als Zahl (oder mit Newline)
|
||||
// $val = trim($out);
|
||||
// // Fallback: manche Versionen geben nur Zahl ohne Kontext zurück,
|
||||
// // sonst aus jail.local ermitteln wäre overkill -> einfache Zahl extrahieren:
|
||||
// if (preg_match('/-?\d+/', $val, $m)) {
|
||||
// return (int)$m[0];
|
||||
// }
|
||||
// // wenn nicht ermittelbar: 600 Sekunden als conservative default
|
||||
// return 600;
|
||||
// }
|
||||
//
|
||||
// /** Sucht die letzte "Ban <IP>"-Zeile für Jail in /var/log/fail2ban.log und gibt Unix-Timestamp zurück. */
|
||||
// private function lastBanTimestamp(string $jail, string $ip): ?int
|
||||
// {
|
||||
// // Beispiel-Logzeilen:
|
||||
// // 2025-10-29 18:07:11,436 fail2ban.actions [12345]: NOTICE [sshd] Ban 1.2.3.4
|
||||
// // Wir holen die letzte passende Zeile (tail mit grep), dann parsen Datum.
|
||||
// $pattern = escapeshellarg(sprintf('\\[%s\\] Ban %s', $jail, $ip));
|
||||
// $cmd = "grep -F \"[{$jail}] Ban {$ip}\" /var/log/fail2ban.log 2>/dev/null | tail -n 1";
|
||||
// $line = (string)@shell_exec($cmd);
|
||||
// $line = trim($line);
|
||||
// if ($line === '') {
|
||||
// return null;
|
||||
// }
|
||||
// // Datumsformat am Anfang: "YYYY-MM-DD HH:MM:SS,mmm"
|
||||
// if (preg_match('/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/', $line, $m)) {
|
||||
// $ts = strtotime($m[1].' '.$m[2]);
|
||||
// return $ts ?: null;
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// private function firstMatch(string $pattern, string $haystack): ?string
|
||||
// {
|
||||
// return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
|
||||
// }
|
||||
//
|
||||
// /** Zählt die häufigsten IPs aus den letzten Fail2Ban-Logs (ban/unban Events) */
|
||||
// private function collectTopIps(): array
|
||||
// {
|
||||
// // 1. Versuch: IPs direkt aus den Jails
|
||||
// $rows = [];
|
||||
// foreach ($this->jails as $jail) {
|
||||
// foreach ($jail['ips'] as $ip) {
|
||||
// $rows[$ip] = ($rows[$ip] ?? 0) + 1;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (!empty($rows)) {
|
||||
// arsort($rows);
|
||||
// return collect($rows)
|
||||
// ->map(fn($count, $ip) => ['ip' => $ip, 'count' => $count])
|
||||
// ->values()
|
||||
// ->take(5)
|
||||
// ->toArray();
|
||||
// }
|
||||
//
|
||||
// // 2. Fallback: Falls keine Jails/IPs → Logdatei
|
||||
// $cmd = 'grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" /var/log/fail2ban.log 2>/dev/null'
|
||||
// . ' | sort | uniq -c | sort -nr | head -5';
|
||||
// $log = (string) @shell_exec($cmd);
|
||||
//
|
||||
// $rows = [];
|
||||
// if ($log !== '') {
|
||||
// foreach (preg_split('/\R+/', trim($log)) as $l) {
|
||||
// if (preg_match('/^\s*(\d+)\s+(\d+\.\d+\.\d+\.\d+)/', $l, $m)) {
|
||||
// $rows[] = ['ip'=>$m[2],'count'=>(int)$m[1]];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return $rows;
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,543 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Livewire\Ui\Security;
|
||||
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
use App\Models\Fail2banSetting;
|
||||
use App\Models\Fail2banIpList;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
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
|
||||
{
|
||||
$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,
|
||||
]);
|
||||
|
||||
$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',
|
||||
]);
|
||||
|
||||
try {
|
||||
// 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: '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
|
||||
{
|
||||
$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
|
||||
{
|
||||
// zieht System + User-Whitelist
|
||||
$ips = Fail2banIpList::allWhitelistForConfig();
|
||||
$ignore = implode(' ', array_unique(array_filter($ips)));
|
||||
|
||||
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||
|
||||
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
||||
}
|
||||
|
||||
/* ---------------- Helper ---------------- */
|
||||
|
||||
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]);
|
||||
stream_get_contents($pipes[1]);
|
||||
stream_get_contents($pipes[2]);
|
||||
$code = proc_close($proc);
|
||||
|
||||
if ($code !== 0) {
|
||||
throw new \RuntimeException("tee failed writing to {$target}");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
//
|
||||
//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;
|
||||
//
|
||||
//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 writeRootFileViaTee(string $target, string $content): void
|
||||
// {
|
||||
// // Nur erlaubte Pfade (Hardening)
|
||||
// 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'], // stdin -> tee
|
||||
// 1 => ['pipe', 'w'], // stdout
|
||||
// 2 => ['pipe', 'w'], // stderr
|
||||
// ];
|
||||
//
|
||||
// $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]);
|
||||
// $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 boolToStr(bool $v): string
|
||||
// {
|
||||
// return $v ? 'true' : 'false';
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.fail2ban-settings');
|
||||
// }
|
||||
//}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\Security\Modal\Fail2BanJailModal;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Php extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.security.modal.fail2-ban-jail-modal.php');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,567 @@
|
|||
<?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 (!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
|
||||
$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') {
|
||||
// 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
|
||||
$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,
|
||||
);
|
||||
}
|
||||
|
||||
// Modal bewusst am Ende schließen (Toast bleibt sichtbar)
|
||||
$this->closeModal();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$this->dispatch('f2b:refresh');
|
||||
$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 ---------------- */
|
||||
|
||||
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 (unsichtbar in der UI)
|
||||
$ips = Fail2banIpList::allWhitelistForConfig();
|
||||
$ignore = implode(' ', array_unique(array_filter($ips)));
|
||||
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||
|
||||
$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;
|
||||
//
|
||||
//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;
|
||||
//
|
||||
//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");
|
||||
// }
|
||||
//}
|
||||
|
|
@ -1,156 +1,343 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// App\Livewire\Ui\Security\RblCard.php
|
||||
namespace App\Livewire\Ui\Security;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use App\Models\Setting;
|
||||
|
||||
class RblCard extends Component
|
||||
{
|
||||
public string $ip = '–';
|
||||
public int $hits = 0;
|
||||
public array $lists = [];
|
||||
|
||||
public string $ip = '–';
|
||||
public ?string $ipv4 = null;
|
||||
public ?string $ipv6 = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->load();
|
||||
}
|
||||
public int $hits = 0;
|
||||
public array $lists = []; // nur gelistete Zonen
|
||||
public array $meta = []; // status je Zone
|
||||
public ?string $checkedAt = null;
|
||||
public ?string $validUntil = null;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.security.rbl-card');
|
||||
}
|
||||
public function mount(): void { $this->load(); }
|
||||
public function render() { return view('livewire.ui.security.rbl-card'); }
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
Cache::forget('dash.rbl');
|
||||
// Manuelles Re-Check via Command (asynchron, damit UI nicht blockiert)
|
||||
@shell_exec('nohup php /var/www/mailwolt/artisan rbl:probe --force >/dev/null 2>&1 &');
|
||||
// Sofortige UI-Aktualisierung aus Settings (altes Ergebnis) …
|
||||
$this->load(true);
|
||||
// … und kurzer Hinweis
|
||||
$this->dispatch('toast', type:'info', title:'RBL-Prüfung gestartet', text:'Ergebnis wird aktualisiert, sobald verfügbar.', duration:2500);
|
||||
}
|
||||
|
||||
protected function load(bool $force = false): void
|
||||
{
|
||||
// 1) IPv4/IPv6 bevorzugt aus /etc/mailwolt/installer.env
|
||||
[$ip4, $ip6] = $this->resolvePublicIpsFromInstallerEnv();
|
||||
$payload = $force
|
||||
? (array) Setting::get('health.rbl', []) // direkt aus DB
|
||||
: (array) (Cache::get('health.rbl') ?: Setting::get('health.rbl', []));
|
||||
|
||||
// 2) Fallback auf .env
|
||||
$this->ipv4 = $ip4 ?: trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: '–';
|
||||
$this->ipv6 = $ip6 ?: trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: '–';
|
||||
|
||||
// 3) RBL-Ermittlung (cached)
|
||||
$data = Cache::remember('dash.rbl', $force ? 1 : 21600, function () {
|
||||
// bevorzugt eine valide IPv4 für den RBL-Check
|
||||
$candidate = $this->validIPv4($this->ipv4 ?? '') ? $this->ipv4 : null;
|
||||
|
||||
if (!$candidate) {
|
||||
$fromFile = @file_get_contents('/etc/mailwolt/public_ip') ?: '';
|
||||
$fromFile = trim($fromFile);
|
||||
if ($this->validIPv4($fromFile)) {
|
||||
$candidate = $fromFile;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$candidate) {
|
||||
// letzter Fallback – kann auf Hardened-Systemen geblockt sein
|
||||
$curl = @shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null") ?: '';
|
||||
$curl = trim($curl);
|
||||
if ($this->validIPv4($curl)) {
|
||||
$candidate = $curl;
|
||||
}
|
||||
}
|
||||
|
||||
$ip = $candidate ?: '0.0.0.0';
|
||||
$lists = $this->queryRblLists($ip);
|
||||
|
||||
return ['ip' => $ip, 'hits' => count($lists), 'lists' => $lists];
|
||||
});
|
||||
|
||||
// 4) Werte ins Component-State
|
||||
foreach ($data as $k => $v) {
|
||||
$this->$k = $v;
|
||||
}
|
||||
}
|
||||
|
||||
/** Bevorzugt Installer-ENV; gibt [ipv4, ipv6] zurück oder [null, null]. */
|
||||
private function resolvePublicIpsFromInstallerEnv(): array
|
||||
{
|
||||
$file = '/etc/mailwolt/installer.env';
|
||||
if (!is_readable($file)) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$ipv4 = null;
|
||||
$ipv6 = null;
|
||||
|
||||
$lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
// Kommentare überspringen
|
||||
if (preg_match('/^\s*#/', $line)) {
|
||||
continue;
|
||||
}
|
||||
// KEY=VALUE (VALUE evtl. in "..." oder '...')
|
||||
if (!str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$v = trim($v, " \t\n\r\0\x0B\"'");
|
||||
|
||||
if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) {
|
||||
$ipv4 = $v;
|
||||
} elseif ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) {
|
||||
$ipv6 = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return [$ipv4, $ipv6];
|
||||
}
|
||||
|
||||
private function validIPv4(?string $ip): bool
|
||||
{
|
||||
return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
}
|
||||
|
||||
private function validIPv6(?string $ip): bool
|
||||
{
|
||||
return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft die IP gegen ein paar gängige RBLs.
|
||||
* Nutzt PHP-DNS (checkdnsrr), keine externen Tools.
|
||||
*
|
||||
* @return array<string> gelistete RBL-Zonen
|
||||
*/
|
||||
private function queryRblLists(string $ip): array
|
||||
{
|
||||
// Nur IPv4 prüfen (die meisten Listen hier sind v4)
|
||||
if (!$this->validIPv4($ip)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rev = implode('.', array_reverse(explode('.', $ip)));
|
||||
$sources = [
|
||||
'zen.spamhaus.org',
|
||||
'bl.spamcop.net',
|
||||
'dnsbl.sorbs.net',
|
||||
'b.barracudacentral.org',
|
||||
];
|
||||
|
||||
$listed = [];
|
||||
foreach ($sources as $zone) {
|
||||
$qname = "{$rev}.{$zone}";
|
||||
// A-Record oder TXT deuten auf Listing hin
|
||||
if (@checkdnsrr($qname . '.', 'A') || @checkdnsrr($qname . '.', 'TXT')) {
|
||||
$listed[] = $zone;
|
||||
}
|
||||
}
|
||||
|
||||
return $listed;
|
||||
$this->ip = (string)($payload['ip'] ?? '–');
|
||||
$this->ipv4 = $payload['ipv4'] ?? null;
|
||||
$this->ipv6 = $payload['ipv6'] ?? null;
|
||||
$this->hits = (int)($payload['hits'] ?? 0);
|
||||
$this->lists = (array)($payload['lists'] ?? []);
|
||||
$this->meta = (array)($payload['meta'] ?? []);
|
||||
$this->checkedAt = $payload['checked_at'] ?? null;
|
||||
$this->validUntil = $payload['valid_until'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class RblCard extends Component
|
||||
//{
|
||||
// public string $ip = '–';
|
||||
// public int $hits = 0;
|
||||
// public array $lists = [];
|
||||
//
|
||||
// public ?string $ipv4 = null;
|
||||
// public ?string $ipv6 = null;
|
||||
//
|
||||
// // Schalte registrierungspflichtige Listen (Barracuda etc.) optional zu
|
||||
// private bool $includeRegistered = false; // env('RBL_INCLUDE_REGISTERED', false) wenn du willst
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.rbl-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// Cache::forget('dash.rbl');
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// [$ip4, $ip6] = $this->resolvePublicIpsFromInstallerEnv();
|
||||
//
|
||||
// $this->ipv4 = $ip4 ?: trim((string)env('SERVER_PUBLIC_IPV4', '')) ?: '–';
|
||||
// $this->ipv6 = $ip6 ?: trim((string)env('SERVER_PUBLIC_IPV6', '')) ?: '–';
|
||||
//
|
||||
// $data = Cache::remember('dash.rbl', $force ? 1 : 6 * 3600, function () {
|
||||
// $candidate = $this->validIPv4($this->ipv4 ?? '') ? $this->ipv4 : null;
|
||||
//
|
||||
// if (!$candidate) {
|
||||
// $fromFile = trim((string)@file_get_contents('/etc/mailwolt/public_ip'));
|
||||
// if ($this->validIPv4($fromFile)) $candidate = $fromFile;
|
||||
// }
|
||||
// if (!$candidate) {
|
||||
// $curl = trim((string)@shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null"));
|
||||
// if ($this->validIPv4($curl)) $candidate = $curl;
|
||||
// }
|
||||
//
|
||||
// $ip = $candidate ?: '0.0.0.0';
|
||||
// $lists = $this->queryRblLists($ip);
|
||||
//
|
||||
// return ['ip' => $ip, 'hits' => count($lists), 'lists' => $lists];
|
||||
// });
|
||||
//
|
||||
// foreach ($data as $k => $v) $this->$k = $v;
|
||||
// }
|
||||
//
|
||||
// /** bevorzugt Installer-ENV */
|
||||
// private function resolvePublicIpsFromInstallerEnv(): array
|
||||
// {
|
||||
// $file = '/etc/mailwolt/installer.env';
|
||||
// if (!is_readable($file)) return [null, null];
|
||||
//
|
||||
// $ipv4 = $ipv6 = null;
|
||||
// $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
// foreach ($lines as $line) {
|
||||
// if (preg_match('/^\s*#/', $line) || !str_contains($line, '=')) continue;
|
||||
// [$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
// $v = trim($v, " \t\n\r\0\x0B\"'");
|
||||
// if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) $ipv4 = $v;
|
||||
// if ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) $ipv6 = $v;
|
||||
// }
|
||||
// return [$ipv4, $ipv6];
|
||||
// }
|
||||
//
|
||||
// private function validIPv4(?string $ip): bool
|
||||
// {
|
||||
// return (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
// }
|
||||
//
|
||||
// private function validIPv6(?string $ip): bool
|
||||
// {
|
||||
// return (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Prüft die IP gegen gängige **öffentliche** RBLs.
|
||||
// * @return array<string> gelistete RBL-Zonen
|
||||
// */
|
||||
// private function queryRblLists(string $ip): array
|
||||
// {
|
||||
// if (!$this->validIPv4($ip)) return [];
|
||||
//
|
||||
// $rev = implode('.', array_reverse(explode('.', $ip)));
|
||||
//
|
||||
// // nur Zonen prüfen, die es wirklich gibt
|
||||
// $zones = [
|
||||
// 'zen.spamhaus.org',
|
||||
// 'psbl.surriel.com',
|
||||
// 'dnsbl-1.uceprotect.net',
|
||||
// 'all.s5h.net',
|
||||
// ];
|
||||
// $zones = array_values(array_filter($zones, fn($z) => @checkdnsrr($z.'.','NS')));
|
||||
//
|
||||
// $listed = [];
|
||||
// foreach ($zones as $zone) {
|
||||
// $q = "{$rev}.{$zone}.";
|
||||
//
|
||||
// $a = @dns_get_record($q, DNS_A) ?: [];
|
||||
// if (!count($a)) continue;
|
||||
//
|
||||
// $ips = array_column($a, 'ip');
|
||||
//
|
||||
// // --- WICHTIG: Spamhaus "blocked" / Ratelimit ignorieren
|
||||
// if (array_intersect($ips, ['127.255.255.254','127.255.255.255'])) {
|
||||
// // optional: merk dir, dass Spamhaus blockt -> UI-Hinweis
|
||||
// $listed[] = ['zone'=>$zone, 'code'=>'blocked', 'txt'=>null];
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// $txtRecs = @dns_get_record($q, DNS_TXT) ?: [];
|
||||
// $txt = $txtRecs[0]['txt'] ?? null;
|
||||
//
|
||||
// $listed[] = ['zone'=>$zone, 'code'=>$ips[0] ?? null, 'txt'=>$txt];
|
||||
// }
|
||||
//
|
||||
// // Nur echte Treffer zurückgeben; „blocked“ separat signalisieren
|
||||
// $real = array_values(array_filter($listed, fn($e) => ($e['code'] ?? null) !== 'blocked'));
|
||||
//
|
||||
// // Falls alles nur "blocked" war, gib leere Liste zurück
|
||||
// return array_map(fn($e) => $e['zone'].($e['code'] ? " ({$e['code']})" : ''), $real);
|
||||
// }
|
||||
//}
|
||||
////declare(strict_types=1);
|
||||
//
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//
|
||||
//class RblCard extends Component
|
||||
//{
|
||||
// public string $ip = '–';
|
||||
// public int $hits = 0;
|
||||
// public array $lists = [];
|
||||
//
|
||||
// public ?string $ipv4 = null;
|
||||
// public ?string $ipv6 = null;
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.security.rbl-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// Cache::forget('dash.rbl');
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// // 1) IPv4/IPv6 bevorzugt aus /etc/mailwolt/installer.env
|
||||
// [$ip4, $ip6] = $this->resolvePublicIpsFromInstallerEnv();
|
||||
//
|
||||
// // 2) Fallback auf .env
|
||||
// $this->ipv4 = $ip4 ?: trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: '–';
|
||||
// $this->ipv6 = $ip6 ?: trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: '–';
|
||||
//
|
||||
// // 3) RBL-Ermittlung (cached)
|
||||
// $data = Cache::remember('dash.rbl', $force ? 1 : 21600, function () {
|
||||
// // bevorzugt eine valide IPv4 für den RBL-Check
|
||||
// $candidate = $this->validIPv4($this->ipv4 ?? '') ? $this->ipv4 : null;
|
||||
//
|
||||
// if (!$candidate) {
|
||||
// $fromFile = @file_get_contents('/etc/mailwolt/public_ip') ?: '';
|
||||
// $fromFile = trim($fromFile);
|
||||
// if ($this->validIPv4($fromFile)) {
|
||||
// $candidate = $fromFile;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (!$candidate) {
|
||||
// // letzter Fallback – kann auf Hardened-Systemen geblockt sein
|
||||
// $curl = @shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null") ?: '';
|
||||
// $curl = trim($curl);
|
||||
// if ($this->validIPv4($curl)) {
|
||||
// $candidate = $curl;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $ip = $candidate ?: '0.0.0.0';
|
||||
// $lists = $this->queryRblLists($ip);
|
||||
//
|
||||
// return ['ip' => $ip, 'hits' => count($lists), 'lists' => $lists];
|
||||
// });
|
||||
//
|
||||
// // 4) Werte ins Component-State
|
||||
// foreach ($data as $k => $v) {
|
||||
// $this->$k = $v;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /** Bevorzugt Installer-ENV; gibt [ipv4, ipv6] zurück oder [null, null]. */
|
||||
// private function resolvePublicIpsFromInstallerEnv(): array
|
||||
// {
|
||||
// $file = '/etc/mailwolt/installer.env';
|
||||
// if (!is_readable($file)) {
|
||||
// return [null, null];
|
||||
// }
|
||||
//
|
||||
// $ipv4 = null;
|
||||
// $ipv6 = null;
|
||||
//
|
||||
// $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||
// foreach ($lines as $line) {
|
||||
// // Kommentare überspringen
|
||||
// if (preg_match('/^\s*#/', $line)) {
|
||||
// continue;
|
||||
// }
|
||||
// // KEY=VALUE (VALUE evtl. in "..." oder '...')
|
||||
// if (!str_contains($line, '=')) {
|
||||
// continue;
|
||||
// }
|
||||
// [$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
// $v = trim($v, " \t\n\r\0\x0B\"'");
|
||||
//
|
||||
// if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) {
|
||||
// $ipv4 = $v;
|
||||
// } elseif ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) {
|
||||
// $ipv6 = $v;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return [$ipv4, $ipv6];
|
||||
// }
|
||||
//
|
||||
// private function validIPv4(?string $ip): bool
|
||||
// {
|
||||
// return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
|
||||
// }
|
||||
//
|
||||
// private function validIPv6(?string $ip): bool
|
||||
// {
|
||||
// return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Prüft die IP gegen ein paar gängige RBLs.
|
||||
// * Nutzt PHP-DNS (checkdnsrr), keine externen Tools.
|
||||
// *
|
||||
// * @return array<string> gelistete RBL-Zonen
|
||||
// */
|
||||
// private function queryRblLists(string $ip): array
|
||||
// {
|
||||
// // Nur IPv4 prüfen (die meisten Listen hier sind v4)
|
||||
// if (!$this->validIPv4($ip)) {
|
||||
// return [];
|
||||
// }
|
||||
//
|
||||
// $rev = implode('.', array_reverse(explode('.', $ip)));
|
||||
// $sources = [
|
||||
// 'zen.spamhaus.org',
|
||||
// 'bl.spamcop.net',
|
||||
// 'dnsbl.sorbs.net',
|
||||
// 'b.barracudacentral.org',
|
||||
// ];
|
||||
//
|
||||
// $listed = [];
|
||||
// foreach ($sources as $zone) {
|
||||
// $qname = "{$rev}.{$zone}";
|
||||
// // A-Record oder TXT deuten auf Listing hin
|
||||
// if (@checkdnsrr($qname . '.', 'A') || @checkdnsrr($qname . '.', 'TXT')) {
|
||||
// $listed[] = $zone;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return $listed;
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Livewire\Ui\Security;
|
|||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SpamAvCard extends Component
|
||||
{
|
||||
|
|
@ -14,30 +15,119 @@ class SpamAvCard extends Component
|
|||
public string $clamVer = '–';
|
||||
public ?string $sigUpdated = null;
|
||||
|
||||
public function mount(): void { $this->load(); }
|
||||
public function render() { return view('livewire.ui.security.spam-av-card'); }
|
||||
public function refresh(): void { $this->load(true); }
|
||||
// wie frisch müssen Zahlen mindestens sein (Sek.)
|
||||
protected int $maxAge = 300;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->load();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.security.spam-av-card');
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->load(true);
|
||||
}
|
||||
|
||||
protected function load(bool $force = false): void
|
||||
{
|
||||
$data = Cache::remember('dash.spamav', $force ? 1 : 60, function () {
|
||||
$out = trim(@shell_exec('rspamc counters 2>/dev/null') ?? '');
|
||||
// very rough counters (adapt to your setup)
|
||||
$ham = preg_match('/ham:\s*(\d+)/i', $out, $m1) ? (int)$m1[1] : 0;
|
||||
$spam = preg_match('/spam:\s*(\d+)/i', $out, $m2) ? (int)$m2[1] : 0;
|
||||
$reject = preg_match('/reject:\s*(\d+)/i', $out, $m3) ? (int)$m3[1] : 0;
|
||||
// 1) Versuche erst aus Settings (DB/Redis), optional mit Cache-Shortcuts
|
||||
$data = $force ? null : Cache::get('dash.spamav');
|
||||
|
||||
$rspamdVer = trim(@shell_exec('rspamadm version 2>/dev/null') ?? '') ?: '–';
|
||||
$clamVer = trim(@shell_exec('clamd --version 2>/dev/null || clamscan --version 2>/dev/null') ?? '') ?: '–';
|
||||
if (!$data) {
|
||||
$data = Setting::get('spamav.metrics');
|
||||
}
|
||||
|
||||
// last signatures update (freshclam log)
|
||||
$sigUpdated = null;
|
||||
$log = @shell_exec('grep -i "Database updated" /var/log/clamav/freshclam.log | tail -n1 2>/dev/null');
|
||||
if ($log) $sigUpdated = trim($log);
|
||||
// 2) Wenn keine Daten oder zu alt ⇒ neu sammeln
|
||||
$stale = !is_array($data) || (time() - (int)($data['ts'] ?? 0) > $this->maxAge);
|
||||
if ($force || $stale) {
|
||||
$data = $this->collectMetrics();
|
||||
// Persistenz: Settings (DB→Redis) + UI-Cache (kurz)
|
||||
Setting::set('spamav.metrics', $data);
|
||||
Cache::put('dash.spamav', $data, 60);
|
||||
}
|
||||
|
||||
return compact('ham','spam','reject','rspamdVer','clamVer','sigUpdated');
|
||||
});
|
||||
// 3) Auf Properties mappen
|
||||
$this->ham = (int)($data['ham'] ?? 0);
|
||||
$this->spam = (int)($data['spam'] ?? 0);
|
||||
$this->reject = (int)($data['reject'] ?? 0);
|
||||
$this->rspamdVer = (string)($data['rspamdVer'] ?? '–'); // <- wichtig
|
||||
$this->clamVer = (string)($data['clamVer'] ?? '–');
|
||||
$this->sigUpdated = $data['sigUpdated'] ?? null;
|
||||
}
|
||||
|
||||
foreach ($data as $k => $v) $this->$k = $v;
|
||||
/** Sammelt live von rspamd/clamav – robust und schnell */
|
||||
protected function collectMetrics(): array
|
||||
{
|
||||
// rspamd counters
|
||||
$out = trim(@shell_exec('rspamc counters 2>/dev/null') ?? '');
|
||||
$ham = preg_match('/\bham\s*:\s*(\d+)/i', $out, $m1) ? (int)$m1[1] : 0;
|
||||
$spam = preg_match('/\bspam\s*:\s*(\d+)/i', $out, $m2) ? (int)$m2[1] : 0;
|
||||
$reject = preg_match('/\breject\s*:\s*(\d+)/i', $out, $m3) ? (int)$m3[1] : 0;
|
||||
|
||||
// Versionen
|
||||
$clamLine = trim((string) @shell_exec('clamd --version 2>/dev/null || clamscan --version 2>/dev/null'));
|
||||
$clamVer = $clamLine !== '' ? $clamLine : '–';
|
||||
$sigUpdated = null;
|
||||
if ($clamLine && preg_match('#/([^/]+\d{4})$#', $clamLine, $m)) {
|
||||
$sigUpdated = $m[1];
|
||||
}
|
||||
|
||||
return [
|
||||
'ts' => time(),
|
||||
'ham' => $ham,
|
||||
'spam' => $spam,
|
||||
'reject' => $reject,
|
||||
'clamLine' => $clamLine,
|
||||
'clamVer' => $clamVer,
|
||||
'sigUpdated' => $sigUpdated,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//namespace App\Livewire\Ui\Security;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//
|
||||
//class SpamAvCard extends Component
|
||||
//{
|
||||
// public int $ham = 0;
|
||||
// public int $spam = 0;
|
||||
// public int $reject = 0;
|
||||
// public string $rspamdVer = '–';
|
||||
// public string $clamVer = '–';
|
||||
// public ?string $sigUpdated = null;
|
||||
//
|
||||
// public function mount(): void { $this->load(); }
|
||||
// public function render() { return view('livewire.ui.security.spam-av-card'); }
|
||||
// public function refresh(): void { $this->load(true); }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// $data = Cache::remember('dash.spamav', $force ? 1 : 60, function () {
|
||||
// $out = trim(@shell_exec('rspamc counters 2>/dev/null') ?? '');
|
||||
// // very rough counters (adapt to your setup)
|
||||
// $ham = preg_match('/ham:\s*(\d+)/i', $out, $m1) ? (int)$m1[1] : 0;
|
||||
// $spam = preg_match('/spam:\s*(\d+)/i', $out, $m2) ? (int)$m2[1] : 0;
|
||||
// $reject = preg_match('/reject:\s*(\d+)/i', $out, $m3) ? (int)$m3[1] : 0;
|
||||
//
|
||||
// $rspamdVer = trim(@shell_exec('rspamadm version 2>/dev/null') ?? '') ?: '–';
|
||||
// $clamVer = trim(@shell_exec('clamd --version 2>/dev/null || clamscan --version 2>/dev/null') ?? '') ?: '–';
|
||||
//
|
||||
// // last signatures update (freshclam log)
|
||||
// $sigUpdated = null;
|
||||
// $log = @shell_exec('grep -i "Database updated" /var/log/clamav/freshclam.log | tail -n1 2>/dev/null');
|
||||
// if ($log) $sigUpdated = trim($log);
|
||||
//
|
||||
// return compact('ham','spam','reject','rspamdVer','clamVer','sigUpdated');
|
||||
// });
|
||||
//
|
||||
// foreach ($data as $k => $v) $this->$k = $v;
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -3,36 +3,748 @@
|
|||
namespace App\Livewire\Ui\System;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class BackupStatusCard extends Component
|
||||
{
|
||||
public ?string $lastAt = null;
|
||||
public ?string $lastSize = null;
|
||||
public ?string $lastDuration = null;
|
||||
// Anzeige-Felder (nur Ausgabe im Blade)
|
||||
public ?string $lastAt = null; // "27.10.2025 18:27:35"
|
||||
public ?string $lastSize = null; // "93.0 MB"
|
||||
public ?string $lastDuration = null; // "11s" / "2m 03s"
|
||||
public ?bool $ok = null;
|
||||
|
||||
public function mount(): void { $this->load(); }
|
||||
public function render() { return view('livewire.ui.system.backup-status-card'); }
|
||||
public function refresh(): void { $this->load(true); }
|
||||
// Progress (nur wenn state=running)
|
||||
public bool $running = false;
|
||||
public int $percent = 0;
|
||||
public ?string $step = null; // z.B. "compress"
|
||||
|
||||
protected string $statusFile = '/var/lib/mailwolt/backup.status';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->load();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.backup-status-card');
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->load(true);
|
||||
}
|
||||
|
||||
public function runNow(): void
|
||||
{
|
||||
@shell_exec('nohup /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
$this->dispatch('toast', type:'info', title:'Backup gestartet');
|
||||
// asynchron starten (sudoers vorausgesetzt)
|
||||
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
|
||||
// Sofort UI auf „läuft“ setzen – Poll holt Echtstatus
|
||||
$this->running = true;
|
||||
$this->percent = 1;
|
||||
$this->step = 'start';
|
||||
}
|
||||
|
||||
protected function load(bool $force=false): void
|
||||
private function load(bool $force = false): void
|
||||
{
|
||||
// Example: parse a tiny status file your backup script writes.
|
||||
$f = '/var/lib/mailwolt/backup.status';
|
||||
if (is_file($f)) {
|
||||
$lines = @file($f, FILE_IGNORE_NEW_LINES) ?: [];
|
||||
foreach ($lines as $ln) {
|
||||
if (str_starts_with($ln,'time=')) $this->lastAt = substr($ln,5);
|
||||
if (str_starts_with($ln,'size=')) $this->lastSize = substr($ln,5);
|
||||
if (str_starts_with($ln,'dur=')) $this->lastDuration = substr($ln,4);
|
||||
if (str_starts_with($ln,'ok=')) $this->ok = (substr($ln,3) === '1');
|
||||
}
|
||||
$state = $this->readStatus();
|
||||
|
||||
// Progress
|
||||
$this->running = ($state['state'] ?? null) === 'running';
|
||||
$this->percent = (int)($state['percent'] ?? 0);
|
||||
$this->step = $state['step'] ?? null;
|
||||
|
||||
// Abschlusswerte
|
||||
$fin = $state['finished_at'] ?? $state['start_at'] ?? null;
|
||||
$this->lastAt = $fin ? $this->fmtDate($fin) : null;
|
||||
|
||||
$sizeB = isset($state['size']) ? (int)$state['size'] : null;
|
||||
$this->lastSize = $sizeB !== null ? $this->fmtBytes($sizeB) : null;
|
||||
|
||||
$durS = isset($state['duration']) ? (int)$state['duration'] : null;
|
||||
$this->lastDuration = $durS !== null ? $this->fmtDuration($durS) : null;
|
||||
|
||||
$this->ok = isset($state['ok']) ? ((string)$state['ok'] === '1') : null;
|
||||
|
||||
// Wenn fertig → Balken aus
|
||||
if (!$this->running) {
|
||||
$this->percent = 0;
|
||||
$this->step = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function readStatus(): array
|
||||
{
|
||||
if (!is_readable($this->statusFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
|
||||
if (!str_contains($line, '=')) continue;
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
// nur erwartete Keys
|
||||
if (in_array($k, ['state', 'pid', 'start_at', 'finished_at', 'step', 'percent', 'size', 'duration', 'ok'], true)) {
|
||||
$out[$k] = $v;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function fmtDate(string $iso): string
|
||||
{
|
||||
// in App-Zeitzone anzeigen
|
||||
$tz = config('app.timezone', 'UTC');
|
||||
try {
|
||||
return Carbon::parse($iso)->setTimezone($tz)->format('d.m.Y H:i:s');
|
||||
} catch (\Throwable) {
|
||||
return $iso;
|
||||
}
|
||||
}
|
||||
|
||||
private function fmtBytes(int $bytes): string
|
||||
{
|
||||
$u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$i = 0;
|
||||
$n = (float)$bytes;
|
||||
while ($n >= 1024 && $i < count($u) - 1) {
|
||||
$n /= 1024;
|
||||
$i++;
|
||||
}
|
||||
return number_format($n, ($i <= 1 ? 0 : 1), ',', '.') . ' ' . $u[$i];
|
||||
}
|
||||
|
||||
private function fmtDuration(int $sec): string
|
||||
{
|
||||
if ($sec < 60) return $sec . 's';
|
||||
$m = intdiv($sec, 60);
|
||||
$s = $sec % 60;
|
||||
return sprintf('%dm %02ds', $m, $s);
|
||||
}
|
||||
}
|
||||
|
||||
//namespace App\Livewire\Ui\System;
|
||||
//
|
||||
//use Illuminate\Support\Str;
|
||||
//use Livewire\Component;
|
||||
//use Carbon\Carbon;
|
||||
//
|
||||
//class BackupStatusCard extends Component
|
||||
//{
|
||||
// // Anzeige-Felder (fertig formatiert)
|
||||
// public ?string $lastAt = null;
|
||||
// public ?string $lastSize = null;
|
||||
// public ?string $lastDuration = null;
|
||||
// public ?bool $ok = null;
|
||||
//
|
||||
// // Laufstatus für Progress (nur zur Sichtbarkeit)
|
||||
// public bool $running = false;
|
||||
// public int $percent = 0;
|
||||
// public string $progressText = '';
|
||||
//
|
||||
// protected string $statusFile = '/var/lib/mailwolt/backup.status';
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.system.backup-status-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// public function runNow(): void
|
||||
// {
|
||||
// // asynchron starten – sudoers wie bereits gesetzt
|
||||
// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
// // UI direkt "laufend" schalten; echte Werte kommen über Poll
|
||||
// $this->running = true;
|
||||
// $this->percent = 1;
|
||||
// $this->progressText = 'Backup gestartet …';
|
||||
// }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// $s = $this->readStatus();
|
||||
//
|
||||
// // Progress
|
||||
// $state = $s['state'] ?? null;
|
||||
// $this->running = in_array($state, ['running'], true);
|
||||
// $this->percent = (int)($s['percent'] ?? 0);
|
||||
// $step = $s['step'] ?? '';
|
||||
// $this->progressText = $this->mapStepText($step, $state);
|
||||
//
|
||||
// // Abschlusswerte
|
||||
// $finishedAt = $s['finished_at'] ?? ($state === 'done' ? ($s['start_at'] ?? null) : null);
|
||||
// $sizeBytes = isset($s['size']) ? (int)$s['size'] : null;
|
||||
// $durSec = isset($s['duration']) ? (int)$s['duration'] : null;
|
||||
// $okStr = $s['ok'] ?? null;
|
||||
//
|
||||
// $this->lastAt = $finishedAt ? $this->fmtDate($finishedAt) : null;
|
||||
// $this->lastSize = $sizeBytes !== null ? $this->fmtBytes($sizeBytes) : null;
|
||||
// $this->lastDuration = $durSec !== null ? $this->fmtDuration($durSec) : null;
|
||||
// $this->ok = $okStr !== null ? ($okStr === '1' || $okStr === 'true') : null;
|
||||
// }
|
||||
//
|
||||
// protected function readStatus(): array
|
||||
// {
|
||||
// if (!is_readable($this->statusFile)) {
|
||||
// return [];
|
||||
// }
|
||||
// $lines = @file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [];
|
||||
// $out = [];
|
||||
// foreach ($lines as $ln) {
|
||||
// if (!str_contains($ln, '=')) continue;
|
||||
// [$k, $v] = array_map('trim', explode('=', $ln, 2));
|
||||
// $out[$k] = $v;
|
||||
// }
|
||||
// return $out;
|
||||
// }
|
||||
//
|
||||
// private function mapStepText(string $step, ?string $state): string
|
||||
// {
|
||||
// if ($state === 'done') return 'Backup abgeschlossen.';
|
||||
// if ($state === 'failed') return 'Backup fehlgeschlagen.';
|
||||
// return match ($step) {
|
||||
// 'start' => 'Backup wird vorbereitet …',
|
||||
// 'mysqldump' => 'Datenbank wird gesichert …',
|
||||
// 'maildir' => 'Maildir wird gesichert …',
|
||||
// 'app' => 'Applikation wird gesichert …',
|
||||
// 'configs' => 'Konfigurationen werden gesichert …',
|
||||
// 'compress' => 'Archiv wird komprimiert …',
|
||||
// 'retention' => 'Alte Backups werden aufgeräumt …',
|
||||
// default => $step ? Str::headline($step) . ' …' : 'Backup läuft …',
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// private function fmtDate(string $iso): string
|
||||
// {
|
||||
// return Carbon::parse($iso)->timezone(config('app.timezone', 'Europe/Berlin'))->format('d.m.Y H:i:s');
|
||||
// }
|
||||
//
|
||||
// private function fmtBytes(int $b): string
|
||||
// {
|
||||
// $units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
// $i = 0;
|
||||
// $val = $b;
|
||||
// while ($val >= 1024 && $i < count($units) - 1) {
|
||||
// $val /= 1024;
|
||||
// $i++;
|
||||
// }
|
||||
// return number_format($val, $val >= 10 ? 0 : ($val >= 1 ? 1 : 0)) . ' ' . $units[$i];
|
||||
// }
|
||||
//
|
||||
// private function fmtDuration(int $s): string
|
||||
// {
|
||||
// if ($s < 60) return $s . 's';
|
||||
// $m = intdiv($s, 60);
|
||||
// $r = $s % 60;
|
||||
// if ($m < 60) return sprintf('%dm %02ds', $m, $r);
|
||||
// $h = intdiv($m, 60);
|
||||
// $m = $m % 60;
|
||||
// return sprintf('%dh %02dm', $h, $m);
|
||||
// }
|
||||
//}
|
||||
|
||||
//namespace App\Livewire\Ui\System;
|
||||
//
|
||||
//use Carbon\Carbon;
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class BackupStatusCard extends Component
|
||||
//{
|
||||
// public string $lastAt = '–';
|
||||
// public string $lastSize = '–';
|
||||
// public string $lastDuration = '–';
|
||||
// public string $statusText = 'unbekannt';
|
||||
// public string $statusColor = 'text-white/60 border-white/20 bg-white/5';
|
||||
//
|
||||
// public string $progressText = '';
|
||||
// public string $progressPercent = '0';
|
||||
// public string $progressVisibleClass = 'hidden'; // <- Sichtbarkeit
|
||||
//
|
||||
// protected string $statusFile = '/var/lib/mailwolt/backup.status';
|
||||
//
|
||||
// public function mount(): void { $this->load(); }
|
||||
// public function render() { return view('livewire.ui.system.backup-status-card'); }
|
||||
// public function refresh(): void { $this->load(true); }
|
||||
//
|
||||
// public function runNow(): void
|
||||
// {
|
||||
// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
// // UI sofort auf "läuft" setzen
|
||||
// $this->progressText = 'Vorbereitung läuft...';
|
||||
// $this->progressPercent = '1';
|
||||
// $this->progressVisibleClass = 'block';
|
||||
// }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// $kv = [];
|
||||
// if (is_file($this->statusFile)) {
|
||||
// foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
|
||||
// $p = strpos($ln, '='); if ($p !== false) $kv[substr($ln,0,$p)] = substr($ln,$p+1);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $state = $kv['state'] ?? null; // running | done | failed
|
||||
// $step = $kv['step'] ?? null;
|
||||
// $percent = isset($kv['percent']) ? (int)$kv['percent'] : 0;
|
||||
// $ok = isset($kv['ok']) ? ((int)$kv['ok'] === 1) : null;
|
||||
//
|
||||
// // Anzeigeformatierungen wie gehabt …
|
||||
// // (deine bestehenden formatBytes/formatDuration/Timezone-Logik)
|
||||
//
|
||||
// $this->progressPercent = (string)max(0, min(100, $percent));
|
||||
// $this->progressText = $this->mapStep($step);
|
||||
//
|
||||
// // Sichtbarkeit steuern – KEINE Blade-Logik nötig
|
||||
// if ($state === 'running') {
|
||||
// $this->progressVisibleClass = 'block';
|
||||
// } else {
|
||||
// // bei done/failed: Balken verstecken und auf 100% / finalen Text setzen
|
||||
// $this->progressVisibleClass = 'hidden';
|
||||
// if ($percent >= 100 || $step === 'done') {
|
||||
// $this->progressPercent = '100';
|
||||
// $this->progressText = 'Backup abgeschlossen.';
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Status-Badge (wie gehabt)
|
||||
// if ($ok === true) {
|
||||
// $this->statusText = 'erfolgreich';
|
||||
// $this->statusColor = 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10';
|
||||
// } elseif ($ok === false) {
|
||||
// $this->statusText = 'fehlgeschlagen';
|
||||
// $this->statusColor = 'text-rose-300 border-rose-400/30 bg-rose-500/10';
|
||||
// } else {
|
||||
// $this->statusText = 'unbekannt';
|
||||
// $this->statusColor = 'text-white/60 border-white/20 bg-white/5';
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private function mapStep(?string $step): string
|
||||
// {
|
||||
// return match($step) {
|
||||
// 'mysqldump' => 'Datenbank wird gesichert...',
|
||||
// 'maildir' => 'Mail-Verzeichnis wird archiviert...',
|
||||
// 'app' => 'Anwendungsdaten werden gesichert...',
|
||||
// 'configs' => 'Konfigurationen werden gesichert...',
|
||||
// 'compress' => 'Backup wird komprimiert...',
|
||||
// 'retention' => 'Alte Backups werden gelöscht...',
|
||||
// 'done' => 'Backup abgeschlossen.',
|
||||
// default => 'Vorbereitung läuft...',
|
||||
// };
|
||||
// }
|
||||
//}
|
||||
|
||||
//
|
||||
//class BackupStatusCard extends Component
|
||||
//{
|
||||
// public string $lastAt = '–';
|
||||
// public string $lastSize = '–';
|
||||
// public string $lastDuration = '–';
|
||||
// public string $statusText = 'unbekannt';
|
||||
// public string $statusColor = 'text-white/60 border-white/20 bg-white/5';
|
||||
// public string $progressText = '';
|
||||
// public string $progressPercent = '0';
|
||||
// public bool $running = false;
|
||||
// protected string $statusFile = '/var/lib/mailwolt/backup.status';
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.system.backup-status-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load(true);
|
||||
// }
|
||||
//
|
||||
// public function runNow(): void
|
||||
// {
|
||||
// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
// $this->running = true;
|
||||
// $this->progressText = 'Starte Backup...';
|
||||
// $this->progressPercent = '1';
|
||||
// }
|
||||
//
|
||||
// protected function load(bool $force = false): void
|
||||
// {
|
||||
// if (!is_file($this->statusFile)) {
|
||||
// $this->running = false;
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// $kv = [];
|
||||
// foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
|
||||
// $p = strpos($ln, '=');
|
||||
// if ($p !== false) {
|
||||
// $kv[substr($ln, 0, $p)] = substr($ln, $p + 1);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// $state = $kv['state'] ?? null;
|
||||
// $step = $kv['step'] ?? null;
|
||||
// $percent = isset($kv['percent']) ? (int)$kv['percent'] : 0;
|
||||
// $ok = isset($kv['ok']) ? ((int)$kv['ok'] === 1) : null;
|
||||
//
|
||||
// // Formatierung
|
||||
// $tz = config('app.timezone', 'Europe/Berlin');
|
||||
// $finished = $kv['finished_at'] ?? $kv['start_at'] ?? null;
|
||||
// $this->lastAt = $finished
|
||||
// ? Carbon::parse($finished)->setTimezone($tz)->format('d.m.Y H:i:s')
|
||||
// : '–';
|
||||
//
|
||||
// $this->lastSize = isset($kv['size'])
|
||||
// ? $this->formatBytes((int)$kv['size'])
|
||||
// : '–';
|
||||
//
|
||||
// $this->lastDuration = isset($kv['duration'])
|
||||
// ? $this->formatDuration((int)$kv['duration'])
|
||||
// : '–';
|
||||
//
|
||||
// // Fortschritt
|
||||
// $this->progressPercent = (string)$percent;
|
||||
// $this->progressText = $this->mapStep($step);
|
||||
//
|
||||
// // Status
|
||||
// $this->running = ($state === 'running');
|
||||
//
|
||||
// if ($ok === true) {
|
||||
// $this->statusText = 'erfolgreich';
|
||||
// $this->statusColor = 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10';
|
||||
// } elseif ($ok === false) {
|
||||
// $this->statusText = 'fehlgeschlagen';
|
||||
// $this->statusColor = 'text-rose-300 border-rose-400/30 bg-rose-500/10';
|
||||
// } else {
|
||||
// $this->statusText = 'unbekannt';
|
||||
// $this->statusColor = 'text-white/60 border-white/20 bg-white/5';
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private function formatBytes(int $b): string
|
||||
// {
|
||||
// if ($b >= 1024 * 1024 * 1024) return number_format($b / (1024 * 1024 * 1024), 1) . ' GB';
|
||||
// if ($b >= 1024 * 1024) return number_format($b / (1024 * 1024), 1) . ' MB';
|
||||
// if ($b >= 1024) return number_format($b / 1024, 0) . ' KB';
|
||||
// return $b . ' B';
|
||||
// }
|
||||
//
|
||||
// private function formatDuration(int $s): string
|
||||
// {
|
||||
// if ($s < 60) return $s . 's';
|
||||
// $m = intdiv($s, 60);
|
||||
// $r = $s % 60;
|
||||
// if ($m < 60) return sprintf('%dm %02ds', $m, $r);
|
||||
// $h = intdiv($m, 60);
|
||||
// $m = $m % 60;
|
||||
// return sprintf('%dh %02dm %02ds', $h, $m, $r);
|
||||
// }
|
||||
//
|
||||
// private function mapStep(?string $step): string
|
||||
// {
|
||||
// return match ($step) {
|
||||
// 'mysqldump' => 'Datenbank wird gesichert...',
|
||||
// 'maildir' => 'Mail-Verzeichnis wird archiviert...',
|
||||
// 'app' => 'Anwendungsdaten werden gesichert...',
|
||||
// 'configs' => 'Konfigurationen werden gesichert...',
|
||||
// 'compress' => 'Backup wird komprimiert...',
|
||||
// 'retention' => 'Alte Backups werden gelöscht...',
|
||||
// 'done' => 'Backup abgeschlossen.',
|
||||
// default => 'Vorbereitung läuft...',
|
||||
// };
|
||||
// }
|
||||
//}
|
||||
//
|
||||
////
|
||||
////
|
||||
////namespace App\Livewire\Ui\System;
|
||||
////
|
||||
////use Carbon\CarbonImmutable;
|
||||
////use Livewire\Component;
|
||||
////
|
||||
////class BackupStatusCard extends Component
|
||||
////{
|
||||
//// public ?string $lastAt = null; // finale Zeit
|
||||
//// public ?string $lastSize = null; // menschenlesbar
|
||||
//// public ?string $lastDuration = null; // menschenlesbar
|
||||
//// public ?bool $ok = null;
|
||||
////
|
||||
//// // Live-Status
|
||||
//// public bool $running = false;
|
||||
//// public ?string $step = null;
|
||||
//// public int $percent = 0;
|
||||
////
|
||||
//// public function mount(): void
|
||||
//// {
|
||||
//// $this->load();
|
||||
//// }
|
||||
////
|
||||
//// public function render()
|
||||
//// {
|
||||
//// return view('livewire.ui.system.backup-status-card');
|
||||
//// }
|
||||
////
|
||||
//// public function refresh(): void
|
||||
//// {
|
||||
//// $this->load(true);
|
||||
//// }
|
||||
////
|
||||
//// public function runNow(): void
|
||||
//// {
|
||||
//// // Script asynchron starten (sudoers muss gesetzt sein)
|
||||
//// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
//// // Sofort UI auf "läuft" stellen – Poll holt echten Status nach
|
||||
//// $this->running = true;
|
||||
//// $this->step = 'start';
|
||||
//// $this->percent = 1;
|
||||
//// }
|
||||
////
|
||||
//// protected function load(bool $force = false): void
|
||||
//// {
|
||||
//// $f = '/var/lib/mailwolt/backup.status';
|
||||
//// if (!is_file($f)) {
|
||||
//// return;
|
||||
//// }
|
||||
////
|
||||
//// $data = [];
|
||||
//// foreach (@file($f, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
|
||||
//// if (strpos($ln, '=') !== false) {
|
||||
//// [$k, $v] = explode('=', $ln, 2);
|
||||
//// $data[$k] = $v;
|
||||
//// }
|
||||
//// }
|
||||
////
|
||||
//// $state = $data['state'] ?? null;
|
||||
//// $this->running = ($state === 'running');
|
||||
////
|
||||
//// // Progress
|
||||
//// $this->step = $data['step'] ?? null;
|
||||
//// $this->percent = (int)($data['percent'] ?? 0);
|
||||
////
|
||||
//// // Finale Werte
|
||||
//// if ($state === 'done' || $state === 'failed') {
|
||||
//// $this->ok = ($data['ok'] ?? '') === '1';
|
||||
//// $ts = $data['finished_at'] ?? $data['start_at'] ?? null;
|
||||
//// $this->lastAt = $ts ? $this->fmtTs($ts) : null;
|
||||
////
|
||||
//// $bytes = (int)($data['size'] ?? 0);
|
||||
//// $this->lastSize = $bytes ? $this->fmtBytes($bytes) : null;
|
||||
////
|
||||
//// $dur = (int)($data['duration'] ?? 0);
|
||||
//// $this->lastDuration = $dur ? $this->fmtDuration($dur) : null;
|
||||
//// }
|
||||
//// }
|
||||
////
|
||||
//// protected function fmtTs(string $iso): string
|
||||
//// {
|
||||
//// return CarbonImmutable::parse($iso)->tz(config('app.timezone'))
|
||||
//// ->format('d.m.Y H:i:s');
|
||||
//// }
|
||||
////
|
||||
//// protected function fmtBytes(int $b): string
|
||||
//// {
|
||||
//// $u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
//// $i = 0;
|
||||
//// while ($b >= 1024 && $i < count($u) - 1) {
|
||||
//// $b /= 1024;
|
||||
//// $i++;
|
||||
//// }
|
||||
//// return sprintf('%.1f %s', $b, $u[$i]);
|
||||
//// }
|
||||
////
|
||||
//// protected function fmtDuration(int $s): string
|
||||
//// {
|
||||
//// if ($s < 60) return $s . 's';
|
||||
//// $m = intdiv($s, 60);
|
||||
//// $r = $s % 60;
|
||||
//// if ($m < 60) return sprintf('%dm %02ds', $m, $r);
|
||||
//// $h = intdiv($m, 60);
|
||||
//// $m %= 60;
|
||||
//// return sprintf('%dh %02dm', $h, $m);
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
//////
|
||||
//////namespace App\Livewire\Ui\System;
|
||||
//////
|
||||
//////use Carbon\CarbonImmutable;
|
||||
//////use Illuminate\Support\Str;
|
||||
//////use Livewire\Component;
|
||||
//////
|
||||
//////class BackupStatusCard extends Component
|
||||
//////{
|
||||
////// public ?string $lastAt = null; // formatierte Zeit
|
||||
////// public ?string $lastSize = null; // human readable
|
||||
////// public ?string $lastDuration = null; // human readable
|
||||
////// public ?bool $ok = null;
|
||||
//////
|
||||
////// // Laufzeit/Progress
|
||||
////// public string $state = 'idle'; // idle|running|done|error
|
||||
////// public string $step = ''; // aktueller Schritt
|
||||
////// public array $steps = [
|
||||
////// 'mysqldump' => 'Datenbank sichern',
|
||||
////// 'maildir' => 'Maildir kopieren',
|
||||
////// 'app' => 'App sichern',
|
||||
////// 'configs' => 'Configs sichern',
|
||||
////// 'archive' => 'Archiv erstellen',
|
||||
////// 'compress' => 'Komprimieren',
|
||||
////// 'retention' => 'Aufräumen',
|
||||
////// 'finish' => 'Abschluss',
|
||||
////// ];
|
||||
//////
|
||||
////// protected string $statusFile = '/var/lib/mailwolt/backup.status';
|
||||
//////
|
||||
////// public function mount(): void
|
||||
////// {
|
||||
////// $this->load(true);
|
||||
////// }
|
||||
//////
|
||||
////// public function render()
|
||||
////// {
|
||||
////// return view('livewire.ui.system.backup-status-card');
|
||||
////// }
|
||||
//////
|
||||
////// public function refresh(): void
|
||||
////// {
|
||||
////// $this->load(true);
|
||||
////// }
|
||||
//////
|
||||
////// public function runNow(): void
|
||||
////// {
|
||||
////// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
////// // Sofort in "running" gehen – Poll übernimmt dann
|
||||
////// $this->state = 'running';
|
||||
////// $this->step = 'mysqldump';
|
||||
////// }
|
||||
//////
|
||||
////// public function load(bool $force = false): void
|
||||
////// {
|
||||
////// $raw = $this->readStatus();
|
||||
////// $this->state = $raw['state'] ?? 'idle';
|
||||
////// $this->step = $raw['step'] ?? '';
|
||||
//////
|
||||
////// // Datum/Zeit hübsch
|
||||
////// if (!empty($raw['time'])) {
|
||||
////// $this->lastAt = $this->fmtTime($raw['time']);
|
||||
////// } else {
|
||||
////// $this->lastAt = null;
|
||||
////// }
|
||||
//////
|
||||
////// // Größe/Dauer hübsch
|
||||
////// $bytes = isset($raw['size_bytes']) ? (int)$raw['size_bytes'] : null;
|
||||
////// $secs = isset($raw['dur_seconds']) ? (int)$raw['dur_seconds'] : null;
|
||||
//////
|
||||
////// $this->lastSize = $bytes !== null ? $this->humanBytes($bytes) : null;
|
||||
////// $this->lastDuration = $secs !== null ? $this->humanDuration($secs) : null;
|
||||
////// $this->ok = isset($raw['ok']) ? ((string)$raw['ok'] === '1') : null;
|
||||
////// }
|
||||
//////
|
||||
////// protected function readStatus(): array
|
||||
////// {
|
||||
////// if (!is_file($this->statusFile)) return [];
|
||||
////// $out = [];
|
||||
////// foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
|
||||
////// if (!str_contains($ln, '=')) continue;
|
||||
////// [$k, $v] = array_map('trim', explode('=', $ln, 2));
|
||||
////// $out[$k] = $v;
|
||||
////// }
|
||||
//////
|
||||
////// // Backward compatibility (alte Keys)
|
||||
////// if (isset($out['size']) && !isset($out['size_bytes'])) {
|
||||
////// $out['size_bytes'] = (int)$out['size'];
|
||||
////// }
|
||||
////// if (isset($out['dur']) && !isset($out['dur_seconds'])) {
|
||||
////// $out['dur_seconds'] = (int)$out['dur'];
|
||||
////// }
|
||||
//////
|
||||
////// return $out;
|
||||
////// }
|
||||
//////
|
||||
////// protected function fmtTime(string $iso): string
|
||||
////// {
|
||||
////// try {
|
||||
////// $tz = config('app.timezone', 'UTC');
|
||||
////// // ISO aus Script ist idealerweise UTC (Z)
|
||||
////// $dt = CarbonImmutable::parse($iso)->timezone($tz);
|
||||
////// // z.B. 27.10.2025, 16:48:02 (CET)
|
||||
////// return $dt->isoFormat('L, LTS') . ' ' . $dt->format('T');
|
||||
////// } catch (\Throwable) {
|
||||
////// return $iso;
|
||||
////// }
|
||||
////// }
|
||||
//////
|
||||
////// protected function humanBytes(int $bytes): string
|
||||
////// {
|
||||
////// $units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
////// $i = 0;
|
||||
////// while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||
////// $bytes /= 1024;
|
||||
////// $i++;
|
||||
////// }
|
||||
////// return number_format($bytes, $i === 0 ? 0 : 1, ',', '.') . ' ' . $units[$i];
|
||||
////// }
|
||||
//////
|
||||
////// protected function humanDuration(int $secs): string
|
||||
////// {
|
||||
////// if ($secs < 60) return $secs . ' s';
|
||||
////// $m = intdiv($secs, 60);
|
||||
////// $s = $secs % 60;
|
||||
////// if ($m < 60) return sprintf('%d min %02d s', $m, $s);
|
||||
////// $h = intdiv($m, 60);
|
||||
////// $m = $m % 60;
|
||||
////// return sprintf('%d h %02d min', $h, $m);
|
||||
////// }
|
||||
//////}
|
||||
//////
|
||||
////////
|
||||
////////namespace App\Livewire\Ui\System;
|
||||
////////
|
||||
////////use Livewire\Component;
|
||||
////////
|
||||
////////class BackupStatusCard extends Component
|
||||
////////{
|
||||
//////// public ?string $lastAt = null;
|
||||
//////// public ?string $lastSize = null;
|
||||
//////// public ?string $lastDuration = null;
|
||||
//////// public ?bool $ok = null;
|
||||
////////
|
||||
//////// public function mount(): void { $this->load(); }
|
||||
//////// public function render() { return view('livewire.ui.system.backup-status-card'); }
|
||||
//////// public function refresh(): void { $this->load(true); }
|
||||
////////
|
||||
//////// public function runNow(): void
|
||||
//////// {
|
||||
//////// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
|
||||
////////// $this->dispatch('toast', type:'info', title:'Backup gestartet');
|
||||
//////// }
|
||||
////////
|
||||
//////// protected function load(bool $force=false): void
|
||||
//////// {
|
||||
//////// // Example: parse a tiny status file your backup script writes.
|
||||
//////// $f = '/var/lib/mailwolt/backup.status';
|
||||
//////// if (is_file($f)) {
|
||||
//////// $lines = @file($f, FILE_IGNORE_NEW_LINES) ?: [];
|
||||
//////// foreach ($lines as $ln) {
|
||||
//////// if (str_starts_with($ln,'time=')) $this->lastAt = substr($ln,5);
|
||||
//////// if (str_starts_with($ln,'size=')) $this->lastSize = substr($ln,5);
|
||||
//////// if (str_starts_with($ln,'dur=')) $this->lastDuration = substr($ln,4);
|
||||
//////// if (str_starts_with($ln,'ok=')) $this->ok = (substr($ln,3) === '1');
|
||||
//////// }
|
||||
//////// }
|
||||
//////// }
|
||||
////////}
|
||||
|
|
|
|||
|
|
@ -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'); }
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System\Modal;
|
||||
|
||||
use Livewire\Attributes\On;
|
||||
use LivewireUI\Modal\ModalComponent;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UpdateModal extends ModalComponent
|
||||
{
|
||||
public string $state = 'unknown'; // running|done|unknown
|
||||
public ?int $rc = null; // exit code
|
||||
public ?string $line = null; // letzte Logzeile hübsch
|
||||
public array $tail = []; // letzte N Logzeilen roh
|
||||
public int $percent = 0; // heuristisch (optional)
|
||||
public static bool $closingAllowed = false;
|
||||
|
||||
private const LOG = '/var/log/mailwolt-update.log';
|
||||
private const STATE_DIR = '/var/lib/mailwolt/update';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->refresh();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.modal.update-modal');
|
||||
}
|
||||
|
||||
#[On('update:modal-refresh')]
|
||||
public function refresh(): void
|
||||
{
|
||||
$st = @trim(@file_get_contents(self::STATE_DIR.'/state') ?: '');
|
||||
$rcRaw = @trim(@file_get_contents(self::STATE_DIR.'/rc') ?: '');
|
||||
|
||||
$this->state = $st ?: 'unknown';
|
||||
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||||
|
||||
// Log einlesen
|
||||
$lines = @file(self::LOG, FILE_IGNORE_NEW_LINES) ?: [];
|
||||
$this->tail = array_slice($lines, -30);
|
||||
|
||||
$last = trim($this->tail ? end($this->tail) : '');
|
||||
$last = preg_replace('/^\[\w\]\s*/', '', $last);
|
||||
$last = preg_replace('/^=+ .*? =+\s*$/', 'Update beendet', $last);
|
||||
$last = preg_replace('/^\d{4}-\d{2}-\d{2}T[^ ]+\s*::\s*/', '', $last);
|
||||
$this->line = Str::limit($last, 160);
|
||||
|
||||
// ganz simple Fortschritts-Heuristik über bekannte Meilensteine
|
||||
$text = implode("\n", $this->tail);
|
||||
$pct = 5;
|
||||
foreach ([
|
||||
'Update gestartet' => 10,
|
||||
'Composer' => 25,
|
||||
'npm ci' => 40,
|
||||
'npm run build' => 60,
|
||||
'migrate' => 75,
|
||||
'optimize' => 85,
|
||||
'Version aktualisiert' => 95,
|
||||
'Update beendet' => 100,
|
||||
] as $needle => $val) {
|
||||
if (stripos($text, $needle) !== false) { $pct = max($pct, $val); }
|
||||
}
|
||||
if ($this->state === 'done') { $pct = 100; }
|
||||
$this->percent = $pct;
|
||||
|
||||
// Auto-Close vorbereiten
|
||||
if ($this->state === 'done' && $this->rc === 0) {
|
||||
static::$closingAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static function modalMaxWidth(): string { return '2xl'; }
|
||||
public static function closeModalOnEscape(): bool { return static::$closingAllowed; }
|
||||
public static function closeModalOnClickAway(): bool { return static::$closingAllowed; }
|
||||
}
|
||||
|
|
@ -45,60 +45,56 @@ class ServicesCard extends Component
|
|||
|
||||
public function load(): void
|
||||
{
|
||||
// 1) Karten aus Config laden
|
||||
$cards = config('woltguard.cards', []);
|
||||
|
||||
if (empty($cards)) {
|
||||
// Config-Clear/-Cache synchron ausführen (kostet ~ms)
|
||||
try {
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('config:cache');
|
||||
} catch (\Throwable $e) {
|
||||
// ignoriere – UI soll trotzdem laufen
|
||||
}
|
||||
$cards = config('woltguard.cards', []);
|
||||
// 1) Cache lesen (versionierter Key)
|
||||
$raw = Cache::get(CacheVer::k('health:services'));
|
||||
|
||||
// 2) Neues Format { ts, rows } → entpacken
|
||||
if (is_array($raw) && array_key_exists('rows', $raw)) {
|
||||
$raw = $raw['rows'];
|
||||
}
|
||||
|
||||
// 2) Service-Status aus Cache (versionierter Key)
|
||||
$key = \App\Support\CacheVer::k('health:services');
|
||||
$raw = Cache::get($key, []);
|
||||
// Legacy-Key als Fallback
|
||||
if (empty($raw)) $raw = Cache::get('health:services', []);
|
||||
// 3) Fallback: Legacy-Key ODER Settings (DB/Redis) – ebenfalls entpacken
|
||||
if (empty($raw)) {
|
||||
$legacy = Cache::get('health:services', []);
|
||||
if (is_array($legacy) && array_key_exists('rows', $legacy)) {
|
||||
$raw = $legacy['rows'];
|
||||
} elseif (!empty($legacy)) {
|
||||
$raw = $legacy;
|
||||
} else {
|
||||
// persistierter Payload aus DB (Settings)
|
||||
$persist = \App\Support\Setting::get('woltguard.services', []);
|
||||
$raw = (is_array($persist) && isset($persist['rows']) && is_array($persist['rows']))
|
||||
? $persist['rows']
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
// Ab hier ist $raw eine Liste von {name, ok}
|
||||
$cached = collect($raw)->keyBy('name');
|
||||
|
||||
$rows = [];
|
||||
$ok = 0;
|
||||
$rows = [];
|
||||
foreach ($cards as $key => $card) {
|
||||
$isOk = (bool) ($cached->get($key)['ok'] ?? false);
|
||||
|
||||
foreach ($cards as $name => $card) {
|
||||
$isOk = (bool) ($cached->get($name)['ok'] ?? false);
|
||||
|
||||
// 3) Wenn Cache leer → aktiv prüfen
|
||||
// Nur wenn wirklich gar nichts im Cache ist, aktiv prüfen
|
||||
if ($cached->isEmpty()) {
|
||||
foreach ($card['sources'] as $src) {
|
||||
if ($this->check($src)) { $isOk = true; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if ($isOk) $ok++;
|
||||
|
||||
$rows[] = [
|
||||
'name' => $name, // <— wichtig, damit der Cache keyBy('name') später klappt
|
||||
'label' => $card['label'] ?? $name,
|
||||
'hint' => $card['hint'] ?? null,
|
||||
'label' => $card['label'],
|
||||
'hint' => $card['hint'],
|
||||
'ok' => $isOk,
|
||||
];
|
||||
}
|
||||
|
||||
// 4) Wenn wir aktiv geprüft haben (Cache war leer) → Cache auffüllen
|
||||
if ($cached->isEmpty() && !empty($rows)) {
|
||||
Cache::put($key, $rows, 600); // 10 Minuten
|
||||
Cache::forget('health:services'); // Legacy aufräumen
|
||||
}
|
||||
|
||||
// 5) Kopfzahlen + UI-Daten
|
||||
// Zähler / Badge / Compact wie vorher…
|
||||
$this->totalCount = count($rows);
|
||||
$this->okCount = $ok;
|
||||
$this->okCount = collect($rows)->where('ok', true)->count();
|
||||
$this->guardOk = $this->totalCount > 0 && $this->okCount === $this->totalCount;
|
||||
|
||||
[$this->badgeText, $this->badgeClass, $this->badgeIcon] =
|
||||
|
|
@ -106,18 +102,93 @@ class ServicesCard extends Component
|
|||
? ['alle Dienste OK', 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30', 'ph ph-check-circle']
|
||||
: ["{$this->okCount}/{$this->totalCount} aktiv", 'text-amber-200 bg-amber-500/10 border-amber-400/30', 'ph ph-warning'];
|
||||
|
||||
$this->servicesCompact = collect($rows)->map(fn($r) => [
|
||||
'label' => $r['label'],
|
||||
'hint' => $r['hint'],
|
||||
'ok' => $r['ok'],
|
||||
'dotClass' => $r['ok'] ? 'bg-emerald-400' : 'bg-rose-400',
|
||||
'pillText' => $r['ok'] ? 'Online' : 'Offline',
|
||||
'pillClass' => $r['ok']
|
||||
$this->servicesCompact = collect($rows)->map(fn($row) => [
|
||||
'label' => $row['label'],
|
||||
'hint' => $row['hint'],
|
||||
'ok' => $row['ok'],
|
||||
'dotClass' => $row['ok'] ? 'bg-emerald-400' : 'bg-rose-400',
|
||||
'pillText' => $row['ok'] ? 'Online' : 'Offline',
|
||||
'pillClass' => $row['ok']
|
||||
? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
|
||||
: 'text-rose-300 border-rose-400/30 bg-rose-500/10',
|
||||
])->all();
|
||||
}
|
||||
|
||||
// public function load(): void
|
||||
// {
|
||||
// // 1) Karten aus Config laden
|
||||
// $cards = config('woltguard.cards', []);
|
||||
//
|
||||
// if (empty($cards)) {
|
||||
// // Config-Clear/-Cache synchron ausführen (kostet ~ms)
|
||||
// try {
|
||||
// Artisan::call('config:clear');
|
||||
// Artisan::call('config:cache');
|
||||
// } catch (\Throwable $e) {
|
||||
// // ignoriere – UI soll trotzdem laufen
|
||||
// }
|
||||
// $cards = config('woltguard.cards', []);
|
||||
// }
|
||||
//
|
||||
// // 2) Service-Status aus Cache (versionierter Key)
|
||||
// $key = \App\Support\CacheVer::k('health:services');
|
||||
// $raw = Cache::get($key, []);
|
||||
// // Legacy-Key als Fallback
|
||||
// if (empty($raw)) $raw = Cache::get('health:services', []);
|
||||
//
|
||||
// $cached = collect($raw)->keyBy('name');
|
||||
//
|
||||
// $rows = [];
|
||||
// $ok = 0;
|
||||
//
|
||||
// foreach ($cards as $name => $card) {
|
||||
// $isOk = (bool) ($cached->get($name)['ok'] ?? false);
|
||||
//
|
||||
// // 3) Wenn Cache leer → aktiv prüfen
|
||||
// if ($cached->isEmpty()) {
|
||||
// foreach ($card['sources'] as $src) {
|
||||
// if ($this->check($src)) { $isOk = true; break; }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if ($isOk) $ok++;
|
||||
//
|
||||
// $rows[] = [
|
||||
// 'name' => $name, // <— wichtig, damit der Cache keyBy('name') später klappt
|
||||
// 'label' => $card['label'] ?? $name,
|
||||
// 'hint' => $card['hint'] ?? null,
|
||||
// 'ok' => $isOk,
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// // 4) Wenn wir aktiv geprüft haben (Cache war leer) → Cache auffüllen
|
||||
// if ($cached->isEmpty() && !empty($rows)) {
|
||||
// Cache::put($key, $rows, 600); // 10 Minuten
|
||||
// Cache::forget('health:services'); // Legacy aufräumen
|
||||
// }
|
||||
//
|
||||
// // 5) Kopfzahlen + UI-Daten
|
||||
// $this->totalCount = count($rows);
|
||||
// $this->okCount = $ok;
|
||||
// $this->guardOk = $this->totalCount > 0 && $this->okCount === $this->totalCount;
|
||||
//
|
||||
// [$this->badgeText, $this->badgeClass, $this->badgeIcon] =
|
||||
// $this->guardOk
|
||||
// ? ['alle Dienste OK', 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30', 'ph ph-check-circle']
|
||||
// : ["{$this->okCount}/{$this->totalCount} aktiv", 'text-amber-200 bg-amber-500/10 border-amber-400/30', 'ph ph-warning'];
|
||||
//
|
||||
// $this->servicesCompact = collect($rows)->map(fn($r) => [
|
||||
// 'label' => $r['label'],
|
||||
// 'hint' => $r['hint'],
|
||||
// 'ok' => $r['ok'],
|
||||
// 'dotClass' => $r['ok'] ? 'bg-emerald-400' : 'bg-rose-400',
|
||||
// 'pillText' => $r['ok'] ? 'Online' : 'Offline',
|
||||
// 'pillClass' => $r['ok']
|
||||
// ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
|
||||
// : 'text-rose-300 border-rose-400/30 bg-rose-500/10',
|
||||
// ])->all();
|
||||
// }
|
||||
|
||||
// public function load(): void
|
||||
// {
|
||||
// $cards = config('woltguard.cards', []);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Livewire\Ui\System;
|
||||
|
||||
use Livewire\Component;
|
||||
|
|
@ -10,17 +11,25 @@ class StorageCard extends Component
|
|||
{
|
||||
public string $target = '/';
|
||||
|
||||
// Summen für die Zahlenanzeige (GB)
|
||||
public ?int $diskTotalGb = null;
|
||||
public ?int $diskUsedGb = null; // inkl. Reserve (passt zum Donut)
|
||||
public ?int $diskFreeGb = null; // wird unten auf free_plus_reserve_gb gesetzt
|
||||
public ?int $diskUsedGb = null;
|
||||
public ?int $diskFreeGb = null;
|
||||
|
||||
// Donut
|
||||
public array $diskSegments = []; // [{angle,class}]
|
||||
public int $diskSegOuterRadius = 92;
|
||||
public int $diskInnerSize = 160;
|
||||
public array $diskCenterText = ['percent' => '–', 'label' => 'SPEICHER BELEGT'];
|
||||
|
||||
// Legende/Bar (GENAU wie dein Blade es erwartet)
|
||||
// [{label,color,gb,percent}]
|
||||
public array $barSegments = [];
|
||||
|
||||
public array $diskSegments = [];
|
||||
public int $diskSegOuterRadius = 92;
|
||||
public int $diskInnerSize = 160;
|
||||
public array $diskCenterText = ['percent' => '–', 'label' => 'SPEICHER BELEGT'];
|
||||
public ?string $measuredAt = null;
|
||||
|
||||
protected int $segCount = 48;
|
||||
protected int $segCount = 100; // Donut-Segmente
|
||||
private int $legendMinBytes = 104858; // 0,1 MiB – darunter blenden wir aus
|
||||
|
||||
public function mount(string $target = '/'): void
|
||||
{
|
||||
|
|
@ -28,7 +37,10 @@ class StorageCard extends Component
|
|||
$this->loadFromSettings();
|
||||
}
|
||||
|
||||
public function render() { return view('livewire.ui.system.storage-card'); }
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.storage-card');
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
|
|
@ -36,41 +48,429 @@ class StorageCard extends Component
|
|||
$this->loadFromSettings();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
private function humanBytes(float|int $bytes): string
|
||||
{
|
||||
$b = max(0, (float)$bytes);
|
||||
if ($b >= 1024 ** 3) return number_format($b / (1024 ** 3), 1) . ' GB';
|
||||
if ($b >= 1024 ** 2) return number_format($b / (1024 ** 2), 2) . ' MiB';
|
||||
if ($b >= 1024) return number_format($b / 1024, 0) . ' KiB';
|
||||
return (string)((int)$b) . ' B';
|
||||
}
|
||||
|
||||
protected function loadFromSettings(): void
|
||||
{
|
||||
$disk = Setting::get('health.disk', []);
|
||||
if (!is_array($disk) || empty($disk)) return;
|
||||
if (!is_array($disk) || empty($disk)) {
|
||||
$this->resetUi();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->diskTotalGb = $disk['total_gb'] ?? null;
|
||||
$this->diskUsedGb = $disk['used_gb'] ?? null;
|
||||
$this->diskFreeGb = $disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null);
|
||||
// Summen (GB) – so wie dein Blade sie zeigt
|
||||
$this->diskTotalGb = self::intOrNull($disk['total_gb'] ?? null);
|
||||
$this->diskUsedGb = self::intOrNull($disk['used_gb'] ?? null);
|
||||
$this->diskFreeGb = self::intOrNull($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null));
|
||||
|
||||
$percent = $disk['percent_used_total'] ?? null;
|
||||
$this->diskCenterText = [
|
||||
'percent' => is_numeric($percent) ? $percent.'%' : '–',
|
||||
'label' => 'SPEICHER BELEGT',
|
||||
'percent' => is_numeric($percent) ? (string)round($percent) . '%' : '–',
|
||||
'label' => 'SPEICHER BELEGT',
|
||||
];
|
||||
$this->diskSegments = $this->buildSegments($percent);
|
||||
|
||||
// Breakdown in BYTES (kommt aus StorageProbe)
|
||||
$bdBytes = $disk['breakdown_bytes'] ?? ['system' => 0, 'mails' => 0, 'backup' => 0];
|
||||
|
||||
// Total/Free in BYTES ableiten
|
||||
$totalBytes = (int)round(((int)($disk['total_gb'] ?? 0)) * 1024 ** 3);
|
||||
$freeGb = $disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? 0);
|
||||
$freeBytes = (int)round(((float)$freeGb) * 1024 ** 3);
|
||||
|
||||
// reportetes Used (Bytes)
|
||||
$usedReportedBytes = max(0, $totalBytes - $freeBytes);
|
||||
|
||||
// Summe Breakdown angleichen (Rest → System), damit Donut/Prozente stimmig sind
|
||||
$sumUsedBreakdown = max(0, (int)$bdBytes['system'] + (int)$bdBytes['mails'] + (int)$bdBytes['backup']);
|
||||
if ($usedReportedBytes > $sumUsedBreakdown) {
|
||||
$bdBytes['system'] += ($usedReportedBytes - $sumUsedBreakdown);
|
||||
}
|
||||
|
||||
// Donut färben
|
||||
$this->diskSegments = $this->buildDonutSegmentsFromBytes($bdBytes, $totalBytes, $freeBytes);
|
||||
|
||||
// Legende/Bar (GB + Prozent; KEIN „text“-Feld)
|
||||
$this->barSegments = $this->buildLegendGbFromBytes($bdBytes, $totalBytes, $freeBytes);
|
||||
|
||||
$this->measuredAt = Setting::get('health.disk_updated_at', null);
|
||||
}
|
||||
|
||||
protected function buildSegments(?int $percent): array
|
||||
protected function buildDonutSegmentsFromBytes(array $bdBytes, int $totalBytes, int $freeBytes): array
|
||||
{
|
||||
if ($totalBytes <= 0) return [];
|
||||
|
||||
$order = [
|
||||
['key' => 'system', 'class' => 'bg-emerald-400'],
|
||||
['key' => 'mails', 'class' => 'bg-rose-400'],
|
||||
['key' => 'backup', 'class' => 'bg-sky-400'],
|
||||
];
|
||||
|
||||
$counts = [];
|
||||
foreach ($order as $d) {
|
||||
$bytes = max(0, (int)($bdBytes[$d['key']] ?? 0));
|
||||
$cnt = (int)round($this->segCount * ($bytes / $totalBytes));
|
||||
if ($bytes > 0 && $cnt === 0) $cnt = 1; // min. 1 Segment, wenn vorhanden
|
||||
$counts[$d['key']] = $cnt;
|
||||
}
|
||||
|
||||
$usedCount = array_sum($counts);
|
||||
$freeCount = max(0, $this->segCount - $usedCount);
|
||||
|
||||
$segments = [];
|
||||
$active = is_int($percent) ? (int) round($this->segCount * $percent / 100) : 0;
|
||||
|
||||
$activeClass = match (true) {
|
||||
!is_int($percent) => 'bg-white/15',
|
||||
$percent >= 90 => 'bg-rose-400',
|
||||
$percent >= 70 => 'bg-amber-300',
|
||||
default => 'bg-emerald-400',
|
||||
};
|
||||
foreach ($order as $d) {
|
||||
for ($i = 0; $i < $counts[$d['key']]; $i++) {
|
||||
$segments[] = ['class' => $d['class']];
|
||||
}
|
||||
}
|
||||
for ($i = 0; $i < $freeCount; $i++) {
|
||||
$segments[] = ['class' => 'bg-white/15'];
|
||||
}
|
||||
while (count($segments) < $this->segCount) {
|
||||
$segments[] = ['class' => 'bg-white/15'];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
for ($i = 0; $i < $this->segCount; $i++) {
|
||||
$angle = (360 / $this->segCount) * $i - 90;
|
||||
$segments[] = ['angle' => $angle, 'class' => $i < $active ? $activeClass : 'bg-white/15'];
|
||||
$out[] = ['angle' => $angle, 'class' => $segments[$i]['class']];
|
||||
}
|
||||
return $segments;
|
||||
return $out;
|
||||
}
|
||||
|
||||
// protected function buildLegendGbFromBytes(array $bdBytes, int $totalBytes, int $freeBytes): array
|
||||
// {
|
||||
// $defs = [
|
||||
// ['key' => 'system', 'label' => 'System', 'class' => 'bg-emerald-400'],
|
||||
// ['key' => 'mails', 'label' => 'Mails', 'class' => 'bg-rose-400'],
|
||||
// ['key' => 'backup', 'label' => 'Backups', 'class' => 'bg-sky-400'],
|
||||
// ];
|
||||
//
|
||||
// $toPercent = function (int $bytes) use ($totalBytes): int {
|
||||
// if ($totalBytes <= 0) return 0;
|
||||
// return (int)max(0, min(100, round($bytes * 100 / $totalBytes)));
|
||||
// };
|
||||
// $toGb = fn(int $bytes) => round($bytes / (1024 ** 3), 1);
|
||||
//
|
||||
// $out = [];
|
||||
// foreach ($defs as $d) {
|
||||
// $val = max(0, (int)($bdBytes[$d['key']] ?? 0));
|
||||
// if ($val <= 0) continue; // zu klein → nicht anzeigen
|
||||
// $out[] = [
|
||||
// 'label' => $d['label'],
|
||||
// 'color' => $d['class'],
|
||||
// 'gb' => $toGb($val), // ← genau das Feld, das dein Blade nutzt
|
||||
// 'percent' => $toPercent($val),
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// // „Frei“ immer anzeigen
|
||||
// $out[] = [
|
||||
// 'label' => 'Frei',
|
||||
// 'color' => 'bg-white/20',
|
||||
// 'gb' => $toGb($freeBytes),
|
||||
// 'percent' => $toPercent($freeBytes),
|
||||
// ];
|
||||
//
|
||||
// return $out;
|
||||
// }
|
||||
|
||||
protected function buildLegendGbFromBytes(array $bdBytes, int $totalBytes, int $freeBytes): array
|
||||
{
|
||||
$defs = [
|
||||
['key' => 'system', 'label' => 'System', 'class' => 'bg-emerald-400'],
|
||||
['key' => 'mails', 'label' => 'Mails', 'class' => 'bg-rose-400'],
|
||||
['key' => 'backup', 'label' => 'Backups', 'class' => 'bg-sky-400'],
|
||||
];
|
||||
|
||||
$toPercent = function (int $bytes) use ($totalBytes): int {
|
||||
if ($totalBytes <= 0) return 0;
|
||||
return (int)max(0, min(100, round($bytes * 100 / $totalBytes)));
|
||||
};
|
||||
$toGbNum = fn(int $bytes) => round($bytes / (1024 ** 3), 1);
|
||||
|
||||
$out = [];
|
||||
|
||||
foreach ($defs as $d) {
|
||||
$val = max(0, (int)($bdBytes[$d['key']] ?? 0));
|
||||
// < 1 MiB NICHT anzeigen
|
||||
if ($val < $this->legendMinBytes) continue;
|
||||
|
||||
$out[] = [
|
||||
'label' => $d['label'],
|
||||
'color' => $d['class'],
|
||||
// numerisch in GB lassen (falls du später rechnen willst)
|
||||
'gb' => $toGbNum($val),
|
||||
'percent' => $toPercent($val),
|
||||
// FERTIG formatiert mit Einheit fürs Blade
|
||||
'human' => $this->humanBytes($val),
|
||||
];
|
||||
}
|
||||
|
||||
// „Frei“ immer anzeigen
|
||||
$out[] = [
|
||||
'label' => 'Frei',
|
||||
'color' => 'bg-white/20',
|
||||
'gb' => $toGbNum($freeBytes),
|
||||
'percent' => $toPercent($freeBytes),
|
||||
'human' => $this->humanBytes($freeBytes),
|
||||
];
|
||||
|
||||
return $out;
|
||||
}
|
||||
protected function resetUi(): void
|
||||
{
|
||||
$this->diskTotalGb = null;
|
||||
$this->diskUsedGb = null;
|
||||
$this->diskFreeGb = null;
|
||||
$this->diskSegments = [];
|
||||
$this->barSegments = [];
|
||||
$this->diskCenterText = ['percent' => '–', 'label' => 'SPEICHER BELEGT'];
|
||||
$this->measuredAt = null;
|
||||
}
|
||||
|
||||
protected static function intOrNull($v): ?int
|
||||
{
|
||||
return is_numeric($v) ? (int)round($v) : null;
|
||||
}
|
||||
}
|
||||
//
|
||||
//
|
||||
//namespace App\Livewire\Ui\System;
|
||||
//
|
||||
//use Livewire\Component;
|
||||
//use Illuminate\Support\Facades\Artisan;
|
||||
//use App\Models\Setting;
|
||||
//
|
||||
//class StorageCard extends Component
|
||||
//{
|
||||
// public string $target = '/';
|
||||
//
|
||||
// // Summen
|
||||
// public ?int $diskTotalGb = null;
|
||||
// public ?int $diskUsedGb = null;
|
||||
// public ?int $diskFreeGb = null;
|
||||
//
|
||||
// // Donut
|
||||
// public array $diskSegments = [];
|
||||
// public int $diskSegOuterRadius = 92;
|
||||
// public int $diskInnerSize = 160;
|
||||
// public array $diskCenterText = ['percent' => '–', 'label' => 'SPEICHER BELEGT'];
|
||||
//
|
||||
// private int $legendMinBytes = 1 * 1024 * 1024; // 1 MiB Schwelle
|
||||
//
|
||||
// // Stacked-Bar + Legende
|
||||
// public array $barSegments = []; // [{label,color,gb,percent}]
|
||||
// public ?string $measuredAt = null;
|
||||
//
|
||||
// protected int $segCount = 100;
|
||||
//
|
||||
// public function mount(string $target = '/'): void
|
||||
// {
|
||||
// $this->target = $target ?: '/';
|
||||
// $this->loadFromSettings();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.system.storage-card');
|
||||
// }
|
||||
//
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// Artisan::call('health:probe-disk', ['target' => $this->target]);
|
||||
// $this->loadFromSettings();
|
||||
// }
|
||||
//
|
||||
// private function humanSize(int $bytes): string
|
||||
// {
|
||||
// if ($bytes >= 1024**3) return number_format($bytes / 1024**3, 1) . ' GB';
|
||||
// if ($bytes >= 1024**2) return number_format($bytes / 1024**2, 2) . ' MiB';
|
||||
// if ($bytes >= 1024) return number_format($bytes / 1024, 0) . ' KiB';
|
||||
// return $bytes . ' B';
|
||||
// }
|
||||
//
|
||||
// protected function loadFromSettings(): void
|
||||
// {
|
||||
// $disk = Setting::get('health.disk', []);
|
||||
// if (!is_array($disk) || empty($disk)) { $this->resetUi(); return; }
|
||||
//
|
||||
// $this->diskTotalGb = self::intOrNull($disk['total_gb'] ?? null);
|
||||
// $this->diskUsedGb = self::intOrNull($disk['used_gb'] ?? null);
|
||||
// $this->diskFreeGb = self::intOrNull($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null));
|
||||
//
|
||||
// $percent = $disk['percent_used_total'] ?? null;
|
||||
// $this->diskCenterText = [
|
||||
// 'percent' => is_numeric($percent) ? (string)round($percent).'%' : '–',
|
||||
// 'label' => 'SPEICHER BELEGT',
|
||||
// ];
|
||||
//
|
||||
// // Donut farbig aus Breakdown
|
||||
// $this->diskSegments = $this->buildDonutSegmentsFromBreakdown($disk);
|
||||
//
|
||||
// // Legende unten (gleiche Farben wie Donut)
|
||||
// $total = max(1, (float)($disk['total_gb'] ?? 1));
|
||||
// $bd = $disk['breakdown'] ?? [];
|
||||
// $sys = (float)($bd['system_gb'] ?? 0);
|
||||
// $mails = (float)($bd['mails_gb'] ?? 0);
|
||||
// $backup = (float)($bd['backup_gb'] ?? 0);
|
||||
// $free = (float)($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? 0));
|
||||
// $p = fn(float $gb) => max(0, min(100, round($gb * 100 / $total)));
|
||||
//
|
||||
// $this->barSegments = [
|
||||
// ['label' => 'System', 'gb' => round($sys,1), 'percent' => $p($sys), 'color' => 'bg-emerald-400'],
|
||||
// ['label' => 'Mails', 'gb' => round($mails,1), 'percent' => $p($mails), 'color' => 'bg-rose-400'],
|
||||
// ['label' => 'Backups', 'gb' => round($backup,1),'percent' => $p($backup), 'color' => 'bg-sky-400'],
|
||||
// ['label' => 'Frei', 'gb' => round($free,1), 'percent' => $p($free), 'color' => 'bg-white/20'],
|
||||
// ];
|
||||
//
|
||||
// $this->barSegments = array_values(array_filter(
|
||||
// $this->barSegments,
|
||||
// fn($b) => ($b['gb'] ?? 0) > 0 // nur Einträge mit realer Größe
|
||||
// ));
|
||||
//
|
||||
// $this->measuredAt = Setting::get('health.disk_updated_at', null);
|
||||
// }
|
||||
//
|
||||
// // NEU: ersetzt buildSegments() + nutzt Breakdown
|
||||
// protected function buildDonutSegmentsFromBreakdown(array $disk): array
|
||||
// {
|
||||
// $total = (float)($disk['total_gb'] ?? 0);
|
||||
// if ($total <= 0) return [];
|
||||
//
|
||||
// // Breakdown lesen
|
||||
// $bd = $disk['breakdown'] ?? [];
|
||||
// $sys = (float)($bd['system_gb'] ?? 0);
|
||||
// $mails = (float)($bd['mails_gb'] ?? 0);
|
||||
// $backup = (float)($bd['backup_gb'] ?? 0);
|
||||
// $free = (float)($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? 0));
|
||||
//
|
||||
// // Robust machen: falls used aus Settings größer ist als Breakdown-Summe → auf System draufschlagen
|
||||
// $usedReported = (float)($disk['used_gb'] ?? ($total - $free));
|
||||
// $sumUsedBd = $sys + $mails + $backup;
|
||||
// if ($usedReported > $sumUsedBd && $usedReported <= $total) {
|
||||
// $sys += ($usedReported - $sumUsedBd);
|
||||
// }
|
||||
// // Grenzen
|
||||
// $sys = max(0, min($sys, $total));
|
||||
// $mails = max(0, min($mails, $total));
|
||||
// $backup = max(0, min($backup, $total));
|
||||
// $free = max(0, min($free, $total));
|
||||
//
|
||||
// // Segmente verteilen
|
||||
// $mkCount = function (float $gb) use ($total) {
|
||||
// return (int) round($this->segCount * $gb / $total);
|
||||
// };
|
||||
//
|
||||
// $segments = [];
|
||||
// $add = function (int $count, string $class) use (&$segments) {
|
||||
// for ($i = 0; $i < $count; $i++) {
|
||||
// $segments[] = ['class' => $class];
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// $add($mkCount($sys), 'bg-emerald-400'); // System
|
||||
// $add($mkCount($mails), 'bg-rose-400'); // Mails
|
||||
// $add($mkCount($backup), 'bg-sky-400'); // Backups
|
||||
//
|
||||
// // Rest = Frei (grau)
|
||||
// while (count($segments) < $this->segCount) {
|
||||
// $segments[] = ['class' => 'bg-white/15'];
|
||||
// }
|
||||
//
|
||||
// // Winkel setzen (gleich wie vorher)
|
||||
// $out = [];
|
||||
// for ($i = 0; $i < $this->segCount; $i++) {
|
||||
// $angle = (360 / $this->segCount) * $i - 90;
|
||||
// $out[] = ['angle' => $angle, 'class' => $segments[$i]['class']];
|
||||
// }
|
||||
// return $out;
|
||||
// }
|
||||
//
|
||||
//// protected function buildSegments(?int $percent): array
|
||||
//// {
|
||||
//// $segments = [];
|
||||
//// $active = is_int($percent) ? (int)round($this->segCount * $percent / 100) : 0;
|
||||
////
|
||||
//// $activeClass = match (true) {
|
||||
//// !is_int($percent) => 'bg-white/15',
|
||||
//// $percent >= 90 => 'bg-rose-400',
|
||||
//// $percent >= 70 => 'bg-amber-300',
|
||||
//// default => 'bg-emerald-400',
|
||||
//// };
|
||||
////
|
||||
//// for ($i = 0; $i < $this->segCount; $i++) {
|
||||
//// $angle = (360 / $this->segCount) * $i - 90;
|
||||
//// $segments[] = [
|
||||
//// 'angle' => $angle,
|
||||
//// 'class' => $i < $active ? $activeClass : 'bg-white/15'
|
||||
//// ];
|
||||
//// }
|
||||
//// return $segments;
|
||||
//// }
|
||||
//
|
||||
// protected function buildBar(array $disk): array
|
||||
// {
|
||||
// $total = (float)($disk['total_gb'] ?? 0);
|
||||
// if ($total <= 0) return [];
|
||||
//
|
||||
// // Breakdown lesen + normalisieren
|
||||
// [$sys, $mails, $backups] = $this->readBreakdown($disk);
|
||||
//
|
||||
// $free = max(0.0, (float)($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? 0)));
|
||||
// $used = min($total, $sys + $mails + $backups); // robust bei Messrauschen
|
||||
//
|
||||
// // Falls Breakdown kleiner ist als used_gb: Rest als “System” draufschlagen,
|
||||
// // damit die Prozent-Summe 100 ergibt.
|
||||
// $usedReported = (float)($disk['used_gb'] ?? ($total - $free));
|
||||
// if ($usedReported > 0 && $used < $usedReported) {
|
||||
// $sys += ($usedReported - $used);
|
||||
// $used = $usedReported;
|
||||
// }
|
||||
//
|
||||
// // Prozent berechnen
|
||||
// $p = fn(float $gb) => max(0, min(100, round($gb * 100 / $total)));
|
||||
//
|
||||
// return [
|
||||
// ['label' => 'System', 'gb' => round($sys, 1), 'percent' => $p($sys), 'color' => 'bg-emerald-400'],
|
||||
// ['label' => 'Mails', 'gb' => round($mails, 1), 'percent' => $p($mails), 'color' => 'bg-rose-400'],
|
||||
// ['label' => 'Backups', 'gb' => round($backups, 1), 'percent' => $p($backups), 'color' => 'bg-sky-400'],
|
||||
// ['label' => 'Frei', 'gb' => round($free, 1), 'percent' => $p($free), 'color' => 'bg-white/20'],
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// protected function readBreakdown(array $disk): array
|
||||
// {
|
||||
// // Deine Keys ggf. hier mappen
|
||||
// $bd = $disk['breakdown'] ?? [];
|
||||
// $sys = (float)($bd['system_gb'] ?? 0);
|
||||
// $mails = (float)($bd['mails_gb'] ?? 0);
|
||||
// $backup = (float)($bd['backup_gb'] ?? 0);
|
||||
//
|
||||
// return [$sys, $mails, $backup];
|
||||
// }
|
||||
//
|
||||
// protected function resetUi(): void
|
||||
// {
|
||||
// $this->diskTotalGb = null;
|
||||
// $this->diskUsedGb = null;
|
||||
// $this->diskFreeGb = null;
|
||||
// $this->diskSegments = [];
|
||||
// $this->barSegments = [];
|
||||
// $this->diskCenterText = ['percent' => '–', 'label' => 'SPEICHER BELEGT'];
|
||||
// $this->measuredAt = null;
|
||||
// }
|
||||
//
|
||||
// protected static function intOrNull($v): ?int
|
||||
// {
|
||||
// return is_numeric($v) ? (int)round($v) : null;
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class UpdateCard extends Component
|
|||
|
||||
public string $state = 'idle'; // idle | running
|
||||
public bool $running = false; // low-level Wrapper-Status
|
||||
public ?string $lowState = null; // 'running' | 'done' | '' (unbekannt)
|
||||
public ?int $rc = null; // Rückgabecode vom Wrapper
|
||||
|
||||
/* ===== UI-Properties (nur fürs Blade) ===== */
|
||||
|
|
@ -31,7 +32,6 @@ class UpdateCard extends Component
|
|||
public ?string $errorLine = null; // Fehlerzeile bei rc != 0
|
||||
|
||||
public ?string $message = null;
|
||||
/* ===== Intern ===== */
|
||||
public bool $postActionsDone = false;
|
||||
protected string $cacheStartedAtKey = 'mw.update.started_at';
|
||||
protected int $failsafeSeconds = 20 * 60;
|
||||
|
|
@ -48,10 +48,10 @@ class UpdateCard extends Component
|
|||
$this->reloadVersionsAndStatus();
|
||||
$this->recompute();
|
||||
$this->progressLine = '';
|
||||
// $this->progressLine = $this->tailUpdateLog();
|
||||
|
||||
if ($this->running) {
|
||||
$this->state = 'running';
|
||||
$this->dispatch('openModal', component: 'ui.system.modal.update-modal');
|
||||
}
|
||||
|
||||
$this->recomputeUi();
|
||||
|
|
@ -69,8 +69,7 @@ class UpdateCard extends Component
|
|||
// evtl. alte Einträge aufräumen
|
||||
Cache::forget('mailwolt.update_available');
|
||||
Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
|
||||
|
||||
// Wrapper starten (setzt /var/lib/mailwolt/update/{state,rc} und schreibt Versionen)
|
||||
$this->dispatch('openModal', component: 'ui.system.modal.update-modal');
|
||||
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-update >/dev/null 2>&1 &');
|
||||
|
||||
// Sofort ins Running gehen
|
||||
|
|
@ -85,34 +84,86 @@ class UpdateCard extends Component
|
|||
$this->recomputeUi();
|
||||
}
|
||||
|
||||
// public function pollUpdate(): void
|
||||
// {
|
||||
// // 1) aktuellen Wrapper-Status einlesen
|
||||
// $this->refreshLowLevelState();
|
||||
//
|
||||
// // 2) Failsafe
|
||||
public function pollUpdate(): void
|
||||
{
|
||||
$this->refreshLowLevelState();
|
||||
|
||||
if ($this->rc !== null) {
|
||||
$this->running = false;
|
||||
}
|
||||
|
||||
// 2) Failsafe
|
||||
$started = (int) Cache::get($this->cacheStartedAtKey, 0);
|
||||
if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) {
|
||||
$this->running = false;
|
||||
$this->lowState = 'done';
|
||||
$this->rc ??= 0;
|
||||
}
|
||||
|
||||
// Abschluss NUR wenn state=done
|
||||
if ($this->lowState === 'done') {
|
||||
Cache::forget($this->cacheStartedAtKey);
|
||||
|
||||
// kurze Gnadenzeit gegen Schreibpuffer
|
||||
usleep(300_000);
|
||||
|
||||
$this->reloadVersionsAndStatus();
|
||||
$this->recompute();
|
||||
|
||||
if ($this->rc === 0 && !$this->postActionsDone) {
|
||||
@shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &');
|
||||
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
||||
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
||||
@shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/dev/null 2>&1 &');
|
||||
@shell_exec('nohup php /var/www/mailwolt/artisan rbl:probe --force >/dev/null 2>&1 &');
|
||||
$this->postActionsDone = true;
|
||||
|
||||
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
||||
$this->progressLine = 'Update abgeschlossen: ' . $ver;
|
||||
// Optional: NICHT sofort reloaden – Nutzer entscheidet
|
||||
// $this->dispatch('reload-page', delay: 6000);
|
||||
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
||||
$this->postActionsDone = true;
|
||||
$this->errorLine = "Update fehlgeschlagen (rc={$this->rc}).";
|
||||
$this->dispatch('toast', type:'error', title:'Update fehlgeschlagen',
|
||||
text:$this->progressLine ?: 'Bitte Logs prüfen: /var/log/mailwolt-update.log',
|
||||
badge:'System', duration:0);
|
||||
}
|
||||
|
||||
$this->state = 'idle';
|
||||
}
|
||||
|
||||
$this->recomputeUi();
|
||||
|
||||
// 3) Abschluss?
|
||||
// $started = (int)Cache::get($this->cacheStartedAtKey, 0);
|
||||
// if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) {
|
||||
// $this->running = false;
|
||||
// $this->rc ??= 0; // wenn unklar, als erfolgreich werten
|
||||
// }
|
||||
//
|
||||
// // 3) Abschluss?
|
||||
|
||||
// if (!$this->running) {
|
||||
// Cache::forget($this->cacheStartedAtKey);
|
||||
//
|
||||
// usleep(500_000);
|
||||
// // Nachlauf: Versionen & Vergleich neu aufbauen
|
||||
// $this->reloadVersionsAndStatus();
|
||||
// $this->recompute();
|
||||
//
|
||||
// if ($this->current && $this->latest && version_compare($this->current, $this->latest, '>=')) {
|
||||
// $this->hasUpdate = false;
|
||||
// }
|
||||
//
|
||||
// if ($this->rc === 0 && !$this->postActionsDone) {
|
||||
// // Dienste neu starten (asynchron)
|
||||
// @shell_exec('nohup php /var/www/mailwolt/artisan mailwolt:restart-services >/dev/null 2>&1 &');
|
||||
//// @shell_exec('nohup php /var/www/mailwolt/artisan mailwolt:restart-services >/dev/null 2>&1 &');
|
||||
// @shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
||||
// @shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
||||
// @shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/dev/null 2>&1 &');
|
||||
// $this->postActionsDone = true;
|
||||
//
|
||||
// $ver = $this->displayCurrent ?? 'aktuelle Version';
|
||||
// $this->progressLine = '';
|
||||
// $this->dispatch('reload-page', delay: 5000);
|
||||
// $this->progressLine = 'Update abgeschlossen: ' . $ver;
|
||||
//// $this->dispatch('reload-page', delay: 5000);
|
||||
// $this->dispatch('toast',
|
||||
// type: 'success',
|
||||
// title: 'Update erfolgreich',
|
||||
|
|
@ -120,6 +171,9 @@ class UpdateCard extends Component
|
|||
// badge: 'System',
|
||||
// duration: 4000
|
||||
// );
|
||||
//
|
||||
// $this->dispatch('reload-page', delay: 5000);
|
||||
//
|
||||
// } elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
||||
// // Fehlerfall
|
||||
// $this->postActionsDone = true;
|
||||
|
|
@ -132,51 +186,9 @@ class UpdateCard extends Component
|
|||
// duration: 0
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// $this->state = 'idle';
|
||||
// }
|
||||
//
|
||||
// // UI bei jedem Poll neu ableiten
|
||||
// $this->recomputeUi();
|
||||
// }
|
||||
|
||||
public function pollUpdate(): void
|
||||
{
|
||||
$this->refreshLowLevelState();
|
||||
|
||||
$started = (int) Cache::get($this->cacheStartedAtKey, 0);
|
||||
if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) {
|
||||
$this->running = false;
|
||||
$this->rc ??= 0; // failsafe: als erfolgreich werten
|
||||
}
|
||||
|
||||
if (!$this->running) {
|
||||
Cache::forget($this->cacheStartedAtKey);
|
||||
|
||||
$this->reloadVersionsAndStatus();
|
||||
$this->recompute();
|
||||
|
||||
if ($this->rc === 0 && !$this->postActionsDone) {
|
||||
// Restarts DE-coupled
|
||||
@shell_exec('nohup php /var/www/mailwolt/artisan mailwolt:restart-services >/dev/null 2>&1 &');
|
||||
$this->postActionsDone = true;
|
||||
|
||||
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
||||
$this->progressLine = '';
|
||||
$this->dispatch('toast', type: 'success', title: 'Update erfolgreich',
|
||||
text: "MailWolt wurde auf {$ver} aktualisiert.", badge: 'System', duration: 4000);
|
||||
$this->dispatch('reload-page', delay: 5000);
|
||||
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
||||
$this->postActionsDone = true;
|
||||
$this->errorLine = "Update fehlgeschlagen (rc={$this->rc}).";
|
||||
$this->dispatch('toast', type: 'error', title: 'Update fehlgeschlagen',
|
||||
text: $this->progressLine ?: 'Bitte /var/log/mailwolt-update.log prüfen', badge: 'System', duration: -1);
|
||||
}
|
||||
|
||||
$this->state = 'idle';
|
||||
}
|
||||
|
||||
$this->recomputeUi();
|
||||
}
|
||||
|
||||
/* ================== Helpers ================== */
|
||||
|
|
@ -272,12 +284,19 @@ class UpdateCard extends Component
|
|||
// protected function refreshLowLevelState(): void
|
||||
// {
|
||||
// $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
|
||||
// $this->running = ($state === 'running');
|
||||
//
|
||||
// $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
|
||||
// $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||||
//
|
||||
// $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||||
// $this->running = ($this->rc === null) && ($state !== 'done');
|
||||
// $this->progressLine = $this->tailUpdateLog();
|
||||
//
|
||||
//// $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
|
||||
//// $this->running = ($state === 'running');
|
||||
////
|
||||
//// $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
|
||||
//// $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||||
////
|
||||
//// $this->progressLine = $this->tailUpdateLog();
|
||||
// }
|
||||
|
||||
protected function refreshLowLevelState(): void
|
||||
|
|
@ -285,10 +304,13 @@ class UpdateCard extends Component
|
|||
$state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
|
||||
$rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
|
||||
|
||||
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||||
$this->lowState = $state !== '' ? $state : null;
|
||||
|
||||
// läuft nur, solange KEINE rc vorliegt
|
||||
$this->running = ($state === 'running' && $this->rc === null);
|
||||
// running: solange NICHT 'done'
|
||||
$this->running = ($this->lowState !== 'done');
|
||||
|
||||
// rc erst freigeben, wenn wirklich done
|
||||
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int)$rcRaw : null;
|
||||
|
||||
$this->progressLine = $this->tailUpdateLog();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Livewire\Ui\System;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Support\CacheVer;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
|
@ -47,25 +48,30 @@ class WoltguardCard extends Component
|
|||
|
||||
protected function load(): void
|
||||
{
|
||||
// 1) Primär: versionierter Key
|
||||
$list = Cache::get(CacheVer::k('health:services'), []);
|
||||
// 2) Fallback: Legacy-Key (nur falls 1) leer)
|
||||
$list = Cache::get(CacheVer::k('health:services'));
|
||||
|
||||
if (empty($list)) {
|
||||
$list = Cache::get('health:services', []);
|
||||
// Fallback: persistierter Wert aus DB/Redis
|
||||
$list = \App\Support\Setting::get('woltguard.services', []);
|
||||
}
|
||||
|
||||
$this->services = is_array($list) ? $list : [];
|
||||
$rows = [];
|
||||
if (is_array($list)) {
|
||||
$rows = isset($list['rows']) && is_array($list['rows'])
|
||||
? $list['rows'] // neues Format
|
||||
: (isset($list[0]) && is_array($list[0]) ? $list : []); // altes Format
|
||||
}
|
||||
|
||||
$this->totalCount = count($this->services);
|
||||
$this->okCount = collect($this->services)->filter(fn($s) => (bool)($s['ok'] ?? false))->count();
|
||||
$this->services = $rows;
|
||||
$this->totalCount = count($rows);
|
||||
$this->okCount = collect($rows)->where('ok', true)->count();
|
||||
$this->downCount = $this->totalCount - $this->okCount;
|
||||
$this->guardOk = ($this->totalCount > 0) && ($this->downCount === 0);
|
||||
$this->guardOk = $this->totalCount > 0 && $this->downCount === 0;
|
||||
|
||||
$this->downServices = collect($this->services)
|
||||
$this->downServices = collect($rows)
|
||||
->filter(fn($s) => !($s['ok'] ?? false))
|
||||
->map(fn($s) => (string)($s['name'] ?? 'unbekannt'))
|
||||
->values()
|
||||
->all();
|
||||
->values()->all();
|
||||
|
||||
// Badge
|
||||
if ($this->totalCount === 0) {
|
||||
|
|
@ -80,104 +86,11 @@ class WoltguardCard extends Component
|
|||
$this->badgeIcon = 'ph ph-check-circle';
|
||||
$this->badgeClass = 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10';
|
||||
} else {
|
||||
if ($this->downCount >= 3) {
|
||||
$this->badgeText = "{$this->downCount} Dienste down";
|
||||
$this->badgeIcon = 'ph ph-x-circle';
|
||||
$this->badgeClass = 'text-rose-300 border-rose-400/30 bg-rose-500/10';
|
||||
} else {
|
||||
$this->badgeText = 'Störung erkannt';
|
||||
$this->badgeIcon = 'ph ph-warning-circle';
|
||||
$this->badgeClass = 'text-amber-300 border-amber-400/30 bg-amber-500/10';
|
||||
}
|
||||
$this->badgeText = $this->downCount >= 3 ? "{$this->downCount} Dienste down" : 'Störung erkannt';
|
||||
$this->badgeIcon = $this->downCount >= 3 ? 'ph ph-x-circle' : 'ph ph-warning-circle';
|
||||
$this->badgeClass = $this->downCount >= 3
|
||||
? 'text-rose-300 border-rose-400/30 bg-rose-500/10'
|
||||
: 'text-amber-300 border-amber-400/30 bg-amber-500/10';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//namespace App\Livewire\Ui\System;
|
||||
//
|
||||
//use Illuminate\Support\Facades\Cache;
|
||||
//use Livewire\Component;
|
||||
//
|
||||
//class WoltguardCard extends Component
|
||||
//{
|
||||
// /** Gesamter Roh-Input aus dem Health-Cache (optional für später) */
|
||||
// public array $services = [];
|
||||
//
|
||||
// /** UI: zusammengefasster Status */
|
||||
// public bool $guardOk = false; // alle Dienste OK?
|
||||
// public int $okCount = 0; // wie viele OK
|
||||
// public int $totalCount = 0; // wie viele gesamt
|
||||
// public int $downCount = 0; // wie viele down
|
||||
// public string $badgeText = 'unbekannt';
|
||||
// public string $badgeIcon = 'ph ph-question';
|
||||
// public string $badgeClass = 'text-white/70 border-white/20 bg-white/10';
|
||||
//
|
||||
// /** Optional: Liste der ausgefallenen Dienste (für Tooltip/weitere Anzeige) */
|
||||
// public array $downServices = [];
|
||||
//
|
||||
// /** Pollintervall steuern (z. B. 30s) */
|
||||
// public int $pollSeconds = 30;
|
||||
//
|
||||
// public function mount(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// public function render()
|
||||
// {
|
||||
// return view('livewire.ui.system.woltguard-card');
|
||||
// }
|
||||
//
|
||||
// /** Manuelles Refresh aus dem UI */
|
||||
// public function refresh(): void
|
||||
// {
|
||||
// $this->load();
|
||||
// }
|
||||
//
|
||||
// /* ---------------- intern ---------------- */
|
||||
//
|
||||
// protected function load(): void
|
||||
// {
|
||||
// // Erwartet: Cache::put('health:services', [['name'=>'postfix','ok'=>true], ...])
|
||||
// $list = Cache::get('health:services', []);
|
||||
// $this->services = is_array($list) ? $list : [];
|
||||
//
|
||||
// $this->totalCount = count($this->services);
|
||||
// $this->okCount = collect($this->services)->filter(fn ($s) => (bool)($s['ok'] ?? false))->count();
|
||||
// $this->downCount = $this->totalCount - $this->okCount;
|
||||
// $this->guardOk = ($this->totalCount > 0) && ($this->downCount === 0);
|
||||
//
|
||||
// // Down-Services Namen extrahieren
|
||||
// $this->downServices = collect($this->services)
|
||||
// ->filter(fn ($s) => !($s['ok'] ?? false))
|
||||
// ->map(fn ($s) => (string)($s['name'] ?? 'unbekannt'))
|
||||
// ->values()
|
||||
// ->all();
|
||||
//
|
||||
// // Badge aufbereiten (Text/Style/Icon)
|
||||
// if ($this->totalCount === 0) {
|
||||
// $this->badgeText = 'keine Daten';
|
||||
// $this->badgeIcon = 'ph ph-warning-circle';
|
||||
// $this->badgeClass = 'text-amber-300 border-amber-400/30 bg-amber-500/10';
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if ($this->guardOk) {
|
||||
// $this->badgeText = 'alle Dienste OK';
|
||||
// $this->badgeIcon = 'ph ph-check-circle';
|
||||
// $this->badgeClass = 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10';
|
||||
// } else {
|
||||
// // kleine Abstufung je nach Anzahl der Störungen
|
||||
// if ($this->downCount >= 3) {
|
||||
// $this->badgeText = "{$this->downCount} Dienste down";
|
||||
// $this->badgeIcon = 'ph ph-x-circle';
|
||||
// $this->badgeClass = 'text-rose-300 border-rose-400/30 bg-rose-500/10';
|
||||
// } else {
|
||||
// $this->badgeText = 'Störung erkannt';
|
||||
// $this->badgeIcon = 'ph ph-warning-circle';
|
||||
// $this->badgeClass = 'text-amber-300 border-amber-400/30 bg-amber-500/10';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Fail2banIpList extends Model
|
||||
{
|
||||
protected $table = 'fail2ban_ip_lists';
|
||||
|
||||
protected $fillable = [
|
||||
'ip',
|
||||
'type',
|
||||
'is_system',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ip' => 'string',
|
||||
'type' => 'string',
|
||||
'is_system' => 'boolean',
|
||||
];
|
||||
|
||||
public const TYPE_WHITELIST = 'whitelist';
|
||||
public const TYPE_BLACKLIST = 'blacklist';
|
||||
|
||||
/* ===========================
|
||||
Boot-Hooks (Schutz & Normalisierung)
|
||||
=========================== */
|
||||
protected static function booted()
|
||||
{
|
||||
// Normalisierung & Loopback-Flag setzen
|
||||
static::saving(function (self $m) {
|
||||
$m->ip = trim($m->ip);
|
||||
|
||||
if (!self::isValidIpOrCidr($m->ip)) {
|
||||
throw new \InvalidArgumentException("Ungültige IP/CIDR: {$m->ip}");
|
||||
}
|
||||
|
||||
// Loopback immer als System markieren
|
||||
if (self::isLoopback($m->ip)) {
|
||||
$m->is_system = true;
|
||||
$m->type = self::TYPE_WHITELIST; // Loopback gehört auf die Whitelist
|
||||
}
|
||||
|
||||
// Systemeinträge dürfen nicht in die Blacklist
|
||||
if ($m->is_system && $m->type === self::TYPE_BLACKLIST) {
|
||||
throw new \InvalidArgumentException("Systemeinträge dürfen nicht auf die Blacklist.");
|
||||
}
|
||||
});
|
||||
|
||||
// Systemeinträge sind unveränderlich (bis auf interne Seeds/Maintenance – dann per DB direkt ändern)
|
||||
static::updating(function (self $m) {
|
||||
if ($m->getOriginal('is_system')) {
|
||||
// Erlaube nur no-op Updates (z. B. Timestamps), aber blocke ip/type Änderungen
|
||||
$blocked = $m->isDirty('ip') || $m->isDirty('type') || $m->isDirty('is_system');
|
||||
if ($blocked) {
|
||||
throw new \RuntimeException("Systemeinträge können nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
static::deleting(function (self $m) {
|
||||
if ($m->is_system) {
|
||||
throw new \RuntimeException("Systemeintrag kann nicht gelöscht werden.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Scopes
|
||||
=========================== */
|
||||
public function scopeWhitelist($q)
|
||||
{
|
||||
return $q->where('type', self::TYPE_WHITELIST);
|
||||
}
|
||||
|
||||
public function scopeBlacklist($q)
|
||||
{
|
||||
return $q->where('type', self::TYPE_BLACKLIST);
|
||||
}
|
||||
|
||||
// Für UI: blende Systemeinträge aus
|
||||
public function scopeVisible($q)
|
||||
{
|
||||
return $q->where('is_system', false);
|
||||
}
|
||||
|
||||
// Kombiniert: z. B. Fail2banIpList::visible()->whitelist()->get();
|
||||
public function scopeVisibleWhitelist($q)
|
||||
{
|
||||
return $q->visible()->whitelist();
|
||||
}
|
||||
|
||||
public function scopeVisibleBlacklist($q)
|
||||
{
|
||||
return $q->visible()->blacklist();
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Helper-Listen
|
||||
=========================== */
|
||||
// Für UI-Listen (ohne System)
|
||||
public static function whitelistArray(): array
|
||||
{
|
||||
return static::where('type', self::TYPE_WHITELIST)
|
||||
->where('is_system', false)
|
||||
->pluck('ip')->all();
|
||||
}
|
||||
|
||||
public static function blacklistArray(): array
|
||||
{
|
||||
return static::where('type', self::TYPE_BLACKLIST)
|
||||
->where('is_system', false)
|
||||
->pluck('ip')->all();
|
||||
}
|
||||
|
||||
// Für das Schreiben der Fail2ban-Whitelist-Datei (inkl. System!)
|
||||
public static function allWhitelistForConfig(): array
|
||||
{
|
||||
return static::where('type', self::TYPE_WHITELIST)
|
||||
->pluck('ip')->all();
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Validierung
|
||||
=========================== */
|
||||
// Erlaubt IPv4/IPv6, optional mit CIDR (/0..32 bzw. /0..128)
|
||||
public static function isValidIpOrCidr(string $value): bool
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
// IP ohne CIDR
|
||||
if (filter_var($value, FILTER_VALIDATE_IP)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IP/CIDR
|
||||
if (strpos($value, '/') !== false) {
|
||||
[$ip, $prefix] = explode('/', $value, 2);
|
||||
if (!ctype_digit($prefix)) {
|
||||
return false;
|
||||
}
|
||||
$prefix = (int)$prefix;
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return $prefix >= 0 && $prefix <= 32;
|
||||
}
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
return $prefix >= 0 && $prefix <= 128;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loopback-Erkennung (IPv4 127.0.0.0/8, IPv6 ::1/128)
|
||||
public static function isLoopback(string $value): bool
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
// Klartext-Fälle
|
||||
if (in_array($value, ['127.0.0.1', '127.0.0.1/8', '::1', '::1/128'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4 Loopback Bereich
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return str_starts_with($value, '127.');
|
||||
}
|
||||
|
||||
// IPv4-CIDR Loopback
|
||||
if (strpos($value, '/') !== false) {
|
||||
[$ip, $prefix] = explode('/', $value, 2);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && ctype_digit($prefix)) {
|
||||
$prefix = (int)$prefix;
|
||||
// Prüfe, ob Netz 127.0.0.0/8 überlappt
|
||||
return self::cidrOverlaps($ip, $prefix, '127.0.0.0', 8);
|
||||
}
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && ctype_digit($prefix)) {
|
||||
$prefix = (int)$prefix;
|
||||
// Prüfe, ob ::1/128 überlappt (nur exakt ::1)
|
||||
return self::cidrOverlaps($ip, $prefix, '::1', 128);
|
||||
}
|
||||
}
|
||||
|
||||
// IPv6 single
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
return $value === '::1';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple Overlap-Check für IPv4/IPv6 Netze
|
||||
private static function cidrOverlaps(string $ip, int $prefix, string $netIp, int $netPrefix): bool
|
||||
{
|
||||
$a = inet_pton($ip);
|
||||
$b = inet_pton($netIp);
|
||||
if ($a === false || $b === false || strlen($a) !== strlen($b)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$len = strlen($a);
|
||||
$bytes = intdiv(max($prefix, $netPrefix), 8);
|
||||
$bits = max($prefix, $netPrefix) % 8;
|
||||
|
||||
// Netzmaske anwenden (auf die längere Präfixlänge)
|
||||
for ($i = 0; $i < $bytes; $i++) {
|
||||
if ($a[$i] !== $b[$i]) return false;
|
||||
}
|
||||
if ($bits > 0) {
|
||||
$mask = chr(0xFF << (8 - $bits));
|
||||
if ((ord($a[$bytes]) & ord($mask)) !== (ord($b[$bytes]) & ord($mask))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//
|
||||
//namespace App\Models;
|
||||
//
|
||||
//use Illuminate\Database\Eloquent\Model;
|
||||
//
|
||||
//class Fail2banIpList extends Model
|
||||
//{
|
||||
// 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();
|
||||
// }
|
||||
//}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Fail2banSetting extends Model
|
||||
{
|
||||
protected $table = 'fail2ban_settings';
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
|
@ -32,9 +32,9 @@ final class BuildMeta
|
|||
}
|
||||
|
||||
// 2) Fallback: .env APP_VERSION
|
||||
if ($m->version === 'dev' && ($envVer = env('APP_VERSION'))) {
|
||||
$m->version = trim($envVer);
|
||||
}
|
||||
// if ($m->version === 'dev' && ($envVer = env('APP_VERSION'))) {
|
||||
// $m->version = trim($envVer);
|
||||
// }
|
||||
|
||||
// 3) Fallback: Git (ohne "-dirty")
|
||||
if ($m->rev === '' || $m->version === 'dev') {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?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->boolean('is_system')->default(false)->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['ip', 'type'], 'fail2ban_ip_lists_ip_type_unique');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('fail2ban_ip_lists');
|
||||
}
|
||||
};
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
Livewire.on('reload-page', e => setTimeout(() => window.location.reload(), e.delay || 0));
|
||||
|
||||
// document.addEventListener('livewire:error', e => {
|
||||
// if (e.detail.status === 419) {
|
||||
// console.warn('Session expired – refreshing...');
|
||||
// window.location.reload();
|
||||
// }
|
||||
// });
|
||||
// // import { showToast } from '../ui/toast.js'
|
||||
//
|
||||
// // — Livewire-Hooks (global)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{{-- resources/views/auth/login.blade.php --}}
|
||||
@extends('layouts.app')
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('title', 'Login')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@extends('layouts.app')
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('title', 'Konto erstellen')
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,6 +34,20 @@
|
|||
@vite(['resources/js/app.js'])
|
||||
@livewireScripts
|
||||
@livewire('wire-elements-modal')
|
||||
<script>
|
||||
// Registriere den Hook, sobald Livewire bereit ist.
|
||||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.hook('request', ({ fail }) => {
|
||||
fail(({ status, preventDefault, json }) => {
|
||||
if (status === 419) {
|
||||
preventDefault();
|
||||
const to = (json && json.redirect) ? json.redirect : '{{ route('login') }}';
|
||||
window.location.replace(to);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{--<!DOCTYPE html>--}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_','-',app()->getLocale()) }}" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>@yield('title', config('app.name'))</title>
|
||||
<script>document.documentElement.setAttribute('data-ui', 'booting');</script>
|
||||
@vite(['resources/css/app.css'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
|
||||
|
||||
<body class="text-slate-100 font-bai">
|
||||
<div class="app-backdrop"></div>
|
||||
<div id="main" class="main content group/side main-shell rail">
|
||||
<div class="px-2.5 pb-2.5 pt-0.5">
|
||||
<div class="mt-5">
|
||||
@yield('content')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@vite(['resources/js/app.js'])
|
||||
@livewireScripts
|
||||
@livewire('wire-elements-modal')
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
{{-- resources/views/livewire/domains/dns-assistant-modal.blade.php --}}
|
||||
@push('modal.header')
|
||||
<div class="px-5 pt-5 pb-3 border-b border-white/10
|
||||
backdrop-blur rounded-t-2xl relative">
|
||||
<span class="absolute top-3 right-3 z-20 inline-flex items-center gap-2
|
||||
rounded-full border border-slate-700/70 bg-slate-800/70
|
||||
px-3 py-1 text-[11px] text-slate-200 shadow-sm">
|
||||
<i class="ph ph-clock text-slate-300"></i> TTL: {{ $ttl }}
|
||||
</span>
|
||||
<h2 class="text-[18px] font-semibold text-slate-100">DNS-Einträge</h2>
|
||||
<div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl relative">
|
||||
<span class="absolute top-3 right-3 z-20 inline-flex items-center gap-2
|
||||
rounded-full border border-slate-700/70 bg-slate-800/70
|
||||
px-3 py-1 text-[11px] text-slate-200 shadow-sm">
|
||||
<i class="ph ph-clock text-slate-300"></i> TTL: {{ $ttl }}
|
||||
</span>
|
||||
|
||||
<h2 class="text-[18px] font-semibold text-slate-100">DNS-Einträge</h2>
|
||||
<p class="text-[13px] text-slate-300/80">
|
||||
Setze die folgenden Records für
|
||||
<span class="text-sky-300 underline decoration-sky-500/40 underline-offset-2">{{ $zone }}</span>.
|
||||
|
|
@ -17,131 +16,365 @@
|
|||
@endpush
|
||||
|
||||
<div class="relative p-5">
|
||||
<div class="space-y-5">
|
||||
{{-- GRID: links (Mail-Records), rechts (Infrastruktur) --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{{-- Mail-Records --}}
|
||||
<section>
|
||||
<div class="mb-2 flex items-center gap-2 text-[11px]">
|
||||
<span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200">Step 1</span>
|
||||
<span class="text-slate-300/80">Mail-Records</span>
|
||||
<span class="ml-auto px-2 py-0.5 rounded bg-indigo-600/20 text-indigo-200 border border-indigo-500/20">
|
||||
<i class="ph ph-seal-check mr-1"></i>Absenderdomain
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{{-- Step 1: Mail-Records (domain-spezifisch) --}}
|
||||
<section>
|
||||
<div class="mb-2 flex items-center gap-2 text-[11px]">
|
||||
<span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200">Step 1</span>
|
||||
<span class="text-slate-300/80">Mail-Records</span>
|
||||
<span class="ml-auto px-2 py-0.5 rounded bg-indigo-600/20 text-indigo-200 border border-indigo-500/20">
|
||||
<i class="ph ph-seal-check mr-1"></i> Absenderdomain
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach ($dynamic as $r)
|
||||
<div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">
|
||||
<div class="flex items-center justify-between px-4 py-2 text-[12px]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>
|
||||
<span class="text-slate-200">{{ $r['name'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-slate-300/70">
|
||||
<x-button.copy-btn :text="$r['value']" />
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
@foreach ($dynamic as $r)
|
||||
<div class="rounded-xl border {{ $r['boxClass'] ?? $stateColors['neutral'] }}">
|
||||
<div class="flex items-center justify-between px-4 py-2 text-[12px]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">
|
||||
{{ $r['type'] }}
|
||||
</span>
|
||||
<span class="text-slate-200">{{ $r['name'] }}</span>
|
||||
</div>
|
||||
<div class="px-4 pb-3">
|
||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||
@if(!empty($r['helpUrl']))
|
||||
<a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"
|
||||
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">
|
||||
<i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}
|
||||
</a>
|
||||
@endif
|
||||
<div class="flex items-center gap-2 text-slate-300/70">
|
||||
<x-button.copy-btn :text="$r['value']" />
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@foreach ($optional as $r)
|
||||
<div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">
|
||||
<div class="flex items-center justify-between px-4 py-2 text-[12px]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>
|
||||
<span class="text-slate-200">{{ $r['name'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-slate-300/70">
|
||||
<span class="text-[11px] px-2 py-0.5 rounded {{ $recordColors['OPTIONAL'] ?? 'bg-slate-700/50 text-slate-300' }}">Optional</span>
|
||||
<x-button.copy-btn :text="$r['value']" />
|
||||
</div>
|
||||
<div class="px-4 pb-3 space-y-2">
|
||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||
|
||||
@if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
|
||||
<div class="text-[11px] text-white/60 break-words">
|
||||
<span class="opacity-70">Ist:</span>
|
||||
<span class="font-mono break-words">{{ $r['display_actual'] }}</span>
|
||||
</div>
|
||||
<div class="px-4 pb-3">
|
||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||
@if(!empty($r['helpUrl']))
|
||||
<a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"
|
||||
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">
|
||||
<i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Step 2: Globale Infrastruktur (MTA-Host) --}}
|
||||
<section>
|
||||
<div class="mb-2 flex items-center gap-2 text-[11px]">
|
||||
<span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200">Step 2</span>
|
||||
<span class="text-slate-300/80">Globale Infrastruktur (MTA-Host)</span>
|
||||
<span class="text-slate-400/70">gilt für alle Domains</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach ($static as $r)
|
||||
<div class="rounded-xl border {{ $r['boxClass'] ?? $stateColors['neutral'] }}">
|
||||
{{-- <div class="flex items-center justify-between px-4 py-2 text-[12px]">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>--}}
|
||||
{{-- <span class="text-slate-200">{{ $r['name'] }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-slate-300/70">--}}
|
||||
{{-- <x-button.copy-btn :text="$r['value']" />--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
<div class="flex items-start justify-between gap-2 px-4 py-2 text-[12px]">
|
||||
<div class="min-w-0 flex items-start gap-2">
|
||||
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">
|
||||
{{ $r['type'] }}
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<span
|
||||
class="text-[12px] leading-5 text-slate-200 break-all"
|
||||
title="{{ $r['name'] }}">{{ $r['name'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{-- Globale Infrastruktur --}}
|
||||
<section>
|
||||
<div class="mb-2 flex items-center gap-2 text-[11px]">
|
||||
<span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200">Step 2</span>
|
||||
<span class="text-slate-300/80">Globale Infrastruktur (MTA-Host)</span>
|
||||
<span class="text-slate-400/70">gilt für alle Domains</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach ($static as $r)
|
||||
<div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">
|
||||
<div class="flex items-center justify-between px-4 py-2 text-[12px]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>
|
||||
<span class="text-slate-200">{{ $r['name'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-slate-300/70">
|
||||
<x-button.copy-btn :text="$r['value']" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 pb-3">
|
||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||
<div class="shrink-0">
|
||||
<x-button.copy-btn :text="$r['value']" />
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="px-4 pb-3 space-y-2">
|
||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||
@if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
|
||||
<div class="text-[11px] text-white/60 break-words">
|
||||
<span class="opacity-70">Ist:</span>
|
||||
<span class="font-mono break-words">{{ $r['display_actual'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{{-- Optional-Block unterhalb (einspaltig) --}}
|
||||
@if(!empty($optional))
|
||||
<div class="mt-5">
|
||||
<div class="mb-2 flex items-center gap-2 text-[11px]">
|
||||
<span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200">Optional</span>
|
||||
<span class="text-slate-300/80">empfohlene Zusatz-Records</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach ($optional as $r)
|
||||
<div class="rounded-xl border {{ $r['boxClass'] ?? $stateColors['neutral'] }}">
|
||||
<div class="flex items-center justify-between px-4 py-2 text-[12px]">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">
|
||||
{{ $r['type'] }}
|
||||
</span>
|
||||
<span class="text-slate-200">{{ $r['name'] }}</span>
|
||||
<span class="ml-2 text-[10px] px-2 py-0.5 rounded bg-white/5 border border-white/10 text-white/70">
|
||||
Optional
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-slate-300/70">
|
||||
<x-button.copy-btn :text="$r['value']" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-3 space-y-2">
|
||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 opacity-80 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||
@if($checked && ($r['state'] ?? 'neutral') !== 'ok' && !empty($r['display_actual']))
|
||||
<div class="text-[11px] text-white/60 break-words">
|
||||
<span class="opacity-70">Ist:</span>
|
||||
<span class="font-mono break-words">{{ $r['display_actual'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($r['helpUrl']))
|
||||
<div>
|
||||
<span class="text-xs text-white/60">{{ $r['info'] }}</span>
|
||||
</div>
|
||||
<a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"
|
||||
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">
|
||||
<i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@push('modal.footer')
|
||||
<div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">
|
||||
<div class="flex flex-wrap items-center gap-4 text-[12px] text-slate-300">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<i class="ph ph-check-circle text-emerald-300"></i> vorhanden
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<i class="ph ph-warning text-amber-300"></i> abweichend
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<i class="ph ph-x-circle text-rose-300"></i> fehlt
|
||||
</span>
|
||||
|
||||
<span class="ml-auto">
|
||||
<div class="flex items-center gap-2">
|
||||
<button wire:click="$dispatch('domain:check-dns')" wire:loading.attr="disabled"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-[12px]
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<i class="ph ph-check-circle text-emerald-300"></i> vorhanden
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<i class="ph ph-warning text-amber-300"></i> Syntaxfehler
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<i class="ph ph-x-circle text-rose-300"></i> fehlt (nur Pflicht)
|
||||
</span>
|
||||
|
||||
<span class="ml-auto"></span>
|
||||
|
||||
<button wire:click="$dispatch('domain:check-dns')" wire:loading.attr="disabled"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-[12px]
|
||||
bg-white/5 border border-white/10 hover:bg-white/10">
|
||||
<i class="ph ph-magnifying-glass"></i>
|
||||
<span wire:loading.remove>DNS prüfen</span>
|
||||
<span wire:loading>prüfe…</span>
|
||||
</button>
|
||||
<button wire:click="$dispatch('closeModal')"
|
||||
class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">
|
||||
<i class="ph ph-check"></i> Fertig
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
<i class="ph ph-magnifying-glass"></i>
|
||||
<span wire:loading.remove>DNS prüfen</span>
|
||||
<span wire:loading>prüfe…</span>
|
||||
</button>
|
||||
|
||||
<button wire:click="$dispatch('closeModal')"
|
||||
class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300
|
||||
border border-emerald-400/30 hover:bg-emerald-500/30">
|
||||
<i class="ph ph-check"></i> Fertig
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
{{--@push('modal.header')--}}
|
||||
{{-- <div class="px-5 pt-5 pb-3 border-b border-white/10--}}
|
||||
{{-- backdrop-blur rounded-t-2xl relative">--}}
|
||||
{{-- <span class="absolute top-3 right-3 z-20 inline-flex items-center gap-2--}}
|
||||
{{-- rounded-full border border-slate-700/70 bg-slate-800/70--}}
|
||||
{{-- px-3 py-1 text-[11px] text-slate-200 shadow-sm">--}}
|
||||
{{-- <i class="ph ph-clock text-slate-300"></i> TTL: {{ $ttl }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <h2 class="text-[18px] font-semibold text-slate-100">DNS-Einträge</h2>--}}
|
||||
|
||||
{{-- <p class="text-[13px] text-slate-300/80">--}}
|
||||
{{-- Setze die folgenden Records für--}}
|
||||
{{-- <span class="text-sky-300 underline decoration-sky-500/40 underline-offset-2">{{ $zone }}</span>.--}}
|
||||
{{-- </p>--}}
|
||||
{{-- </div>--}}
|
||||
{{--@endpush--}}
|
||||
|
||||
{{--<div class="relative p-5">--}}
|
||||
{{-- <div class="space-y-5">--}}
|
||||
{{-- --}}{{-- GRID: links (Mail-Records), rechts (Infrastruktur) --}}
|
||||
{{-- <div class="grid grid-cols-1 lg:grid-cols-2 gap-5">--}}
|
||||
{{-- --}}{{-- Mail-Records --}}
|
||||
{{-- <section>--}}
|
||||
{{-- <div class="mb-2 flex items-center gap-2 text-[11px]">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200">Step 1</span>--}}
|
||||
{{-- <span class="text-slate-300/80">Mail-Records</span>--}}
|
||||
{{-- <span class="ml-auto px-2 py-0.5 rounded bg-indigo-600/20 text-indigo-200 border border-indigo-500/20">--}}
|
||||
{{-- <i class="ph ph-seal-check mr-1"></i>Absenderdomain--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="space-y-4">--}}
|
||||
{{-- @foreach ($dynamic as $r)--}}
|
||||
{{-- <div class="rounded-xl border--}}
|
||||
{{-- {{ $checked ? ($stateColors[$r['state'] ?? 'neutral'] ?? $stateColors['neutral']) : $stateColors['neutral'] }}">--}}
|
||||
{{-- <div class="flex items-center justify-between px-4 py-2 text-[12px]">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">--}}
|
||||
{{-- {{ $r['type'] }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="text-slate-200">{{ $r['name'] }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-slate-300/70">--}}
|
||||
{{-- <x-button.copy-btn :text="$r['value']" />--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="px-4 pb-3">--}}
|
||||
{{-- <pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>--}}
|
||||
{{-- @if(!empty($r['helpUrl']))--}}
|
||||
{{-- <a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"--}}
|
||||
{{-- class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">--}}
|
||||
{{-- <i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}--}}
|
||||
{{-- </a>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">--}}
|
||||
{{-- <div class="flex items-center justify-between px-4 py-2 text-[12px]">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>--}}
|
||||
{{-- <span class="text-slate-200">{{ $r['name'] }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-slate-300/70">--}}
|
||||
{{-- <x-button.copy-btn :text="$r['value']" />--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="px-4 pb-3">--}}
|
||||
{{-- <pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>--}}
|
||||
{{-- @if(!empty($r['helpUrl']))--}}
|
||||
{{-- <a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"--}}
|
||||
{{-- class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">--}}
|
||||
{{-- <i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}--}}
|
||||
{{-- </a>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforeach--}}
|
||||
|
||||
{{-- @foreach ($optional as $r)--}}
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">--}}
|
||||
{{-- <div class="flex items-center justify-between px-4 py-2 text-[12px]">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>--}}
|
||||
{{-- <span class="text-slate-200">{{ $r['name'] }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-slate-300/70">--}}
|
||||
{{-- <span class="text-[11px] px-2 py-0.5 rounded {{ $recordColors['OPTIONAL'] ?? 'bg-slate-700/50 text-slate-300' }}">Optional</span>--}}
|
||||
{{-- <x-button.copy-btn :text="$r['value']" />--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="px-4 pb-3">--}}
|
||||
{{-- <pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>--}}
|
||||
{{-- @if(!empty($r['helpUrl']))--}}
|
||||
{{-- <a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"--}}
|
||||
{{-- class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">--}}
|
||||
{{-- <i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}--}}
|
||||
{{-- </a>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </section>--}}
|
||||
|
||||
{{-- --}}{{-- Globale Infrastruktur --}}
|
||||
{{-- <section>--}}
|
||||
{{-- <div class="mb-2 flex items-center gap-2 text-[11px]">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded bg-slate-800/80 text-slate-200">Step 2</span>--}}
|
||||
{{-- <span class="text-slate-300/80">Globale Infrastruktur (MTA-Host)</span>--}}
|
||||
{{-- <span class="text-slate-400/70">gilt für alle Domains</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="space-y-4">--}}
|
||||
{{-- @foreach ($static as $r)--}}
|
||||
{{-- <div class="rounded-xl border--}}
|
||||
{{-- {{ $checked ? ($stateColors[$r['state'] ?? 'neutral'] ?? $stateColors['neutral']) : $stateColors['neutral'] }}">--}}
|
||||
{{-- <div class="flex items-center justify-between px-4 py-2 text-[12px]">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">--}}
|
||||
{{-- {{ $r['type'] }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="text-slate-200">{{ $r['name'] }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-slate-300/70">--}}
|
||||
{{-- <x-button.copy-btn :text="$r['value']" />--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="px-4 pb-3">--}}
|
||||
{{-- <pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/[0.04] #rounded-xl #border #border-slate-700/50 #bg-slate-900/60">--}}
|
||||
{{-- <div class="flex items-center justify-between px-4 py-2 text-[12px]">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded {{ $recordColors[$r['type']] ?? 'bg-slate-700/50 text-slate-300' }}">{{ $r['type'] }}</span>--}}
|
||||
{{-- <span class="text-slate-200">{{ $r['name'] }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-slate-300/70">--}}
|
||||
{{-- <x-button.copy-btn :text="$r['value']" />--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="px-4 pb-3">--}}
|
||||
{{-- <pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </section>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--@push('modal.footer')--}}
|
||||
{{-- <div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">--}}
|
||||
{{-- <div class="flex flex-wrap items-center gap-4 text-[12px] text-slate-300">--}}
|
||||
{{-- <span class="inline-flex items-center gap-1">--}}
|
||||
{{-- <i class="ph ph-check-circle text-emerald-300"></i> vorhanden--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="inline-flex items-center gap-1">--}}
|
||||
{{-- <i class="ph ph-warning text-amber-300"></i> abweichend--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="inline-flex items-center gap-1">--}}
|
||||
{{-- <i class="ph ph-x-circle text-rose-300"></i> fehlt--}}
|
||||
{{-- </span>--}}
|
||||
|
||||
{{-- <span class="ml-auto">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <button wire:click="$dispatch('domain:check-dns')" wire:loading.attr="disabled"--}}
|
||||
{{-- class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-[12px]--}}
|
||||
{{-- bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||
{{-- <i class="ph ph-magnifying-glass"></i>--}}
|
||||
{{-- <span wire:loading.remove>DNS prüfen</span>--}}
|
||||
{{-- <span wire:loading>prüfe…</span>--}}
|
||||
{{-- </button>--}}
|
||||
{{-- <button wire:click="$dispatch('closeModal')"--}}
|
||||
{{-- class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">--}}
|
||||
{{-- <i class="ph ph-check"></i> Fertig--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--@endpush--}}
|
||||
{{--@push('modal.footer')--}}
|
||||
{{-- <div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">--}}
|
||||
{{-- <div class="flex justify-end">--}}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,230 @@
|
|||
<div wire:poll.60s="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
|
||||
<i class="ph ph-globe-stand text-white/70 text-[13px]"></i>
|
||||
<span class="text-[11px] uppercase text-white/70">DKIM / DMARC / TLSA</span>
|
||||
<span class="text-[11px] uppercase text-white/70 tracking-wide">Mail-DNS Health</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button wire:click="refresh"
|
||||
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||
Neu prüfen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-white/5">
|
||||
@forelse($rows as $r)
|
||||
<div class="py-2 flex items-center justify-between">
|
||||
<div class="text-white/85">{{ $r['dom'] }}</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="px-2 py-0.5 rounded-full border {{ $r['dkim'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">DKIM</span>
|
||||
<span class="px-2 py-0.5 rounded-full border {{ $r['dmarc'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">DMARC</span>
|
||||
<span class="px-2 py-0.5 rounded-full border {{ $r['tlsa'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">TLSA</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
wire:click="openDnsModal({{ $r['id'] }})"
|
||||
class="w-full text-left py-3 flex items-center justify-between rounded-lg px-2 hover:bg-white/5 text-sm">
|
||||
<div class="text-white/85">{{ $r['name'] }}</div>
|
||||
|
||||
@if($r['ok'])
|
||||
<span class="px-2 py-0.5 rounded-full border text-emerald-300 border-emerald-400/30 bg-emerald-500/10 text-xs">
|
||||
OK
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">
|
||||
Fertig konfigurieren
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
@empty
|
||||
<div class="py-4 text-sm text-white/60">Keine Domains.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{--<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-4">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-globe-stand text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70 tracking-wide">Mail-DNS Health</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-xs text-white/60">--}}
|
||||
{{-- <span class="opacity-70">TLSA:</span>--}}
|
||||
{{-- <span class="{{ $tlsa ? 'text-emerald-300' : 'text-rose-300' }}">--}}
|
||||
{{-- {{ $tlsa ? 'ok' : 'fehlend' }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="opacity-50">({{ $mtaHost }})</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="divide-y divide-white/5">--}}
|
||||
{{-- @forelse($rows as $r)--}}
|
||||
{{-- <button type="button"--}}
|
||||
{{-- wire:click="openDnsModal({{ $r['id'] }})"--}}
|
||||
{{-- class="w-full text-left py-3 flex items-center justify-between hover:bg-white/5/20 rounded-lg px-2 hover:bg-white/5">--}}
|
||||
{{-- <div class="text-white/85">{{ $r['name'] }}</div>--}}
|
||||
|
||||
{{-- @if($r['ok'])--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-emerald-300 border-emerald-400/30 bg-emerald-500/10 text-xs">--}}
|
||||
{{-- OK--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">--}}
|
||||
{{-- Fertig konfigurieren--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="text-[11px] text-white/45">--}}
|
||||
{{-- fehlt: {{ implode(', ', $r['missing']) }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </button>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="py-4 text-sm text-white/60">Keine Domains.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-4">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-globe-stand text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70 tracking-wide">DNS / Mail Health</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-xs text-white/60">--}}
|
||||
{{-- <span class="opacity-70">IP:</span>--}}
|
||||
{{-- <span class="text-white/90 font-mono">{{ $ipv4 ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Hostweite TLSA-Anzeige --}}
|
||||
{{-- <div class="mb-4 flex items-center gap-2 text-xs">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border--}}
|
||||
{{-- {{ $tlsa ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- TLSA ({{ $host }})--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="text-white/45">für 25/465/587</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="divide-y divide-white/5">--}}
|
||||
{{-- @forelse($domains as $dom)--}}
|
||||
{{-- <div class="py-2 flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/85">{{ $dom['name'] }}</div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-xs">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border--}}
|
||||
{{-- {{ $dom['dkim'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- DKIM--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border--}}
|
||||
{{-- {{ $dom['dmarc'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- DMARC--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="py-4 text-sm text-white/60">Keine Domains.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-4">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-database text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70 tracking-wide">DNS Overview</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-xs text-white/60">--}}
|
||||
{{-- IP: <span class="text-white/90 font-mono">{{ $ipv4 ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @foreach($domains as $dom)--}}
|
||||
{{-- <div class="border-t border-white/5 py-3">--}}
|
||||
{{-- <div class="text-white/85 font-medium">{{ $dom['name'] }}</div>--}}
|
||||
{{-- <div class="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2 text-xs">--}}
|
||||
{{-- @foreach($dom['checks'] as $label => $status)--}}
|
||||
{{-- <div class="flex items-center gap-1">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border--}}
|
||||
{{-- {{ $status ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- {{ strtoupper($label) }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div wire:poll.60s="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-4">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-globe-stand text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70 tracking-wide">DKIM / DMARC / TLSA</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-xs text-white/60">--}}
|
||||
{{-- <span class="opacity-70">IP:</span>--}}
|
||||
{{-- <span class="text-white/90 font-mono">--}}
|
||||
{{-- {{ $ipv4 ?? '–' }}{{ $ipv6 ? ' / '.$ipv6 : '' }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Host-Status (TLSA) --}}
|
||||
{{-- <div class="mb-4 flex items-center justify-between px-3 py-2 rounded-xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <i class="ph ph-server text-white/60 text-[15px]"></i>--}}
|
||||
{{-- <span class="text-white/85 font-mono">{{ $host }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div>--}}
|
||||
{{-- <span class="px-2 py-0.5 text-xs rounded-full border--}}
|
||||
{{-- {{ $tlsa ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- TLSA--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Domainliste: DKIM/DMARC --}}
|
||||
{{-- <div class="divide-y divide-white/5">--}}
|
||||
{{-- @forelse($rows as $r)--}}
|
||||
{{-- <div class="py-2 flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/85">{{ $r['dom'] }}</div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-xs">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border--}}
|
||||
{{-- {{ $r['dkim'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- DKIM--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border--}}
|
||||
{{-- {{ $r['dmarc'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- DMARC--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="py-4 text-sm text-white/60 text-center">Keine aktiven Domains gefunden.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div wire:poll.60s="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-globe-stand text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">DKIM / DMARC / TLSA</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="divide-y divide-white/5">--}}
|
||||
{{-- @forelse($rows as $r)--}}
|
||||
{{-- <div class="py-2 flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/85">{{ $r['dom'] }}</div>--}}
|
||||
{{-- <div class="flex items-center gap-2 text-xs">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border {{ $r['dkim'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">DKIM</span>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border {{ $r['dmarc'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">DMARC</span>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border {{ $r['tlsa'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">TLSA</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="py-4 text-sm text-white/60">Keine Domains.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -4,20 +4,374 @@
|
|||
<i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>
|
||||
<span class="text-[11px] uppercase text-white/70">Fail2Ban</span>
|
||||
</div>
|
||||
<span class="px-2 py-0.5 rounded-full border text-xs
|
||||
{{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">
|
||||
{{ $activeBans }} aktuell
|
||||
</span>
|
||||
|
||||
@if($available)
|
||||
<span class="px-2 py-0.5 rounded-full border text-xs
|
||||
{{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">
|
||||
{{ $activeBans }} aktuell
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">
|
||||
nicht installiert
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-sm text-white/70">Top IPs (letzte Logs):</div>
|
||||
<ul class="mt-2 space-y-1 text-sm">
|
||||
@forelse($topIps as $i)
|
||||
<li class="flex justify-between">
|
||||
<span class="text-white/80">{{ $i['ip'] }}</span>
|
||||
<span class="text-white/60">{{ $i['count'] }}</span>
|
||||
</li>
|
||||
@empty
|
||||
<li class="text-white/50">–</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
|
||||
@if(!$available)
|
||||
<div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>
|
||||
|
||||
@elseif($permDenied)
|
||||
<div class="text-sm text-amber-200">
|
||||
Keine Berechtigung (sudo) auf <code class="font-mono">fail2ban-client</code>/<code class="font-mono">journalctl</code>/<code class="font-mono">zgrep</code>.
|
||||
<span class="opacity-80">Sudo-Regel prüfen.</span>
|
||||
</div>
|
||||
|
||||
@elseif($error)
|
||||
<div class="text-sm text-amber-200">
|
||||
Unerwartete Ausgabe von <code class="font-mono">fail2ban-client status</code>.
|
||||
<span class="opacity-80">Details in <code>storage/logs/laravel.log</code>.</span>
|
||||
</div>
|
||||
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
@forelse($jails as $j)
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-white/85 font-medium">{{ $j['name'] }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-white/60">
|
||||
Bannzeit: {{ $j['bantime'] === -1 ? 'permanent' : ($j['bantime'].'s') }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full border text-[11px]
|
||||
{{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">
|
||||
{{ $j['banned'] }} gebannt
|
||||
</span>
|
||||
|
||||
<button wire:click.stop="openDetails('{{ $j['name'] }}')"
|
||||
class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">
|
||||
Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-sm text-white/60">Keine Jails gefunden.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button wire:click="refresh" wire:loading.attr="disabled"
|
||||
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||
<span wire:loading.remove>Neu prüfen</span>
|
||||
<span wire:loading>prüfe…</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if($available)--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||
{{-- {{ $activeBans }} aktuell--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||
{{-- nicht installiert--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if(!$available)--}}
|
||||
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||
{{-- @elseif($permDenied)--}}
|
||||
{{-- <div class="text-sm text-amber-200">--}}
|
||||
{{-- Keine Berechtigung auf <code class="font-mono">/var/run/fail2ban/fail2ban.sock</code>.--}}
|
||||
{{-- <span class="opacity-80">Sudo-Regel prüfen.</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <div class="space-y-2">--}}
|
||||
{{-- @forelse($jails as $j)--}}
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="text-[11px] text-white/60">--}}
|
||||
{{-- Bannzeit:--}}
|
||||
{{-- @if($j['bantime'] === -1)--}}
|
||||
{{-- permanent--}}
|
||||
{{-- @else--}}
|
||||
{{-- {{ $j['bantime'] }}s--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||
{{-- {{ $j['banned'] }} gebannt--}}
|
||||
{{-- </span>--}}
|
||||
|
||||
{{-- --}}{{-- fix: stop event bubbling --}}
|
||||
{{-- <button wire:click.stop="openDetails('{{ $j['name'] }}')"--}}
|
||||
{{-- class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">--}}
|
||||
{{-- Details--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="mt-4 flex justify-end">--}}
|
||||
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||
{{-- <span wire:loading>prüfe…</span>--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if($available)--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||
{{-- {{ $activeBans }} aktuell--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||
{{-- nicht installiert--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if(!$available)--}}
|
||||
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||
{{-- @elseif($permDenied)--}}
|
||||
{{-- <div class="text-sm text-amber-200">--}}
|
||||
{{-- Keine Berechtigung auf <code class="font-mono">/var/run/fail2ban/fail2ban.sock</code>.--}}
|
||||
{{-- <span class="opacity-80">Sudo-Regel prüfen.</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <div class="space-y-2">--}}
|
||||
{{-- @forelse($jails as $j)--}}
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||
{{-- {{ $j['banned'] }} gebannt--}}
|
||||
{{-- </span>--}}
|
||||
{{-- --}}{{-- Optional: Details öffnen (Tab/Modal) --}}
|
||||
{{-- <button wire:click="openDetails('{{ $j['name'] }}')"--}}
|
||||
{{-- class="text-[11px] px-2 py-0.5 rounded border border-white/15 bg-white/5 hover:bg-white/10">--}}
|
||||
{{-- Details--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="mt-4 flex justify-end">--}}
|
||||
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||
{{-- <span wire:loading>prüfe…</span>--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if($available)--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||
{{-- {{ $activeBans }} aktuell--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||
{{-- nicht installiert--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if(!$available)--}}
|
||||
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||
{{-- @else--}}
|
||||
{{-- --}}{{-- Jails --}}
|
||||
{{-- <div class="space-y-2">--}}
|
||||
{{-- @forelse($jails as $j)--}}
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||
{{-- <div class="flex items-center gap-2">--}}
|
||||
{{-- <span class="text-[11px] text-white/50">--}}
|
||||
{{-- Bannzeit:--}}
|
||||
{{-- @if($j['bantime'] === -1) permanent--}}
|
||||
{{-- @else {{ $j['bantime'] }}s--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </span>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||
{{-- {{ $j['banned'] }} gebannt--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if(!empty($j['ips']))--}}
|
||||
{{-- <div class="mt-2 grid gap-1">--}}
|
||||
{{-- @foreach($j['ips'] as $ip)--}}
|
||||
{{-- <div class="flex items-center gap-2 text-[12px] text-white/80 font-mono">--}}
|
||||
{{-- <span>{{ $ip['ip'] }}</span>--}}
|
||||
{{-- @if($ip['remaining'] === -1)--}}
|
||||
{{-- <span class="px-1.5 py-0.5 rounded border border-rose-400/30 bg-rose-500/10 text-rose-200">--}}
|
||||
{{-- permanent--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @elseif(is_int($ip['remaining']) && $ip['remaining'] > 0)--}}
|
||||
{{-- <span class="px-1.5 py-0.5 rounded border border-amber-400/30 bg-amber-500/10 text-amber-100">--}}
|
||||
{{-- {{ gmdate('H\h i\m s\s', $ip['remaining']) }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Top IPs aus Ban-Events --}}
|
||||
{{-- <div class="mt-4">--}}
|
||||
{{-- <div class="text-sm text-white/70">Top IPs (letzte Fail2Ban-Logs):</div>--}}
|
||||
{{-- <ul class="mt-2 space-y-1 text-sm">--}}
|
||||
{{-- @forelse($topIps as $i)--}}
|
||||
{{-- <li class="flex justify-between">--}}
|
||||
{{-- <span class="text-white/80 font-mono">{{ $i['ip'] }}</span>--}}
|
||||
{{-- <span class="text-white/60">{{ $i['count'] }}</span>--}}
|
||||
{{-- </li>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <li class="text-white/50">–</li>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </ul>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="mt-4 flex justify-end">--}}
|
||||
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||
{{-- <span wire:loading>prüfe…</span>--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Fail2Ban</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if($available)--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $activeBans>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' }}">--}}
|
||||
{{-- {{ $activeBans }} aktuell--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs text-rose-300 border-rose-400/30 bg-rose-500/10">--}}
|
||||
{{-- nicht installiert--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if(!$available)--}}
|
||||
{{-- <div class="text-sm text-white/60">fail2ban-client wurde nicht gefunden.</div>--}}
|
||||
{{-- @else--}}
|
||||
{{-- --}}{{-- Jails --}}
|
||||
{{-- <div class="space-y-2">--}}
|
||||
{{-- @forelse($jails as $j)--}}
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-3 py-2">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/85 font-medium">{{ $j['name'] }}</div>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-[11px]--}}
|
||||
{{-- {{ $j['banned']>0 ? 'text-amber-200 border-amber-400/30 bg-amber-500/10' : 'text-white/60 border-white/20 bg-white/5' }}">--}}
|
||||
{{-- {{ $j['banned'] }} gebannt--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @if(!empty($j['ips']))--}}
|
||||
{{-- <div class="mt-1 text-[12px] text-white/65 font-mono break-words">--}}
|
||||
{{-- {{ implode(', ', $j['ips']) }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="text-sm text-white/60">Keine Jails gefunden.</div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Top IPs aus Ban-Events --}}
|
||||
{{-- <div class="mt-3">--}}
|
||||
{{-- <div class="text-sm text-white/70">Top IPs (letzte Fail2Ban-Logs):</div>--}}
|
||||
{{-- <ul class="mt-2 space-y-1 text-sm">--}}
|
||||
{{-- @foreach($j['ips'] as $ip)--}}
|
||||
{{-- <div class="flex items-center gap-2 text-[12px] text-white/80 font-mono">--}}
|
||||
{{-- <span>{{ $ip['ip'] }}</span>--}}
|
||||
{{-- @if($ip['remaining'] === -1)--}}
|
||||
{{-- <span class="px-1.5 py-0.5 rounded border border-rose-400/30 bg-rose-500/10 text-rose-200">permanent</span>--}}
|
||||
{{-- @elseif(is_int($ip['remaining']) && $ip['remaining'] > 0)--}}
|
||||
{{-- <span class="px-1.5 py-0.5 rounded border border-amber-400/30 bg-amber-500/10 text-amber-100">--}}
|
||||
{{-- {{ gmdate('H\h i\m s\s', $ip['remaining']) }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- @forelse($topIps as $i)--}}
|
||||
{{-- <li class="flex justify-between">--}}
|
||||
{{-- <span class="text-white/80 font-mono">{{ $i['ip'] }}</span>--}}
|
||||
{{-- <span class="text-white/60">{{ $i['count'] }}</span>--}}
|
||||
{{-- </li>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <li class="text-white/50">–</li>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{-- </ul>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="mt-4 flex justify-end">--}}
|
||||
{{-- <button wire:click="refresh" wire:loading.attr="disabled"--}}
|
||||
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||
{{-- <span wire:loading>prüfe…</span>--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{--</div>--}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
<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-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>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
@push('modal.header')
|
||||
<div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">
|
||||
<h2 class="text-[18px] font-semibold text-slate-100">Fail2Ban – {{ $jail }}</h2>
|
||||
<p class="text-[13px] text-slate-300/80">Aktuell gebannte IPs und Restlaufzeiten.</p>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
<div class="p-5 space-y-3">
|
||||
@forelse($rows as $r)
|
||||
<div class="rounded-xl border px-4 py-3 {{ $r['box_class'] }}">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-white/90 font-mono text-[14px]">{{ $r['ip'] }}</div>
|
||||
<div class="text-[12px] text-white/80">{{ $r['time_text'] }}</div>
|
||||
</div>
|
||||
<div class="mt-1 text-[12px] text-white/55">{{ $r['meta_text'] }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/70">
|
||||
Keine gebannten IPs in diesem Jail.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@push('modal.footer')
|
||||
<div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">
|
||||
<div class="flex items-center gap-2 justify-end">
|
||||
{{-- WICHTIG: refresht NUR das Modal --}}
|
||||
<button wire:click="$dispatch('f2b:refresh-banlist')" wire:loading.attr="disabled"
|
||||
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||
<span wire:loading.remove>Neu prüfen</span>
|
||||
<span wire:loading>prüfe…</span>
|
||||
</button>
|
||||
<button wire:click="$dispatch('closeModal')"
|
||||
class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
||||
{{--@push('modal.header')--}}
|
||||
{{-- <div class="px-5 pt-5 pb-3 border-b border-white/10 backdrop-blur rounded-t-2xl">--}}
|
||||
{{-- <h2 class="text-[18px] font-semibold text-slate-100">--}}
|
||||
{{-- Fail2Ban – {{ $jail }}--}}
|
||||
{{-- </h2>--}}
|
||||
{{-- <p class="text-[13px] text-slate-300/80">--}}
|
||||
{{-- Aktuell gebannte IPs und Restlaufzeiten.--}}
|
||||
{{-- </p>--}}
|
||||
{{-- </div>--}}
|
||||
{{--@endpush--}}
|
||||
|
||||
{{--<div class="p-5 space-y-3">--}}
|
||||
{{-- @forelse($rows as $r)--}}
|
||||
{{-- <div class="rounded-xl border px-4 py-3 {{ $r['box_class'] }}">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <div class="text-white/90 font-mono text-[14px]">{{ $r['ip'] }}</div>--}}
|
||||
{{-- <div class="text-[12px] text-white/80">--}}
|
||||
{{-- {{ $r['time_text'] }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="mt-1 text-[12px] text-white/55">--}}
|
||||
{{-- {{ $r['meta_text'] }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @empty--}}
|
||||
{{-- <div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white/70">--}}
|
||||
{{-- Keine gebannten IPs in diesem Jail.--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endforelse--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--@push('modal.footer')--}}
|
||||
{{-- <div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">--}}
|
||||
{{-- <div class="flex items-center gap-2 justify-end">--}}
|
||||
{{-- <button wire:click="$dispatch('f2b:refresh-banlist')" wire:loading.attr="disabled"--}}
|
||||
{{-- class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">--}}
|
||||
{{-- <i class="ph ph-arrows-clockwise text-[13px]"></i>--}}
|
||||
{{-- <span wire:loading.remove>Neu prüfen</span>--}}
|
||||
{{-- <span wire:loading>prüfe…</span>--}}
|
||||
{{-- </button>--}}
|
||||
{{-- <button wire:click="$dispatch('closeModal')"--}}
|
||||
{{-- class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">--}}
|
||||
{{-- Fertig--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--@endpush--}}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,20 +1,83 @@
|
|||
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
|
||||
<div class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
|
||||
<i class="ph ph-shield-warning text-white/70 text-[13px]"></i>
|
||||
<span class="text-[11px] uppercase text-white/70">Reputation / RBL</span>
|
||||
<i class="ph ph-shield text-white/70 text-[13px]"></i>
|
||||
<span class="text-[11px] uppercase text-white/70 tracking-wide">Reputation / RBL</span>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-white/60">
|
||||
<span class="opacity-70">IP:</span>
|
||||
<span class="text-white/90 font-mono">{{ $ip }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-white/70">IP: <span class="text-white/90">{{ $ip }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="mt-5">
|
||||
@if($hits === 0)
|
||||
<span class="px-2 py-0.5 rounded-full border text-emerald-300 border-emerald-400/30 bg-emerald-500/10">Keine Treffer</span>
|
||||
<div class="flex items-center gap-3 px-4 py-3 rounded-xl bg-emerald-500/10 border border-emerald-400/20">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-emerald-500/20">
|
||||
<i class="ph ph-shield-check text-emerald-300 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-emerald-300 font-medium text-[15px]">
|
||||
Deine IP ist auf keiner bekannten Blacklist.
|
||||
</div>
|
||||
<div class="text-[12px] text-white/50 mt-0.5">
|
||||
Gute Reputation – keine Auffälligkeiten gefunden.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-[11px] text-white/45 text-center">
|
||||
Geprüfte öffentliche RBLs:
|
||||
<span class="text-white/60">Spamhaus, PSBL, UCEPROTECT-1, s5h</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-sm text-white/70">{{ $hits }} Treffer:</div>
|
||||
<ul class="mt-2 space-y-1 text-sm text-rose-300">
|
||||
@foreach($lists as $l)<li>• {{ $l }}</li>@endforeach
|
||||
</ul>
|
||||
<div class="flex items-center gap-3 px-4 py-3 rounded-xl bg-rose-500/10 border border-rose-400/20">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-rose-500/20">
|
||||
<i class="ph ph-warning-circle text-rose-300 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-rose-300 font-medium text-[15px]">
|
||||
{{ $hits }} {{ Str::plural('Treffer', $hits) }} auf Blacklists
|
||||
</div>
|
||||
<ul class="mt-1 space-y-1 text-sm text-rose-300/90">
|
||||
@foreach($lists as $l)
|
||||
<li class="flex items-center gap-2">
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full bg-rose-400/80"></span>
|
||||
<span>{{ $l }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex justify-center">
|
||||
<button wire:click="refresh"
|
||||
class="px-3 py-1.5 text-[12px] rounded-lg bg-white/5 border border-white/10 hover:bg-white/10">
|
||||
<i class="ph ph-arrows-clockwise text-[13px]"></i>
|
||||
Neu prüfen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-shield-warning text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Reputation / RBL</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-xs text-white/70">IP: <span class="text-white/90">{{ $ip }}</span></div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <div class="mt-3">--}}
|
||||
{{-- @if($hits === 0)--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-emerald-300 border-emerald-400/30 bg-emerald-500/10">Keine Treffer</span>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <div class="text-sm text-white/70">{{ $hits }} Treffer:</div>--}}
|
||||
{{-- <ul class="mt-2 space-y-1 text-sm text-rose-300">--}}
|
||||
{{-- @foreach($lists as $l)<li>• {{ $l }}</li>@endforeach--}}
|
||||
{{-- </ul>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-4 grid md:grid-cols-2 gap-3 text-sm text-white/70">
|
||||
<div class="glass-chip">Rspamd: <span class="text-white/90">{{ $rspamdVer }}</span></div>
|
||||
<div class="glass-chip">ClamAV: <span class="text-white/90">{{ $clamVer }}</span></div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,295 @@
|
|||
<div wire:key="storage-{{ md5($target ?? '/') }}" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
|
||||
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"
|
||||
@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">
|
||||
<i class="ph ph-archive-box text-white/70 text-[13px]"></i>
|
||||
<span class="text-[11px] uppercase text-white/70">Backups</span>
|
||||
</div>
|
||||
<button wire:click="runNow" class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20">Jetzt sichern</button>
|
||||
<button wire:click="runNow"
|
||||
class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20">
|
||||
Jetzt sichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if($running)
|
||||
<div class="text-white/70 mb-2">
|
||||
{{ $step === 'compress' ? 'Komprimiere …' : ($step === 'mysqldump' ? 'Datenbank exportieren …' : 'Backup läuft …') }}
|
||||
<span class="float-right text-white/70">{{ $percent }}%</span>
|
||||
</div>
|
||||
<div class="w-full h-2 rounded-full bg-white/10 overflow-hidden mb-3">
|
||||
<div class="h-2 bg-white/40" style="width: {{ $percent }}%"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="space-y-1 text-sm">
|
||||
<div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt ?? '–' }}</span></div>
|
||||
<div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize ?? '–' }}</span></div>
|
||||
<div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration ?? '–' }}</span></div>
|
||||
<div class="text-white/70">
|
||||
Letztes Backup: <span class="text-white/90">{{ $lastAt ?? '–' }}</span>
|
||||
</div>
|
||||
<div class="text-white/70">
|
||||
Größe: <span class="text-white/90">{{ $lastSize ?? '–' }}</span>
|
||||
</div>
|
||||
<div class="text-white/70">
|
||||
Dauer: <span class="text-white/90">{{ $lastDuration ?? '–' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="px-2 py-0.5 rounded-full border text-xs
|
||||
{{ $ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">
|
||||
{{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 rounded-full border text-xs
|
||||
{{ $ok === true ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
|
||||
: ($ok === false ? 'text-rose-300 border-rose-400/30 bg-rose-500/10'
|
||||
: 'text-white/60 border-white/20 bg-white/5') }}">
|
||||
{{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{--<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5" wire:poll.2s="refresh">--}}
|
||||
{{-- <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">--}}
|
||||
{{-- <i class="ph ph-archive-box text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Backups</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <button wire:click="runNow" class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20">--}}
|
||||
{{-- Jetzt sichern--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if($running)--}}
|
||||
{{-- <div class="text-white/70 mb-2">{{ $progressText }}</div>--}}
|
||||
{{-- <div class="w-full h-2 rounded-full bg-white/10 overflow-hidden mb-4">--}}
|
||||
{{-- <div class="h-2 bg-white/40" style="width: {{ $percent }}%"></div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
|
||||
{{-- <div class="space-y-1 text-sm">--}}
|
||||
{{-- <div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt ?? '–' }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize ?? '–' }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration ?? '–' }}</span></div>--}}
|
||||
{{-- <div>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $ok === true ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : ($ok === false ? 'text-rose-300 border-rose-400/30 bg-rose-500/10'--}}
|
||||
{{-- : 'text-white/60 border-white/20 bg-white/5') }}">--}}
|
||||
{{-- {{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div wire:key="backup-card" wire:poll.2s="refresh"--}}
|
||||
{{-- class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
|
||||
{{-- <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">--}}
|
||||
{{-- <i class="ph ph-archive-box text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Backups</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <button wire:click="runNow"--}}
|
||||
{{-- class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20">--}}
|
||||
{{-- Jetzt sichern--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Fortschritt (sichtbar nur während run) --}}
|
||||
{{-- <div class="mb-3 {{ $progressVisibleClass }}">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-1 text-xs text-white/70">--}}
|
||||
{{-- <span>{{ $progressText }}</span>--}}
|
||||
{{-- <span>{{ $progressPercent }}%</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="w-full h-2 rounded bg-white/10 overflow-hidden">--}}
|
||||
{{-- <div class="h-2 bg-white/40" style="width: {{ $progressPercent }}%"></div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Infos --}}
|
||||
{{-- <div class="space-y-1 text-sm">--}}
|
||||
{{-- <div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration }}</span></div>--}}
|
||||
{{-- <div>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs {{ $statusColor }}">--}}
|
||||
{{-- {{ $statusText }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div--}}
|
||||
{{-- wire:key="backup-card"--}}
|
||||
{{-- @if($running) wire:poll.1s="refresh" @endif--}}
|
||||
{{-- class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"--}}
|
||||
{{-->--}}
|
||||
{{-- <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">--}}
|
||||
{{-- <i class="ph ph-archive-box text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Backups</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <button--}}
|
||||
{{-- wire:click="runNow"--}}
|
||||
{{-- class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50"--}}
|
||||
{{-- @disabled($running)--}}
|
||||
{{-- >--}}
|
||||
{{-- Jetzt sichern--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Fortschritt --}}
|
||||
{{-- <div class="mb-3">--}}
|
||||
{{-- <div class="flex items-center justify-between mb-1 text-xs text-white/70">--}}
|
||||
{{-- <span>{{ $progressText }}</span>--}}
|
||||
{{-- <span>{{ $progressPercent }}%</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="w-full h-2 rounded bg-white/10 overflow-hidden">--}}
|
||||
{{-- <div class="h-2 bg-white/40" style="width: {{ $progressPercent }}%"></div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Backup Infos --}}
|
||||
{{-- <div class="space-y-1 text-sm">--}}
|
||||
{{-- <div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration }}</span></div>--}}
|
||||
{{-- <div>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs {{ $statusColor }}">--}}
|
||||
{{-- {{ $statusText }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div--}}
|
||||
{{-- class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"--}}
|
||||
{{-- @if($running) wire:poll.1500ms="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">--}}
|
||||
{{-- <i class="ph ph-archive-box text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Backups</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <button wire:click="runNow"--}}
|
||||
{{-- @disabled($running)--}}
|
||||
{{-- class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50">--}}
|
||||
{{-- {{ $running ? 'Läuft…' : 'Jetzt sichern' }}--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- @if($running)--}}
|
||||
{{-- <div class="space-y-2">--}}
|
||||
{{-- <div class="flex items-center justify-between text-xs text-white/70">--}}
|
||||
{{-- <span>Schritt: <span class="text-white/90">{{ $step ?? 'start' }}</span></span>--}}
|
||||
{{-- <span>{{ $percent }}%</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="w-full h-2 rounded-full bg-white/10 overflow-hidden">--}}
|
||||
{{-- <div class="h-2 bg-white/60" style="width: {{ max(2,$percent) }}%"></div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @else--}}
|
||||
{{-- <div class="space-y-1 text-sm">--}}
|
||||
{{-- <div class="text-white/70">Letztes Backup:--}}
|
||||
{{-- <span class="text-white/90">{{ $lastAt ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-white/70">Größe:--}}
|
||||
{{-- <span class="text-white/90">{{ $lastSize ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-white/70">Dauer:--}}
|
||||
{{-- <span class="text-white/90">{{ $lastDuration ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- {{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{--</div>--}}
|
||||
|
||||
{{--<div--}}
|
||||
{{-- @class([--}}
|
||||
{{-- 'glass-card p-4 rounded-2xl border bg-white/5',--}}
|
||||
{{-- 'border-white/10'--}}
|
||||
{{-- ])--}}
|
||||
{{-- @if($state === 'running') wire:poll.1000ms="refresh" @endif--}}
|
||||
{{-->--}}
|
||||
{{-- <div class="flex items-center justify-between mb-3">--}}
|
||||
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
|
||||
{{-- <i class="ph ph-archive-box text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Backups</span>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- <button--}}
|
||||
{{-- wire:click="runNow"--}}
|
||||
{{-- class="px-3 py-1.5 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50"--}}
|
||||
{{-- @disabled($state === 'running')--}}
|
||||
{{-- >--}}
|
||||
{{-- {{ $state === 'running' ? 'Läuft…' : 'Jetzt sichern' }}--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Laufender Fortschritt --}}
|
||||
{{-- @if($state === 'running')--}}
|
||||
{{-- <div class="space-y-2">--}}
|
||||
{{-- <div class="text-white/80 text-sm">Sicherung läuft…</div>--}}
|
||||
{{-- <ul class="space-y-1">--}}
|
||||
{{-- @foreach($steps as $k => $label)--}}
|
||||
{{-- @php--}}
|
||||
{{-- $done = $k === 'finish' ? false : ($k === $step ? false : (array_key_first(array_flip($steps)) !== $k && $k !== $step && $step && array_search($k, array_keys($steps)) < array_search($step, array_keys($steps))));--}}
|
||||
{{-- $isCurrent = $k === $step;--}}
|
||||
{{-- @endphp--}}
|
||||
{{-- <li class="flex items-center gap-2 text-sm">--}}
|
||||
{{-- <span class="w-2 h-2 rounded-full--}}
|
||||
{{-- {{ $done ? 'bg-emerald-400' : ($isCurrent ? 'bg-amber-400 animate-pulse' : 'bg-white/20') }}"></span>--}}
|
||||
{{-- <span class="{{ $done ? 'text-white/80' : ($isCurrent ? 'text-white' : 'text-white/60') }}">--}}
|
||||
{{-- {{ $label }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </li>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- </ul>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @else--}}
|
||||
{{-- --}}{{-- Letztes Ergebnis --}}
|
||||
{{-- <div class="space-y-1 text-sm">--}}
|
||||
{{-- <div class="text-white/70">Letztes Backup:--}}
|
||||
{{-- <span class="text-white/90 font-medium">{{ $lastAt ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-white/70">Größe:--}}
|
||||
{{-- <span class="text-white/90 font-medium">{{ $lastSize ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-white/70">Dauer:--}}
|
||||
{{-- <span class="text-white/90 font-medium">{{ $lastDuration ?? '–' }}</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="pt-1">--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $ok === null--}}
|
||||
{{-- ? 'text-slate-300 border-slate-400/30 bg-slate-500/10'--}}
|
||||
{{-- : ($ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
|
||||
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10') }}">--}}
|
||||
{{-- {{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{--</div>--}}
|
||||
{{--<div wire:key="storage-{{ md5($target ?? '/') }}" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
|
||||
{{-- <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">--}}
|
||||
{{-- <i class="ph ph-archive-box text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] uppercase text-white/70">Backups</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <button wire:click="runNow" class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20">Jetzt sichern</button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="space-y-1 text-sm">--}}
|
||||
{{-- <div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt ?? '–' }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize ?? '–' }}</span></div>--}}
|
||||
{{-- <div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration ?? '–' }}</span></div>--}}
|
||||
{{-- <div>--}}
|
||||
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
|
||||
{{-- {{ $ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">--}}
|
||||
{{-- {{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}--}}
|
||||
{{-- </span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<div wire:poll.2s="refresh" class="p-4">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="shrink-0 w-10 h-10 rounded-full bg-emerald-500/15 border border-emerald-400/30
|
||||
flex items-center justify-center">
|
||||
<i class="ph ph-arrows-clockwise text-xl {{ $state==='done' ? '' : 'animate-spin' }}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-white/90 font-semibold">
|
||||
{{ $state==='done'
|
||||
? ($rc===0 ? 'Update abgeschlossen' : 'Update fehlgeschlagen')
|
||||
: 'Update läuft …' }}
|
||||
</div>
|
||||
<div class="text-xs text-white/60">
|
||||
{{ $line ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{-- Progress --}}
|
||||
<div class="mb-3">
|
||||
<div class="h-2 bg-white/10 rounded overflow-hidden">
|
||||
<div class="h-full bg-emerald-400 transition-all"
|
||||
style="width: {{ $percent }}%"></div>
|
||||
</div>
|
||||
<div class="text-[11px] text-white/50 mt-1">{{ $percent }}%</div>
|
||||
</div>
|
||||
|
||||
{{-- Log Tail --}}
|
||||
<div class="rounded-lg bg-black/40 border border-white/10 p-2 max-h-64 overflow-auto text-[12px] font-mono text-white/80">
|
||||
@foreach($tail as $l)
|
||||
{{ $l }}
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
@if($state==='done' && $rc===0)
|
||||
<button wire:click="$dispatch('closeModal')" class="px-3 py-1 rounded-md
|
||||
bg-emerald-500/10 border border-emerald-400/30 text-emerald-200">
|
||||
Schließen
|
||||
</button>
|
||||
@elseif($state==='done' && $rc!==0)
|
||||
<button wire:click="$dispatch('closeModal')" class="px-3 py-1 rounded-md
|
||||
bg-rose-500/10 border border-rose-400/30 text-rose-200">
|
||||
Schließen
|
||||
</button>
|
||||
@else
|
||||
<button class="px-3 py-1 rounded-md bg-white/10 border border-white/15 text-white/50 cursor-not-allowed" disabled>
|
||||
Bitte warten …
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<span class="text-[11px] tracking-wide uppercase text-white/70">Dienste</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ul class="overflow-auto divide-y divide-white/5 max-h-96">
|
||||
@forelse($servicesCompact as $s)
|
||||
<li class="grid grid-cols-[auto,1fr,auto] items-center gap-3 py-2">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl">
|
||||
<div class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl" wire:poll.30s="refresh">
|
||||
{{-- Kopf --}}
|
||||
<div class="flex items-center justify-between -mb-3">
|
||||
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
|
||||
|
|
@ -12,56 +12,138 @@
|
|||
</div>
|
||||
|
||||
{{-- Inhalt --}}
|
||||
<div class="grid grid-cols-1 items-center">
|
||||
<div class="grid grid-cols-1 items-center gap-4">
|
||||
{{-- Donut --}}
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex items-center justify-center mt-2">
|
||||
<div class="relative" style="width: {{ $diskInnerSize + 80 }}px; height: {{ $diskInnerSize + 80 }}px;">
|
||||
{{-- Innerer grauer Kreis --}}
|
||||
<div class="absolute inset-[36px] rounded-full bg-white/[0.04] backdrop-blur-sm ring-1 ring-white/10"></div>
|
||||
|
||||
{{-- Prozentanzeige im Zentrum --}}
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<div class="text-2xl md:text-3xl font-semibold leading-none tracking-tight">
|
||||
{{ $diskCenterText['percent'] }}
|
||||
{{-- <small class="text-sm text-white/60 font-light ">/ {{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '–' }}</small>--}}
|
||||
</div>
|
||||
<div class="text-[10px] md:text-[11px] text-white/60 mt-1 uppercase tracking-wide">
|
||||
{{ $diskCenterText['label'] }}
|
||||
</div>
|
||||
@if($measuredAt)
|
||||
<div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">
|
||||
zuletzt aktualisiert: <br> {{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- @if($measuredAt)--}}
|
||||
<div class="#absolute bottom-14 mt-2 text-[10px] text-white/45 text-center inline-flex items-center gap-0.5 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
|
||||
<dd class="font-medium tabular-nums text-[10px] md:text-[11px]">Gesamt</dd>
|
||||
<dd class="font-medium tabular-nums text-[10px] md:text-[11px]">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '–' }}</dd>
|
||||
</div>
|
||||
{{-- @endif--}}
|
||||
</div>
|
||||
|
||||
{{-- Segment-Ring --}}
|
||||
@foreach($diskSegments as $seg)
|
||||
<span class="absolute top-1/2 left-1/2 block"
|
||||
style="
|
||||
transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);
|
||||
width: 12px; height: 6px; margin:-3px 0 0 -6px;">
|
||||
style="transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);
|
||||
width: 12px; height: 3px; margin:-3px 0 0 -6px;">
|
||||
<span class="block w-full h-full rounded-full {{ $seg['class'] }}"></span>
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Zahlen --}}
|
||||
<div class="md:pl-2">
|
||||
<dl class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<dt class="text-white/60 text-sm">Gesamt</dt>
|
||||
<dd class="font-medium tabular-nums text-base">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '–' }}</dd>
|
||||
{{-- Zahlen + Stacked Bar --}}
|
||||
<div class="md:pl-2 space-y-4">
|
||||
{{-- Stacked-Bar (horizontale Leiste) --}}
|
||||
@if(!empty($barSegments))
|
||||
<div class="space-y-2">
|
||||
{{-- Legende --}}
|
||||
<div class="flex flex-wrap gap-3 text-[12px] text-white/70">
|
||||
@foreach($barSegments as $b)
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full {{ $b['color'] }}"></span>
|
||||
<span>{{ $b['label'] }} {{ $b['human'] }} ({{ $b['percent'] }}%)</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt class="text-white/60 text-sm">Genutzt</dt>
|
||||
<dd class="font-medium tabular-nums text-base">{{ is_numeric($diskUsedGb) ? $diskUsedGb.' GB' : '–' }}</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt class="text-white/60 text-sm">Frei</dt>
|
||||
<dd class="font-medium tabular-nums text-base">{{ is_numeric($diskFreeGb) ? $diskFreeGb.' GB' : '–' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@endif
|
||||
|
||||
{{-- Zahlen unten rechts --}}
|
||||
{{-- <dl class="space-y-2">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <dt class="text-white/60 text-sm">Gesamt</dt>--}}
|
||||
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '–' }}</dd>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <dt class="text-white/60 text-sm">Genutzt</dt>--}}
|
||||
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskUsedGb) ? $diskUsedGb.' GB' : '–' }}</dd>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <dt class="text-white/60 text-sm">Frei</dt>--}}
|
||||
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskFreeGb) ? $diskFreeGb.' GB' : '–' }}</dd>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </dl>--}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{--<div class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl">--}}
|
||||
{{-- --}}{{-- Kopf --}}
|
||||
{{-- <div class="flex items-center justify-between -mb-3">--}}
|
||||
{{-- <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-hard-drives text-white/70 text-[13px]"></i>--}}
|
||||
{{-- <span class="text-[11px] tracking-wide uppercase text-white/70">Storage</span>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <button wire:click="refresh"--}}
|
||||
{{-- class="inline-flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-white/70 hover:text-white hover:border-white/20 transition">--}}
|
||||
{{-- Update <i class="ph ph-arrows-clockwise text-[12px]"></i>--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Inhalt --}}
|
||||
{{-- <div class="grid grid-cols-1 items-center">--}}
|
||||
{{-- --}}{{-- Donut --}}
|
||||
{{-- <div class="flex items-center justify-center">--}}
|
||||
{{-- <div class="relative" style="width: {{ $diskInnerSize + 80 }}px; height: {{ $diskInnerSize + 80 }}px;">--}}
|
||||
{{-- --}}{{-- Innerer grauer Kreis --}}
|
||||
{{-- <div class="absolute inset-[36px] rounded-full bg-white/[0.04] backdrop-blur-sm ring-1 ring-white/10"></div>--}}
|
||||
|
||||
{{-- --}}{{-- Prozentanzeige im Zentrum --}}
|
||||
{{-- <div class="absolute inset-0 flex flex-col items-center justify-center">--}}
|
||||
{{-- <div class="text-2xl md:text-3xl font-semibold leading-none tracking-tight">--}}
|
||||
{{-- {{ $diskCenterText['percent'] }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="text-[10px] md:text-[11px] text-white/60 mt-1 uppercase tracking-wide">--}}
|
||||
{{-- {{ $diskCenterText['label'] }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @if($measuredAt)--}}
|
||||
{{-- <div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">--}}
|
||||
{{-- zuletzt aktualisiert: <br> {{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Segment-Ring --}}
|
||||
{{-- @foreach($diskSegments as $seg)--}}
|
||||
{{-- <span class="absolute top-1/2 left-1/2 block"--}}
|
||||
{{-- style="--}}
|
||||
{{-- transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);--}}
|
||||
{{-- width: 12px; height: 6px; margin:-3px 0 0 -6px;">--}}
|
||||
{{-- <span class="block w-full h-full rounded-full {{ $seg['class'] }}"></span>--}}
|
||||
{{-- </span>--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
|
||||
{{-- --}}{{-- Zahlen --}}
|
||||
{{-- <div class="md:pl-2">--}}
|
||||
{{-- <dl class="space-y-2">--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <dt class="text-white/60 text-sm">Gesamt</dt>--}}
|
||||
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '–' }}</dd>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <dt class="text-white/60 text-sm">Genutzt</dt>--}}
|
||||
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskUsedGb) ? $diskUsedGb.' GB' : '–' }}</dd>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <div class="flex items-center justify-between">--}}
|
||||
{{-- <dt class="text-white/60 text-sm">Frei</dt>--}}
|
||||
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskFreeGb) ? $diskFreeGb.' GB' : '–' }}</dd>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </dl>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{--</div>--}}
|
||||
|
|
|
|||
|
|
@ -1 +1,10 @@
|
|||
<?php
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Fail2Ban')
|
||||
@section('header_title', 'Fail2Ban')
|
||||
|
||||
@section('content')
|
||||
|
||||
<livewire:ui.security.fail2ban-settings />
|
||||
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ Artisan::command('inspire', function () {
|
|||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::job(RunHealthChecks::class)->everyMinute()->withoutOverlapping();
|
||||
//Schedule::command('woltguard:collect-services')->everyMinute();
|
||||
Schedule::command('spamav:collect')->everyFiveMinutes()->withoutOverlapping();
|
||||
//Schedule::command('mailwolt:check-updates')->dailyAt('04:10');
|
||||
Schedule::command('mailwolt:check-updates')->everytwoMinutes();
|
||||
Schedule::command('rbl:probe')->weeklyOn(0, '3:30')->withoutOverlapping();
|
||||
|
||||
|
||||
Schedule::command('mail:update-stats')->everyFiveMinutes()->withoutOverlapping();
|
||||
Schedule::command('health:probe-disk', ['target' => '/', '--ttl' => 900])->everyTenMinutes();
|
||||
|
|
|
|||
|
|
@ -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