Files
Htmx/templates/audit/list.html
T

259 lines
18 KiB
HTML

{% extends "base.html" %}
{% import "components/macros.html" as ui %}
{% block title %}Audit Logs - Stick{% endblock %}
{% block content %}
<div class="grow py-12 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto w-full">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8 pb-6 border-b border-slate-900 gap-4">
<div>
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight">Audit Logs</h1>
<p class="text-slate-400 text-sm mt-1">Review, search, and audit system activities and state changes</p>
</div>
<div class="flex items-center gap-3">
<button data-modal-target="purge-modal" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-4 py-2.5 bg-rose-950/40 border border-rose-900/30 hover:bg-rose-950/60 text-rose-400 shadow-md shadow-rose-950/10">
Purge Logs
</button>
<a href="/auth/users" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-4 py-2.5 bg-slate-900 border border-slate-800 hover:bg-slate-800 text-slate-300">
Manage Users
</a>
<span class="text-xs font-semibold px-3 py-1.5 rounded-xl bg-slate-900 border border-slate-800 text-slate-300">
Matches Found: {{ logs.len() }}
</span>
</div>
</div>
<!-- Filters Form -->
<div class="bg-[#1e293b]/40 backdrop-blur-xl border border-slate-900 rounded-3xl p-6 shadow-xl mb-8">
<h3 class="text-sm font-bold text-slate-200 mb-4">Filter Log Entries</h3>
<form method="get" action="/auth/audit" class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4 items-end">
<!-- User Filter -->
<div>
<label for="username" class="block text-[11px] font-semibold text-slate-400 mb-1.5">User/Username</label>
<input type="text" id="username" name="username"
value="{% if let Some(u) = query.username %}{{ u }}{% endif %}"
placeholder="Filter by user"
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
</div>
<!-- Action Type Filter -->
<div>
<label for="action_type" class="block text-[11px] font-semibold text-slate-400 mb-1.5">Event Type</label>
<select id="action_type" name="action_type"
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
<option value="all" {% if let Some(a) = query.action_type %}{% if a == "all" %}selected{% endif %}{% endif %}>All Actions</option>
<option value="Login" {% if let Some(a) = query.action_type %}{% if a == "Login" %}selected{% endif %}{% endif %}>Login</option>
<option value="Logout" {% if let Some(a) = query.action_type %}{% if a == "Logout" %}selected{% endif %}{% endif %}>Logout</option>
<option value="Register" {% if let Some(a) = query.action_type %}{% if a == "Register" %}selected{% endif %}{% endif %}>Register</option>
<option value="Create" {% if let Some(a) = query.action_type %}{% if a == "Create" %}selected{% endif %}{% endif %}>Create</option>
<option value="Update" {% if let Some(a) = query.action_type %}{% if a == "Update" %}selected{% endif %}{% endif %}>Update</option>
<option value="Delete" {% if let Some(a) = query.action_type %}{% if a == "Delete" %}selected{% endif %}{% endif %}>Delete</option>
<option value="View" {% if let Some(a) = query.action_type %}{% if a == "View" %}selected{% endif %}{% endif %}>View</option>
<option value="Search" {% if let Some(a) = query.action_type %}{% if a == "Search" %}selected{% endif %}{% endif %}>Search</option>
</select>
</div>
<!-- Entity Type Filter -->
<div>
<label for="entity_type" class="block text-[11px] font-semibold text-slate-400 mb-1.5">Entity Type</label>
<select id="entity_type" name="entity_type"
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
<option value="all" {% if let Some(e) = query.entity_type %}{% if e == "all" %}selected{% endif %}{% endif %}>All Entities</option>
<option value="User" {% if let Some(e) = query.entity_type %}{% if e == "User" %}selected{% endif %}{% endif %}>User</option>
<option value="Task" {% if let Some(e) = query.entity_type %}{% if e == "Task" %}selected{% endif %}{% endif %}>Task</option>
<option value="Developer" {% if let Some(e) = query.entity_type %}{% if e == "Developer" %}selected{% endif %}{% endif %}>Developer</option>
<option value="System" {% if let Some(e) = query.entity_type %}{% if e == "System" %}selected{% endif %}{% endif %}>System</option>
</select>
</div>
<!-- Specific Object ID -->
<div>
<label for="entity_id" class="block text-[11px] font-semibold text-slate-400 mb-1.5">Target Entity ID</label>
<input type="text" id="entity_id" name="entity_id"
value="{% if let Some(e_id) = query.entity_id %}{{ e_id }}{% endif %}"
placeholder="e.g. 6a12ab..."
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
</div>
<!-- Start Date -->
<div>
<label for="start_date" class="block text-[11px] font-semibold text-slate-400 mb-1.5">Start Date</label>
<input type="date" id="start_date" name="start_date"
value="{% if let Some(s) = query.start_date %}{{ s }}{% endif %}"
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
</div>
<!-- End Date -->
<div>
<label for="end_date" class="block text-[11px] font-semibold text-slate-400 mb-1.5">End Date</label>
<input type="date" id="end_date" name="end_date"
value="{% if let Some(e) = query.end_date %}{{ e }}{% endif %}"
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-slate-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
</div>
<!-- Submit & Reset Buttons -->
<div class="md:col-span-3 lg:col-span-6 flex justify-end gap-3 mt-2">
<a href="/auth/audit" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all border border-border bg-transparent hover:bg-secondary text-slate-200 px-5 py-2.5">
Clear Filters
</a>
<button type="submit" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-5 py-2.5 bg-indigo-650 hover:bg-indigo-600 text-white shadow-lg shadow-indigo-650/10">
Apply Filters
</button>
</div>
</form>
</div>
<!-- Logs Table -->
{{ ui::table_container_open(id="audit-table", has_top_scroll=true, max_height="68vh") }}
<table class="min-w-full divide-y divide-slate-800 text-left text-sm text-slate-300 relative">
<thead class="shadow-[0_1px_0_0_rgba(255,255,255,0.05)]">
<tr class="text-xs font-bold text-slate-400 uppercase tracking-wider">
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031]">Timestamp</th>
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031]">User</th>
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031]">Event</th>
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031]">Target Entity</th>
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031]">IP / Details</th>
<th class="sticky top-0 z-10 px-6 py-4 bg-[#162031] text-right">Replay Payload</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
{% if logs.is_empty() %}
<tr>
<td colspan="6" class="px-6 py-12 text-center text-slate-500 font-medium">
No audit log entries matched the filter criteria.
</td>
</tr>
{% else %}
{% for log in logs %}
<tr class="hover:bg-[#1e293b]/10 transition duration-150 align-top">
<!-- Timestamp -->
<td class="px-6 py-4 whitespace-nowrap text-xs font-semibold text-slate-400">
{{ log.timestamp.format("%Y-%m-%d %H:%M:%S UTC") }}
</td>
<!-- User -->
<td class="px-6 py-4 whitespace-nowrap">
{% if let Some(uname) = log.username %}
<span class="text-sm font-semibold text-slate-200">{{ uname }}</span>
{% if let Some(uid) = log.user_id %}
<div class="text-[9px] font-mono text-slate-500">{{ uid.to_hex() }}</div>
{% endif %}
{% else %}
<span class="text-xs italic text-slate-500">Anonymous</span>
{% endif %}
</td>
<!-- Event / Action Type -->
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border
{% if log.action_type == "Delete" %}
bg-rose-500/10 text-rose-400 border-rose-500/20
{% elif log.action_type == "Create" %}
bg-emerald-500/10 text-emerald-400 border-emerald-500/20
{% elif log.action_type == "Update" %}
bg-sky-500/10 text-sky-400 border-sky-500/20
{% elif log.action_type == "Login" %}
bg-indigo-500/10 text-indigo-400 border-indigo-500/20
{% elif log.action_type == "Logout" %}
bg-amber-500/10 text-amber-400 border-amber-500/20
{% else %}
bg-slate-900 text-slate-400 border-slate-800
{% endif %}">
{{ log.action_type }}
</span>
</td>
<!-- Target Entity -->
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-semibold text-slate-300">{{ log.entity_type }}</span>
{% if let Some(e_id) = log.entity_id %}
<div class="text-[9px] font-mono text-sky-400 hover:underline cursor-pointer" onclick="document.getElementById('entity_id').value='{{ e_id.to_hex() }}';">
{{ e_id.to_hex() }}
</div>
{% endif %}
</td>
<!-- IP / Details -->
<td class="px-6 py-4">
<div class="text-xs text-slate-200">
{% if let Some(det) = log.details %}
{{ det }}
{% endif %}
</div>
<div class="flex items-center gap-2 mt-1 text-[9px] text-slate-500 font-medium">
{% if let Some(ip) = log.ip_address %}
<span class="bg-slate-900 border border-slate-800 px-1.5 py-0.5 rounded">IP: {{ ip }}</span>
{% endif %}
{% if let Some(ua) = log.user_agent %}
<span class="truncate max-w-xs bg-slate-900 border border-slate-800 px-1.5 py-0.5 rounded" title="{{ ua }}">{{ ua }}</span>
{% endif %}
</div>
</td>
<!-- Replay Payload -->
<td class="px-6 py-4 whitespace-nowrap text-right">
{% if !log.formatted_payload().is_empty() %}
<details class="group text-left">
<summary class="inline-flex items-center gap-1 text-[11px] font-bold text-sky-400 hover:text-sky-300 cursor-pointer list-none select-none">
<span>View Payload</span>
<svg class="w-3.5 h-3.5 transition-transform duration-200 group-open:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
<polyline points="6 9 12 15 18 9"/>
</svg>
</summary>
<div class="mt-2 p-3 bg-slate-950 border border-slate-900 rounded-xl max-w-md overflow-x-auto text-[10px] font-mono text-emerald-400/90 whitespace-pre scrollbar-thin">{{ log.formatted_payload() }}</div>
</details>
{% else %}
<span class="text-xs italic text-slate-650">No payload</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
{{ ui::table_container_close(id="audit-table", has_top_scroll=true) }}
<!-- Footer Navigation -->
<div class="mt-6 text-center text-sm text-slate-400">
<a href="/auth/password" class="font-medium text-sky-400 hover:underline">← Back to Account Settings</a>
</div>
</div>
<!-- Purge Modal Dialog -->
<div id="purge-modal" class="modal-dialog fixed inset-0 z-50 items-center justify-center hidden" role="dialog" aria-modal="true">
<div class="modal-backdrop fixed inset-0 bg-[#07090e]/80 backdrop-blur-sm transition-opacity duration-300"></div>
<div class="modal-content relative z-10 w-full max-w-md scale-95 opacity-0 transition-all duration-300 border border-border bg-popover/95 backdrop-blur-xl p-6 shadow-2xl rounded-3xl">
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-full bg-rose-500/10 border border-rose-500/20 text-rose-400 mb-3">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</div>
<h3 class="text-sm font-bold text-slate-100 text-center">Purge Audit Logs</h3>
<p class="text-xs text-slate-400 mt-2 text-center leading-relaxed">
Specify the number of days of logs to retain. This operation will permanently delete all logs older than this window. You will be forced to download a JSON archival dump of the purged logs before deletion.
</p>
<form method="POST" action="/auth/audit/purge" class="mt-4 space-y-4">
<div>
<label for="retention_days" class="block text-[11px] font-semibold text-slate-400 mb-1.5 text-left">Retention Period (Days)</label>
<input type="number" id="retention_days" name="retention_days" min="1" value="30" required
class="block h-10 w-full rounded-xl border border-border bg-background px-4 py-2 text-sm text-white placeholder-slate-650 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/50">
<span class="text-[10px] text-slate-500 mt-1 block text-left">Minimum 1 day of logs must be retained.</span>
</div>
<div class="flex gap-3 pt-2">
<button type="button" class="modal-close flex-1 py-2.5 rounded-xl bg-secondary border border-border hover:bg-slate-800 transition text-xs font-semibold text-slate-200">
Cancel
</button>
<button type="submit" class="flex-1 py-2.5 rounded-xl bg-rose-650 hover:bg-rose-600 text-white shadow-lg shadow-rose-600/10 transition text-xs font-bold" onclick="setTimeout(() => { window.closeModal(document.getElementById('purge-modal')); window.location.reload(); }, 1500)">
Archived Purge
</button>
</div>
</form>
</div>
</div>
{% endblock %}