GeoNetAgent, LDAPWeb, server-audit, server-connection
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.
 
 
 
 
 
 

201 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">
<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