feat: implement audit logs system, request extractor, admin log panel, and dedicated documentation
- Added an enterprise-grade, request-scoped AuditLogger extractor in Axum. - Configured MongoDB persistence for structured, replayable audit logs (capturing timestamp, user, action, type, payload snapshot, client IP with proxy header support, and User-Agent). - Created a live Administrator console at /auth/audit to filter and inspect log events. - Re-architected documentation by moving Design Wiki pages out of /components into a dedicated /docs route. - Published logging architecture documentation at /docs/logging.
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
{% 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">
|
||||
<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 -->
|
||||
<div class="bg-[#1e293b]/40 backdrop-blur-xl border border-slate-900 rounded-3xl shadow-2xl overflow-hidden mb-8">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-800 text-left text-sm text-slate-300">
|
||||
<thead>
|
||||
<tr class="text-xs font-bold text-slate-400 uppercase tracking-wider bg-slate-900/30">
|
||||
<th class="px-6 py-4">Timestamp</th>
|
||||
<th class="px-6 py-4">User</th>
|
||||
<th class="px-6 py-4">Event</th>
|
||||
<th class="px-6 py-4">Target Entity</th>
|
||||
<th class="px-6 py-4">IP / Details</th>
|
||||
<th class="px-6 py-4 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
{% endblock %}
|
||||
@@ -13,6 +13,9 @@
|
||||
<p class="text-slate-400 text-sm mt-1">Manage system access, toggle roles, and provision credentials</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/auth/audit" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-4 py-2.5 bg-indigo-650 hover:bg-indigo-600 text-white shadow-md shadow-indigo-600/10">
|
||||
View Audit Logs
|
||||
</a>
|
||||
<a href="/auth/register" class="inline-flex items-center justify-center rounded-xl text-xs font-bold transition-all px-4 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white shadow-md shadow-emerald-500/10">
|
||||
Register New User
|
||||
</a>
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="flex items-center space-x-4">
|
||||
<a href="/components" class="text-sm font-medium text-muted-foreground hover:text-white transition py-2 px-3 rounded-lg hover:bg-secondary">
|
||||
Design Wiki
|
||||
<a href="/docs" class="text-sm font-medium text-muted-foreground hover:text-white transition py-2 px-3 rounded-lg hover:bg-secondary">
|
||||
Documentation
|
||||
</a>
|
||||
{% if authenticated %}
|
||||
<a href="/tasks" class="text-sm font-medium text-muted-foreground hover:text-white transition py-2 px-3 rounded-lg hover:bg-secondary">
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<aside class="lg:w-64 shrink-0">
|
||||
<div class="sticky top-24 space-y-1.5 p-4 rounded-3xl border border-border bg-card/50 backdrop-blur-xl" id="wiki-sidebar">
|
||||
<span class="px-3 text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-2">Wiki Navigation</span>
|
||||
<a href="/components" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components">Introduction</a>
|
||||
<div class="h-px bg-secondary my-1 mx-2"></div>
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Actions</span>
|
||||
<a href="/components/buttons" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/buttons">Buttons</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Forms & Inputs</span>
|
||||
<a href="/components/inputs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/inputs">Form Fields & Select</a>
|
||||
<a href="/components/date-time" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/date-time">Date & Time Pickers</a>
|
||||
<a href="/components/combobox" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/combobox">Autocomplete (Combobox)</a>
|
||||
<a href="/components/toggles" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/toggles">Switches & Checkboxes</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Overlays</span>
|
||||
<a href="/components/modals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/modals">Dialog Modals</a>
|
||||
<a href="/components/sheets" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/sheets">Slide-over Drawers</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Layout & Navigation</span>
|
||||
<a href="/components/tabs-accordion" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/tabs-accordion">Tabs & Accordions</a>
|
||||
<a href="/components/scrollbars" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/scrollbars">Custom Scrollbars</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Visuals & Feedback</span>
|
||||
<a href="/components/visuals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/visuals">Avatars & Badges</a>
|
||||
<a href="/components/feedback" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/components/feedback">Toasts & Alerts</a>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Floating Sidebar Navigation -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 space-y-10">
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
<h3 class="text-sm font-bold text-slate-200">Vertical Feature Architecture</h3>
|
||||
<p class="text-xs text-muted-foreground/90 leading-relaxed">
|
||||
Components are packaged inside feature directories (e.g. <code>src/components/</code>) rather than spread horizontally. Handlers render Askama templates, static JS, and compiled Tailwind assets dynamically.
|
||||
The backend routes and documentation handlers are located under <code>src/docs/</code>. Shared components reside in <code>templates/components/</code>. The rest of the application is packaged inside clean vertical feature directories (e.g. <code>src/auth/</code>, <code>src/tasks/</code>, <code>src/audit/</code>).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -0,0 +1,240 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "components/macros.html" as ui %}
|
||||
|
||||
{% block title %}Audit Logging - Documentation{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="pb-6 border-b border-border">
|
||||
<span class="text-xs font-semibold text-indigo-400">Architecture / Operations</span>
|
||||
<h1 class="text-3xl font-extrabold text-slate-100 tracking-tight mt-1">Audit Logging Architecture</h1>
|
||||
<p class="text-muted-foreground text-sm mt-2 leading-relaxed">
|
||||
A first-class, request-scoped auditing framework built to log and replay critical user mutations (Create, Read, Update, Delete, Search) transparently. Powered by Axum extractors, MongoDB, and JSON payloads.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Section: Design Philosophy -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-slate-200">1. Architectural Philosophy</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
In enterprise-grade software, logs are not secondary diagnostics. They are an <strong>immutable audit trail</strong> that ensures accountability and replayability. Every critical event records the state of the entity at the time of modification, who performed it, and their network metadata.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
|
||||
<div class="border border-border bg-secondary/5 rounded-2xl p-5 space-y-2">
|
||||
<span class="text-xs font-bold text-sky-400">Request-Scoped Context</span>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
No manual extraction of IP addresses, headers, or cookies. The system resolves all metadata implicitly via request parts.
|
||||
</p>
|
||||
</div>
|
||||
<div class="border border-border bg-secondary/5 rounded-2xl p-5 space-y-2">
|
||||
<span class="text-xs font-bold text-emerald-400">State Snapshotting</span>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
Snapshots are preserved as raw JSON payloads, making historical state transitions fully auditable and replayable.
|
||||
</p>
|
||||
</div>
|
||||
<div class="border border-border bg-secondary/5 rounded-2xl p-5 space-y-2">
|
||||
<span class="text-xs font-bold text-indigo-400">Administrator Console</span>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
A real-time search interface available to authorized administrators, supporting filtering by user, action, type, and timeline.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section: How it Works (Extractor) -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-slate-200">2. The AuditLogger Extractor</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Rather than cluttering handlers with boilerplate code to parse headers and look up user accounts, developers can leverage the custom Axum <code>AuditLogger</code> extractor. This struct implements Axum's <code>FromRequestParts</code> trait.
|
||||
</p>
|
||||
|
||||
<div class="border border-border rounded-3xl p-5 bg-secondary/10 space-y-4">
|
||||
<div class="flex border-b border-border/60 pb-1.5">
|
||||
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-sky-500 text-sky-400" onclick="toggleWikiTabs(this, 'logger-impl')">Usage in Handlers</button>
|
||||
<button class="px-3 py-1.5 text-xs font-semibold border-b-2 border-transparent text-muted-foreground hover:text-muted-foreground" onclick="toggleWikiTabs(this, 'logger-struct')">Extractor Source</button>
|
||||
</div>
|
||||
|
||||
<!-- Handler Usage Pane -->
|
||||
<div id="logger-impl" class="wiki-pane space-y-4">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Adding <code>logger: AuditLogger</code> to any Axum handler automatically intercepts the request context. Writing a log requires just a single asynchronous call.
|
||||
</p>
|
||||
<div class="relative group">
|
||||
<button class="absolute top-2 right-2 p-1.5 rounded-lg border border-border bg-popover/80 backdrop-blur text-[10px] font-semibold text-muted-foreground/90 hover:text-white hover:bg-secondary opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center gap-1.5" onclick="copyCodeSnippet(this)">
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||
Copy Code
|
||||
</button>
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10.5px] text-sky-450 font-mono"><code class="text-sky-400">use crate::audit::AuditLogger;
|
||||
use axum::{response::IntoResponse, extract::State};
|
||||
use serde_json::json;
|
||||
|
||||
pub async fn update_task_handler(
|
||||
State(repo): State<MongoTaskRepository>,
|
||||
logger: AuditLogger, // <-- Extractor automatically fetches DB, User, IP, UA
|
||||
axum::Form(input): axum::Form<UpdateTaskForm>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
// 1. Perform database operation
|
||||
let updated_task = repo.update(&input.id, &input).await?;
|
||||
|
||||
// 2. Audit the event (single line, fully request-aware)
|
||||
logger.log(
|
||||
"Update", // action_type
|
||||
"Task", // entity_type
|
||||
Some(updated_task.id), // entity_id
|
||||
Some("Updated task details".into()), // details
|
||||
Some(json!(updated_task)), // payload for replayability
|
||||
).await;
|
||||
|
||||
Ok(Redirect::to("/tasks"))
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extractor Code Pane -->
|
||||
<div id="logger-struct" class="wiki-pane hidden space-y-4">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Below is the underlying implementation of the extractor. It queries the JWT cookie, standardizes client IP resolution, and manages the database handle.
|
||||
</p>
|
||||
<div class="relative group">
|
||||
<pre class="bg-popover p-4 rounded-xl border border-border overflow-x-auto text-[10px] text-sky-400 font-mono"><code>pub struct AuditLogger {
|
||||
pub db: mongodb::Database,
|
||||
pub user: Option<AuthenticatedUser>,
|
||||
pub ip_address: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
}
|
||||
|
||||
impl<S> axum::extract::FromRequestParts<S> for AuditLogger
|
||||
where
|
||||
mongodb::Database: axum::extract::FromRef<S>,
|
||||
Config: axum::extract::FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = crate::common::errors::AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let db = mongodb::Database::from_ref(state);
|
||||
let user = AuthenticatedUser::from_request_parts(parts, state).await.ok();
|
||||
let user_agent = parts.headers.get(USER_AGENT)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(String::from);
|
||||
|
||||
let ip_address = parts.headers.get("x-forwarded-for")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| s.split(',').next())
|
||||
.map(|s| s.trim().to_string())
|
||||
.or_else(|| {
|
||||
parts.extensions.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|ConnectInfo(addr)| addr.ip().to_string())
|
||||
});
|
||||
|
||||
Ok(AuditLogger { db, user, ip_address, user_agent })
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section: Audit Log Model Schema -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-slate-200">3. Schema Design</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Audit logs are persisted in the <code>audit_logs</code> collection in MongoDB. The model captures details on the actor, action, target entity, network details, and the structural payload.
|
||||
</p>
|
||||
|
||||
<div class="border border-border bg-card/50 rounded-2xl p-5 overflow-x-auto">
|
||||
<table class="w-full border-collapse text-left text-xs">
|
||||
<thead>
|
||||
<tr class="border-b border-border text-muted-foreground font-bold">
|
||||
<th class="pb-2.5">Field</th>
|
||||
<th class="pb-2.5">Data Type</th>
|
||||
<th class="pb-2.5">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border/40 text-muted-foreground/90 font-sans text-xs">
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">timestamp</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">DateTime<Utc></td>
|
||||
<td class="py-2.5">Chronological timestamp of when the action occurred in UTC.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">user_id</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">Option<ObjectId></td>
|
||||
<td class="py-2.5">Reference identifier of the actor. <code>None</code> for guest actions.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">username</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">Option<String></td>
|
||||
<td class="py-2.5">Cached username of the user for immediate visual queries.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">action_type</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">String</td>
|
||||
<td class="py-2.5">The command category (e.g. <code>Create</code>, <code>Update</code>, <code>Delete</code>, <code>View</code>, <code>Login</code>, <code>Search</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">entity_type</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">String</td>
|
||||
<td class="py-2.5">The resource kind (e.g. <code>Task</code>, <code>User</code>, <code>Developer</code>, <code>Auth</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">entity_id</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">Option<ObjectId></td>
|
||||
<td class="py-2.5">The target resource primary key identifier.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">details</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">Option<String></td>
|
||||
<td class="py-2.5">Human readable description of the event.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">payload</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">Option<serde_json::Value></td>
|
||||
<td class="py-2.5 text-sky-400">A snapshot of the affected model state or diff data for complete audit replayability.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">ip_address</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">Option<String></td>
|
||||
<td class="py-2.5">Origin client IP address resolved via proxy headers or TCP socket.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2.5 font-mono text-[11px] text-indigo-400">user_agent</td>
|
||||
<td class="py-2.5 font-mono text-[11px]">Option<String></td>
|
||||
<td class="py-2.5">Browser details used for authentication forensics.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section: Best Practices for Junior Developers -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-slate-200">4. Guidelines for Developers</h2>
|
||||
<div class="rounded-2xl border border-indigo-500/20 bg-indigo-500/5 p-5 text-xs space-y-4">
|
||||
<div>
|
||||
<span class="font-bold text-indigo-400 block mb-1">💡 When to write audit logs?</span>
|
||||
<p class="text-slate-400 leading-normal">
|
||||
Audit logs should always be populated during state changes. Whenever writing an endpoint that uses <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE</code> operations, inject the <code>AuditLogger</code> and record the event after a successful database execution.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="font-bold text-indigo-400 block mb-1">🔄 Replayability Principle</span>
|
||||
<p class="text-slate-400 leading-normal">
|
||||
The `payload` field is crucial. By storing the JSON serialization of the model BEFORE or AFTER the action, system administrators can reconstruct state history. For deletions, always record the serial snapshot of the deleted model in the payload so it is never permanently lost to audit inquiries.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -0,0 +1,31 @@
|
||||
<aside class="lg:w-64 shrink-0">
|
||||
<div class="sticky top-24 space-y-1.5 p-4 rounded-3xl border border-border bg-card/50 backdrop-blur-xl" id="wiki-sidebar">
|
||||
<span class="px-3 text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-2">Wiki Navigation</span>
|
||||
<a href="/docs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs">Introduction</a>
|
||||
<div class="h-px bg-secondary my-1 mx-2"></div>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Architecture</span>
|
||||
<a href="/docs/logging" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/logging">Audit Logging</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Actions</span>
|
||||
<a href="/docs/buttons" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/buttons">Buttons</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Forms & Inputs</span>
|
||||
<a href="/docs/inputs" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/inputs">Form Fields & Select</a>
|
||||
<a href="/docs/date-time" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/date-time">Date & Time Pickers</a>
|
||||
<a href="/docs/combobox" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/combobox">Autocomplete (Combobox)</a>
|
||||
<a href="/docs/toggles" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/toggles">Switches & Checkboxes</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Overlays</span>
|
||||
<a href="/docs/modals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/modals">Dialog Modals</a>
|
||||
<a href="/docs/sheets" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/sheets">Slide-over Drawers</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Layout & Navigation</span>
|
||||
<a href="/docs/tabs-accordion" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/tabs-accordion">Tabs & Accordions</a>
|
||||
<a href="/docs/scrollbars" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/scrollbars">Custom Scrollbars</a>
|
||||
|
||||
<span class="px-3 text-[9px] font-bold text-slate-600 uppercase tracking-wider block mt-2 mb-1">Visuals & Feedback</span>
|
||||
<a href="/docs/visuals" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/visuals">Avatars & Badges</a>
|
||||
<a href="/docs/feedback" class="flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-secondary transition" data-wiki-path="/docs/feedback">Toasts & Alerts</a>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="grow max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10 flex flex-col lg:flex-row gap-8">
|
||||
|
||||
<!-- Left Navigation Sidebar -->
|
||||
{% include "components/sidebar.html" %}
|
||||
{% include "docs/sidebar.html" %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 space-y-8">
|
||||
Reference in New Issue
Block a user