Files
Htmx/templates/docs/logging.html
T
shaamilahmed 478d5f3c17 feat: implement responsive mobile navigation header and wiki sidebar drawer
- Refactored templates/base.html to hide navigation on mobile and expose a hamburger menu button.
- Built a right-aligned sliding mobile menu drawer sheet in base.html.
- Hidden the documentation sidebar in templates/docs/sidebar.html by default on mobile, placing it inside a left-aligned sliding drawer sheet.
- Added a floating 'Explore Guides' directory trigger bar at the top of doc pages on mobile.
- Updated static/js/components.js to apply active route highlighting globally to both desktop and mobile sidebars.
2026-05-30 19:03:27 +05:00

259 lines
16 KiB
HTML

{% 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&lt;S&gt; axum::extract::FromRequestParts&lt;S&gt; for AuditLogger
where
mongodb::Database: axum::extract::FromRef&lt;S&gt;,
Config: axum::extract::FromRef&lt;S&gt;,
S: Send + Sync,
{
type Rejection = crate::common::errors::AppError;
async fn from_request_parts(parts: &amp;mut Parts, state: &amp;S) -&gt; Result&lt;Self, Self::Rejection&gt; {
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::&lt;ConnectInfo&lt;SocketAddr&gt;&gt;()
.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&lt;Utc&gt;</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&lt;ObjectId&gt;</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&lt;String&gt;</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&lt;ObjectId&gt;</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&lt;String&gt;</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&lt;serde_json::Value&gt;</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&lt;String&gt;</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&lt;String&gt;</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>
<span class="font-bold text-indigo-400 block mb-1">🗑️ Compliance & Archival Purging</span>
<p class="text-slate-400 leading-normal">
To maintain storage hygiene, administrators can purge logs older than <code>x</code> days (minimum 1 day). To prevent accidental loss of audit history, the system enforces a strict <strong>archival download requirement</strong>:
</p>
<ul class="list-disc pl-5 mt-1.5 space-y-1.5 text-slate-400 leading-normal">
<li>
The purge action queries the records target for deletion and serializes them to JSON format in memory.
</li>
<li>
The database records are deleted, and a new audit log entry recording the purge event details is created to preserve the audit trail chain.
</li>
<li>
The server immediately streams the JSON data as a downloadable attachment, forcing the browser to save the backup locally.
</li>
</ul>
</div>
</div>
</section>
</div>
</div>
{% endblock %}