Files
Mark Gregory 7fb71c4d24 Central Security Centre
* Security policy management
* Minimum password-length policy
* Mixed-case, number, and symbol requirements
* Password expiry and history settings
* MFA requirement policy
* Failed-login and lockout settings
* Session idle-time policy
* Single-session policy
* Automatic successful-login recording
* Automatic failed-login recording
* Automatic logout recording
* Authentication history
* IP allow and deny rules
* IPv4 CIDR support
* Administration and login rule scopes
* Trusted-device storage and revocation
* Security middleware registration
* Administration navigation
* Security permissions
* Module registration and tests
2026-07-24 12:30:40 +01:00

6 lines
5.2 KiB
PHP

<x-layouts.app :title="__('Security Centre')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Security Centre</h1><p class="text-sm text-zinc-500">Identity policy, authentication history, trusted devices and network access controls.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif
<div class="grid gap-4 md:grid-cols-4"><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Failed logins (24h)</div><div class="text-2xl font-semibold">{{ $failed24h }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Active trusted devices</div><div class="text-2xl font-semibold">{{ $devices->count() }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">IP rules</div><div class="text-2xl font-semibold">{{ $rules->count() }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">MFA required</div><div class="text-2xl font-semibold">{{ $policy?->require_mfa ? 'Yes' : 'No' }}</div></div></div>
<div class="grid gap-6 xl:grid-cols-2"><form method="POST" action="{{ route('administration.security.policy.update') }}" class="space-y-4 rounded-xl border p-5">@csrf @method('PUT')<h2 class="font-semibold">Security policy</h2><div class="grid gap-4 md:grid-cols-2"><flux:input name="minimum_password_length" type="number" label="Minimum password length" value="{{ old('minimum_password_length',$policy?->minimum_password_length ?? 12) }}"/><flux:input name="password_expiry_days" type="number" label="Password expiry days" value="{{ old('password_expiry_days',$policy?->password_expiry_days) }}"/><flux:input name="password_history" type="number" label="Password history" value="{{ old('password_history',$policy?->password_history ?? 5) }}"/><flux:input name="max_failed_logins" type="number" label="Maximum failed logins" value="{{ old('max_failed_logins',$policy?->max_failed_logins ?? 10) }}"/><flux:input name="lockout_minutes" type="number" label="Lockout minutes" value="{{ old('lockout_minutes',$policy?->lockout_minutes ?? 15) }}"/><flux:input name="session_idle_minutes" type="number" label="Session idle minutes" value="{{ old('session_idle_minutes',$policy?->session_idle_minutes ?? 120) }}"/></div><div class="grid gap-3 md:grid-cols-2"><flux:checkbox name="require_mixed_case" label="Require mixed case" :checked="$policy?->require_mixed_case ?? true"/><flux:checkbox name="require_numbers" label="Require numbers" :checked="$policy?->require_numbers ?? true"/><flux:checkbox name="require_symbols" label="Require symbols" :checked="$policy?->require_symbols ?? true"/><flux:checkbox name="require_mfa" label="Require multi-factor authentication" :checked="$policy?->require_mfa ?? false"/><flux:checkbox name="single_session" label="Allow one session per user" :checked="$policy?->single_session ?? false"/></div><flux:button type="submit" variant="primary">Save policy</flux:button></form>
<form method="POST" action="{{ route('administration.security.rules.store') }}" class="space-y-4 rounded-xl border p-5">@csrf<h2 class="font-semibold">Add IP rule</h2><flux:input name="name" label="Rule name" required/><div class="grid gap-4 md:grid-cols-2"><flux:select name="type" label="Action"><option value="deny">Deny</option><option value="allow">Allow</option></flux:select><flux:input name="address" label="IP address or IPv4 CIDR" placeholder="192.0.2.10 or 10.0.0.0/8" required/></div><flux:textarea name="description" label="Description"/><div class="flex gap-5"><flux:checkbox name="applies_to_admin" label="Administration" checked/><flux:checkbox name="applies_to_login" label="Login" checked/></div><flux:button type="submit" variant="primary">Create rule</flux:button><div class="space-y-2">@foreach($rules as $rule)<div class="flex items-center justify-between rounded border p-3"><div><span class="font-medium">{{ $rule->name }}</span><span class="ml-2 text-xs uppercase text-zinc-500">{{ $rule->type }}</span><div class="text-sm text-zinc-500">{{ $rule->address }}</div></div><form method="POST" action="{{ route('administration.security.rules.destroy',$rule) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></div>@endforeach</div></form></div>
<div class="rounded-xl border"><div class="border-b p-4 font-semibold">Authentication history</div><div class="overflow-x-auto"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">When</th><th class="p-3 text-left">Event</th><th class="p-3 text-left">User</th><th class="p-3 text-left">IP address</th><th class="p-3 text-left">Client</th></tr></thead><tbody>@forelse($events as $event)<tr class="border-t"><td class="p-3">{{ $event->occurred_at?->format('d M Y H:i:s') }}</td><td class="p-3 capitalize">{{ $event->event }}</td><td class="p-3">{{ $event->email ?: 'Unknown' }}</td><td class="p-3">{{ $event->ip_address ?: '—' }}</td><td class="max-w-md truncate p-3 text-zinc-500">{{ $event->user_agent ?: '—' }}</td></tr>@empty<tr><td colspan="5" class="p-6 text-center text-zinc-500">No authentication events recorded.</td></tr>@endforelse</tbody></table></div><div class="p-4">{{ $events->links() }}</div></div></div></x-layouts.app>