clusev/app/Livewire/Settings/LoginProtection.php

164 lines
5.4 KiB
PHP

<?php
namespace App\Livewire\Settings;
use App\Models\AuditEvent;
use App\Models\BannedIp;
use App\Models\Setting;
use App\Rules\ValidIpOrCidr;
use App\Services\BruteforceGuard;
use App\Support\Confirm\ConfirmToken;
use App\Support\Confirm\InvalidConfirmToken;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use Livewire\Attributes\On;
use Livewire\Component;
class LoginProtection extends Component
{
public bool $enabled = true;
public int $maxretry = 10;
public int $findtime = 10;
public int $bantime = 60;
public string $whitelist = '';
public function mount(BruteforceGuard $guard): void
{
$this->enabled = $guard->enabled();
$this->maxretry = $guard->maxretry();
$this->findtime = $guard->findtime();
$this->bantime = $guard->bantime();
$this->whitelist = implode("\n", $guard->whitelist());
}
public function save(BruteforceGuard $guard): void
{
$this->validate([
'maxretry' => ['required', 'integer', 'min:1', 'max:1000'],
'findtime' => ['required', 'integer', 'min:1', 'max:1440'],
'bantime' => ['required', 'integer', 'min:1', 'max:43200'],
'whitelist' => ['nullable', 'string'],
]);
$lines = array_values(array_filter(array_map('trim', preg_split('/[\r\n,]+/', $this->whitelist) ?: [])));
foreach ($lines as $line) {
if (Validator::make(['v' => $line], ['v' => [new ValidIpOrCidr]])->fails()) {
$this->addError('whitelist', __('settings.lp_whitelist_invalid', ['value' => $line]));
return;
}
}
Setting::put('bruteforce_enabled', $this->enabled ? '1' : '0');
Setting::put('bruteforce_maxretry', (string) $this->maxretry);
Setting::put('bruteforce_findtime', (string) $this->findtime);
Setting::put('bruteforce_bantime', (string) $this->bantime);
Setting::put('bruteforce_whitelist', implode("\n", $lines));
// Widening the whitelist immediately clears any now-exempt bans.
foreach (BannedIp::all() as $ban) {
if ($guard->isExempt($ban->ip)) {
$guard->unban($ban->ip);
}
}
$this->audit('auth.protection_updated', null);
$this->dispatch('notify', message: __('settings.lp_saved'));
}
public function whitelistMyIp(): void
{
$ip = (string) request()->ip();
$lines = array_values(array_filter(array_map('trim', preg_split('/[\r\n,]+/', $this->whitelist) ?: [])));
if (! in_array($ip, $lines, true)) {
$lines[] = $ip;
}
$this->whitelist = implode("\n", $lines);
}
public function confirmUnban(string $ip): void
{
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => __('settings.lp_unban_heading'),
'body' => __('settings.lp_unban_body', ['ip' => $ip]),
'confirmLabel' => __('settings.lp_unban'),
'danger' => false,
'icon' => 'shield',
'notify' => __('settings.lp_unban_notify'),
'token' => ConfirmToken::issue('banCleared', ['ip' => $ip], auditTarget: $ip),
],
);
}
#[On('banCleared')]
public function unban(string $confirmToken, BruteforceGuard $guard): void
{
try {
$payload = ConfirmToken::consume($confirmToken, 'banCleared');
} catch (InvalidConfirmToken) {
return;
}
$ip = (string) $payload['params']['ip'];
$guard->unban($ip);
$this->audit('auth.ip_unbanned', $ip);
}
public function confirmUnbanAll(): void
{
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => __('settings.lp_unban_all_heading'),
'body' => __('settings.lp_unban_all_body'),
'confirmLabel' => __('settings.lp_unban_all'),
'danger' => true,
'icon' => 'shield',
'notify' => __('settings.lp_unban_all_notify'),
'token' => ConfirmToken::issue('bansClearedAll'),
],
);
}
#[On('bansClearedAll')]
public function unbanAll(string $confirmToken): void
{
try {
ConfirmToken::consume($confirmToken, 'bansClearedAll');
} catch (InvalidConfirmToken) {
return; // forged / replayed / direct-bypass attempt — no-op
}
foreach (BannedIp::query()->pluck('ip') as $ip) {
Cache::forget('bruteforce:banned:'.$ip);
}
BannedIp::query()->delete();
$this->audit('auth.ip_unbanned', 'all');
}
private function audit(string $action, ?string $target): void
{
AuditEvent::create([
'user_id' => Auth::id(),
'actor' => Auth::user()?->name ?? 'system',
'action' => $action,
'target' => $target,
'ip' => request()->ip(),
]);
}
public function render(BruteforceGuard $guard)
{
return view('livewire.settings.login-protection', [
'bans' => BannedIp::query()->where('banned_until', '>', now())->orderByDesc('banned_until')->get(),
'currentIp' => (string) request()->ip(),
'currentIpExempt' => $guard->isExempt((string) request()->ip()),
]);
}
}