You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
13 KiB
203 lines
13 KiB
@extends('layouts.app') |
|
|
|
@section('title', 'Audit Logs — ' . config('app.name')) |
|
|
|
@section('content') |
|
<div class="max-w-[1600px] mx-auto px-4 py-6 sm:py-8"> |
|
@include('partials.nav') |
|
|
|
<div class="mt-4 sm:mt-6 mb-6"> |
|
<h1 class="text-2xl sm:text-3xl font-bold text-white">Audit Logs</h1> |
|
<p class="text-slate-400 mt-1 text-sm">Log login, aksi admin, API, serta akses UI (browser, durasi halaman).</p> |
|
</div> |
|
|
|
@include('partials.flash') |
|
|
|
@if ($error) |
|
<div class="mb-6 rounded-lg bg-amber-950/50 border border-amber-800 text-amber-200 px-4 py-3">{{ $error }}</div> |
|
@endif |
|
|
|
@if (!empty($uiStats['page_views']) || !empty($uiStats['page_durations'])) |
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> |
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl p-5"> |
|
<h2 class="text-lg font-semibold text-white mb-3">UI Access Count (7 hari)</h2> |
|
<div class="space-y-2 text-sm"> |
|
@forelse ($uiStats['page_views'] as $row) |
|
<div class="flex justify-between gap-3 border-b border-slate-800 pb-2"> |
|
<span class="text-slate-300 font-mono text-xs truncate">{{ $row['path'] }}</span> |
|
<span class="text-cyan-300 font-medium shrink-0">{{ $row['count'] }}×</span> |
|
</div> |
|
@empty |
|
<p class="text-slate-500">Belum ada data page view.</p> |
|
@endforelse |
|
</div> |
|
</div> |
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl p-5"> |
|
<h2 class="text-lg font-semibold text-white mb-3">UI Duration (7 hari)</h2> |
|
<div class="space-y-2 text-sm"> |
|
@forelse ($uiStats['page_durations'] as $row) |
|
<div class="flex justify-between gap-3 border-b border-slate-800 pb-2"> |
|
<span class="text-slate-300 font-mono text-xs truncate">{{ $row['path'] }}</span> |
|
<span class="text-violet-300 shrink-0 text-xs"> |
|
avg {{ $row['avg_seconds'] }}s · {{ $row['visits'] }} visit · total {{ gmdate('i\m s\s', $row['total_seconds']) }} |
|
</span> |
|
</div> |
|
@empty |
|
<p class="text-slate-500">Belum ada data durasi.</p> |
|
@endforelse |
|
</div> |
|
</div> |
|
</div> |
|
@endif |
|
|
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden flex flex-col list-panel mb-6"> |
|
<div class="list-panel__toolbar p-4 border-b border-slate-800 flex flex-col md:flex-row gap-3 md:items-center md:justify-between"> |
|
<form method="GET" class="flex flex-wrap gap-2"> |
|
<input name="q" value="{{ $search }}" placeholder="Search user, IP, action..." |
|
class="rounded-lg bg-slate-800 border border-slate-700 px-3 py-2 text-sm text-white min-w-[12rem]"> |
|
<select name="type" class="rounded-lg bg-slate-800 border border-slate-700 px-3 py-2 text-sm text-white"> |
|
<option value="">All types</option> |
|
<option value="web" @selected($actorType === 'web')>Web</option> |
|
<option value="api" @selected($actorType === 'api')>API</option> |
|
</select> |
|
<select name="module" class="rounded-lg bg-slate-800 border border-slate-700 px-3 py-2 text-sm text-white"> |
|
<option value="">All modules</option> |
|
<option value="web.ui" @selected($module === 'web.ui')>UI Access</option> |
|
<option value="auth" @selected($module === 'auth')>Auth</option> |
|
<option value="ldap" @selected($module === 'ldap')>LDAP</option> |
|
<option value="databases" @selected($module === 'databases')>Database</option> |
|
<option value="database-connections" @selected($module === 'database-connections')>DB Connections</option> |
|
<option value="mail-settings" @selected($module === 'mail-settings')>Mail SMTP</option> |
|
<option value="api-clients" @selected($module === 'api-clients')>API Clients</option> |
|
<option value="profile" @selected($module === 'profile')>Profile</option> |
|
<option value="api.v1" @selected($module === 'api.v1')>API v1</option> |
|
</select> |
|
<select name="ip_scope" class="rounded-lg bg-slate-800 border border-slate-700 px-3 py-2 text-sm text-white"> |
|
<option value="">Semua IP</option> |
|
<option value="public" @selected($ipScope === 'public')>Publik</option> |
|
<option value="private" @selected($ipScope === 'private')>Lokal (LAN)</option> |
|
<option value="localhost" @selected($ipScope === 'localhost')>Localhost</option> |
|
</select> |
|
<button class="rounded-lg bg-cyan-600 hover:bg-cyan-500 px-4 py-2 text-sm font-medium text-white">Filter</button> |
|
</form> |
|
<div class="text-sm text-slate-400">{{ count($logs) }} entries · PG {{ $available ? 'connected' : 'unavailable' }}</div> |
|
</div> |
|
<div class="list-panel__scroll"> |
|
<table class="min-w-full text-sm list-table"> |
|
<thead class="text-slate-300"> |
|
<tr> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Time</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Type</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">CRUD / Event</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">User / Client</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">IP</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Client</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Action</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Resource</th> |
|
<th class="px-4 py-3 text-left bg-slate-800 border-b border-slate-700">Duration</th> |
|
</tr> |
|
</thead> |
|
<tbody class="divide-y divide-slate-800"> |
|
@forelse ($logs as $log) |
|
@php |
|
$client = $log->metadata['client'] ?? []; |
|
$clientLine = collect([ |
|
isset($client['browser']) ? $client['browser'].($client['browser_version'] ?? '' ? ' '.$client['browser_version'] : '') : null, |
|
isset($client['os']) ? $client['os'].($client['os_version'] ?? '' ? ' '.$client['os_version'] : '') : null, |
|
])->filter()->implode(' · '); |
|
$durationSec = $log->metadata['duration_seconds'] ?? null; |
|
@endphp |
|
<tr class="hover:bg-slate-800/40 align-top"> |
|
<td class="px-4 py-3 text-slate-400 whitespace-nowrap text-xs">{{ $log->occurred_at?->format('Y-m-d H:i:s') }}</td> |
|
<td class="px-4 py-3"> |
|
<span class="text-xs rounded px-2 py-0.5 {{ $log->actor_type === 'api' ? 'bg-violet-950 text-violet-300' : 'bg-cyan-950 text-cyan-300' }}"> |
|
{{ strtoupper($log->actor_type) }} |
|
</span> |
|
</td> |
|
<td class="px-4 py-3 text-slate-400 text-xs uppercase"> |
|
{{ $log->metadata['event'] ?? ($log->metadata['crud'] ?? ($log->metadata['http_method'] ?? '—')) }} |
|
</td> |
|
<td class="px-4 py-3 text-white text-xs"> |
|
{{ $log->actor_username ?? '—' }} |
|
@if ($log->actor_client) |
|
<div class="text-slate-500">{{ $log->actor_client }}</div> |
|
@endif |
|
</td> |
|
<td class="px-4 py-3 whitespace-nowrap text-xs"> |
|
@if ($log->ip_address) |
|
<span class="text-white font-mono">{{ $log->ip_address }}</span> |
|
@php $scope = $log->metadata['ip_scope'] ?? null; @endphp |
|
@if ($scope) |
|
<span class="ml-1 text-xs rounded px-1.5 py-0.5 |
|
{{ $scope === 'public' ? 'bg-amber-950 text-amber-300' : ($scope === 'private' ? 'bg-emerald-950 text-emerald-300' : 'bg-slate-800 text-slate-400') }}"> |
|
{{ $scope === 'public' ? 'Publik' : ($scope === 'private' ? 'Lokal' : ($scope === 'localhost' ? 'Localhost' : $scope)) }} |
|
</span> |
|
@endif |
|
@if (!empty($client['webrtc_ip']) && $client['webrtc_ip'] !== $log->ip_address) |
|
<div class="text-slate-500 font-mono mt-0.5" title="WebRTC">RTC: {{ $client['webrtc_ip'] }}</div> |
|
@endif |
|
@else |
|
<span class="text-slate-500">—</span> |
|
@endif |
|
</td> |
|
<td class="px-4 py-3 text-xs text-slate-400 max-w-[11rem]"> |
|
@if ($clientLine !== '') |
|
<div class="text-slate-300">{{ $clientLine }}</div> |
|
@endif |
|
@if (!empty($client['screen'])) |
|
<div>{{ $client['screen'] }}</div> |
|
@endif |
|
@if (!empty($client['cpu_cores'])) |
|
<div>{{ $client['cpu_cores'] }} cores@if(!empty($client['device_memory_gb'])) · {{ $client['device_memory_gb'] }} GB RAM@endif</div> |
|
@endif |
|
@if (!empty($client['gpu'])) |
|
<div class="truncate" title="{{ $client['gpu'] }}">GPU: {{ Str::limit($client['gpu'], 28) }}</div> |
|
@endif |
|
@if (!empty($client['language']) || !empty($client['timezone'])) |
|
<div class="text-slate-500">{{ $client['language'] ?? '' }}@if(!empty($client['timezone'])) · {{ $client['timezone'] }}@endif</div> |
|
@endif |
|
@if ($clientLine === '' && empty($client['screen'])) |
|
<span class="text-slate-500">—</span> |
|
@endif |
|
</td> |
|
<td class="px-4 py-3 text-slate-300 text-xs">{{ $log->action }}</td> |
|
<td class="px-4 py-3 text-slate-400 text-xs">{{ $log->resource ?? $log->path ?? '—' }}</td> |
|
<td class="px-4 py-3 text-slate-400 text-xs whitespace-nowrap"> |
|
@if ($durationSec !== null) |
|
{{ $durationSec }}s |
|
@else |
|
— |
|
@endif |
|
</td> |
|
</tr> |
|
@empty |
|
<tr><td colspan="9" class="px-4 py-8 text-center text-slate-400">No audit logs yet.</td></tr> |
|
@endforelse |
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
<aside class="bg-slate-900 border border-slate-800 rounded-2xl p-5 notes-panel"> |
|
<h2 class="text-lg font-semibold text-white mb-4">Catatan</h2> |
|
<dl class="space-y-3 text-sm text-slate-400"> |
|
<div> |
|
<dt class="font-medium text-cyan-400 mb-1">Web audit client</dt> |
|
<dd>Browser, OS, resolusi, CPU cores, RAM (perkiraan), GPU WebGL, bahasa, timezone, dan IP (server + WebRTC jika tersedia) dikumpulkan otomatis saat login.</dd> |
|
</div> |
|
<div> |
|
<dt class="font-medium text-violet-400 mb-1">UI count & duration</dt> |
|
<dd><code class="text-cyan-300">web.ui.view</code> = kunjungan halaman. <code class="text-cyan-300">web.ui.duration</code> = lama aktif di halaman (dikirim saat pindah/tutup tab).</dd> |
|
</div> |
|
<div> |
|
<dt class="font-medium text-amber-400 mb-1">IP Address</dt> |
|
<dd>IP server dari reverse proxy. WebRTC IP opsional untuk deteksi IP lokal client.</dd> |
|
</div> |
|
<div> |
|
<dt class="font-medium text-emerald-400 mb-1">CRUD & API</dt> |
|
<dd>Semua POST/PUT/DELETE web & API mutasi tercatat dengan konteks client dari session.</dd> |
|
</div> |
|
</dl> |
|
</aside> |
|
</div> |
|
@endsection
|
|
|