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

main v1.0.98
boban 2025-10-29 18:34:08 +01:00
parent aaae226c8d
commit 251f2d9c8f
10 changed files with 246 additions and 40 deletions

View File

@ -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);
}
}

View File

@ -6,29 +6,116 @@ use Livewire\Component;
class Fail2BanCard extends Component
{
public int $activeBans = 0;
public array $topIps = []; // [['ip'=>'1.2.3.4','count'=>12],...]
public bool $available = true; // ob fail2ban vorhanden ist
public int $activeBans = 0; // Summe über alle Jails
/** @var array<int,array{name:string,banned:int,ips:array<int,string>}> */
public array $jails = []; // je Jail: Name, Anzahl, IPs (gekürzt)
/** @var array<int,array{ip:string,count:int}> */
public array $topIps = []; // Top IPs aus Log/Journal (Ban-Events)
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');
}
public function refresh(): void
{
$this->load(true);
}
protected function load(bool $force = false): void
{
// 0) vorhanden?
$bin = trim((string) @shell_exec('command -v fail2ban-client 2>/dev/null')) ?: '';
if ($bin === '') {
$this->available = false;
$this->activeBans = 0;
$this->jails = [];
$this->topIps = [];
return;
}
// 1) Jails ermitteln
$status = (string) (@shell_exec("timeout 2 $bin status 2>/dev/null") ?? '');
$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) {
$s = (string) (@shell_exec("timeout 2 $bin status ".escapeshellarg($j)." 2>/dev/null") ?? '');
$banned = (int) ($this->firstMatch('/Currently banned:\s+(\d+)/i', $s) ?: 0);
// „Banned IP list:“ kann fehlen → leeres Array
$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,
// nur die ersten 8 zur UI-Anzeige
'ips' => array_slice($ips, 0, 8),
];
$total += $banned;
}
$this->available = true;
$this->activeBans = $total;
$this->jails = $rows;
// 2) Top-IPs aus den letzten Ban-Events
$this->topIps = $this->collectTopIps();
}
/** Extrahiert erste Regex-Gruppe oder null */
private function firstMatch(string $pattern, string $haystack): ?string
{
return preg_match($pattern, $haystack, $m) ? trim($m[1]) : null;
}
/** Zählt „Ban <IP>“ aus fail2ban.log (Fallback: journalctl) */
private function collectTopIps(): array
{
$lines = '';
if (is_readable('/var/log/fail2ban.log')) {
// letzte 500 Zeilen reichen völlig
$lines = (string) (@shell_exec('tail -n 500 /var/log/fail2ban.log 2>/dev/null') ?? '');
}
if ($lines === '') {
// Fallback: Journal (falls rsyslog die Datei nicht schreibt)
$lines = (string) (@shell_exec('timeout 2 journalctl -u fail2ban -n 500 --no-pager 2>/dev/null') ?? '');
}
if ($lines === '') {
return [];
}
// Nur Ban-Events, IP extrahieren
$ips = [];
foreach (preg_split('/\R+/', $lines) as $ln) {
if (stripos($ln, 'Ban ') === false) continue;
if (preg_match('/\b(\d{1,3}(?:\.\d{1,3}){3})\b/', $ln, $m)) {
$ip = $m[1];
$ips[$ip] = ($ips[$ip] ?? 0) + 1;
}
}
$this->topIps = $rows;
if (!$ips) return [];
arsort($ips);
$top = array_slice($ips, 0, 5, true);
$out = [];
foreach ($top as $ip => $cnt) {
$out[] = ['ip' => $ip, 'count' => (int)$cnt];
}
return $out;
}
}

View File

@ -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)

View File

@ -1,5 +1,5 @@
{{-- resources/views/auth/login.blade.php --}}
@extends('layouts.app')
@extends('layouts.blank')
@section('title', 'Login')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('layouts.blank')
@section('title', 'Konto erstellen')

View File

@ -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>--}}

View File

@ -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>

View File

@ -7,6 +7,7 @@
<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>
@ -16,7 +17,7 @@
@forelse($rows as $r)
<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">
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'])

View File

@ -4,20 +4,65 @@
<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>
@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">
@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"
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>
@endif
</div>

View File

@ -54,9 +54,7 @@
<div class="mt-5 flex justify-center">
<button wire:click="refresh"
class="inline-flex items-center gap-1.5 rounded-full text-[12px] px-4 py-1.5
text-white/80 bg-white/10 border border-white/15
hover:bg-white/15 hover:text-white transition">
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>