164 lines
5.5 KiB
PHP
164 lines
5.5 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\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 active bans.
|
|
foreach (BannedIp::active()->get() 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, BruteforceGuard $guard): void
|
|
{
|
|
try {
|
|
ConfirmToken::consume($confirmToken, 'bansClearedAll');
|
|
} catch (InvalidConfirmToken) {
|
|
return; // forged / replayed / direct-bypass attempt — no-op
|
|
}
|
|
$guard->unbanAll();
|
|
$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)
|
|
{
|
|
$ip = (string) request()->ip();
|
|
|
|
return view('livewire.settings.login-protection', [
|
|
'bans' => BannedIp::query()->where('banned_until', '>', now())->orderByDesc('banned_until')->limit(200)->get(),
|
|
'currentIp' => $ip,
|
|
'currentIpExempt' => $guard->isExempt($ip),
|
|
// Read-only status of the env-driven honeypot deception layer (no toggle here).
|
|
'honeypotEnabled' => (bool) config('clusev.honeypot.enabled'),
|
|
]);
|
|
}
|
|
}
|