parent
aaae226c8d
commit
251f2d9c8f
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue