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:
2026-05-30 18:23:49 +05:00
parent f6ea8a99d9
commit 4c98dd93ad
34 changed files with 1389 additions and 134 deletions
+115 -42
View File
@@ -1,36 +1,90 @@
# Stick
Stick is a Rust web application starter template built on Axum, Askama, and MongoDB. Unlike traditional MVC architectures that organize code by technical layers (controllers, models, views), Stick is organized by **vertical slices** (features or use-cases). All files related to a specific domain feature—such as authentication or task management—live together in a single module.
Stick is a high-fidelity Rust web application starter template built on Axum, Askama, and MongoDB.
This setup is ideal for medium-to-large projects where horizontal layers become hard to navigate, and compiling templates at runtime is too risky.
Unlike traditional MVC frameworks that segment code horizontally by technical layers (controllers, models, views), Stick is organized by **vertical slices**. Each domain feature—such as authentication, tasks, or audit logging—lives together in its own self-contained module. This simplifies code maintenance and cognitive load in medium-to-large codebases where horizontal layers inevitably grow difficult to navigate.
---
## Technical Architecture
### 1. Vertical Slice Layout
Each feature slice is self-contained. For example, the authentication domain has the following structure:
## Architectural Philosophy & Layout
### 1. Vertical Slice Domain Layout
Every domain feature owns its database access models, business logic, endpoints, and markup templates. For example:
* `src/auth/models.rs`: BSON data structures and claims.
* `src/auth/repository.rs`: Database operations and query logic.
* `src/auth/handlers.rs`: Request/response lifecycle logic.
* `src/auth/extractors.rs`: Axum extractors for session state.
* `templates/auth/`: HTML markup templates rendered at compile-time.
* `src/auth/extractors.rs`: Axum extractors for user session handling.
* `templates/auth/`: HTML templates compiled at build-time.
### 2. Key Stack Decisions
* **Axum (v0.8)**: Handles routing, middleware, and request extraction.
* **Askama (v0.16)**: Evaluates and compiles HTML templates into Rust code at compile time. If you reference a variable or field that doesn't exist, the project fails to compile, catching UI rendering bugs before deployment.
### 2. Separating Components from Documentation
To prevent domain pollution and maintain code integrity, we strictly separate template assets:
* **Reusable UI Components** (`templates/components/`): Reserved strictly for operational UI blocks used across the application. Reusable elements like fields, dropdowns, calendars, and buttons are defined as Askama macros inside [templates/components/macros.html](templates/components/macros.html).
* **System Documentation** (`templates/docs/` & `src/docs/`): Design manuals, guidelines, and interactive preview sandboxes are isolated. The Documentation portal is served on a dedicated `/docs` route, separate from core application logic.
### 3. Key Stack Decisions
* **Axum (v0.8)**: Modern, async-first routing and middleware.
* **Askama (v0.16)**: Evaluates and compiles HTML templates into Rust code at compile time. Syntax, variable existence, and type constraints are verified by the compiler.
* **MongoDB**: Standard Rust driver configured with BSON serialization.
* **Tailwind CSS**: Pre-compiled utility styling using a Node-based wrapper process.
* **Authentication**: Managed via JWT (JSON Web Tokens) stored in secure, encrypted `HttpOnly` cookies.
* **Tailwind CSS**: Utility-first styling compiled using a Node-based wrapper process.
* **JWT Authentication**: Managed via JSON Web Tokens stored in secure, encrypted `HttpOnly` cookies.
---
## Audit Logging Framework (First-Class Feature)
Stick contains a built-in, request-scoped **Audit Logging and Replay** framework designed to make all critical system mutations (Create, Read, Update, Delete, Search) auditable.
```
Request
┌──────────────────┐
│ AuditLogger │ ──► Resolves: DB, User, User-Agent
│ Extractor │
└──────────────────┘
├──► Parses "x-forwarded-for" & "x-real-ip" proxy headers
├──► Parses Direct ConnectInfo TCP socket IP
┌──────────────────┐
│ Log Entry Writ │ ──► Formats: "ProxyIP (Socket: SocketIP)" if mismatched
└──────────────────┘
┌──────────────────┐
│ MongoDB Store │ ──► Stores structured entry + JSON state snapshot
└──────────────────┘
```
### 1. Request-Scoped Extractor (`AuditLogger`)
The custom `AuditLogger` extractor implements Axum's `FromRequestParts` trait. When added to any handler function signature, it automatically resolves:
* Database connection reference.
* Optional authenticated user context.
* Client metadata (IP address and User-Agent).
### 2. Intelligent IP & Proxy Resolution
To support load-balanced or proxied environments, the extractor resolves and logs both network paths:
- Inspects standard proxy headers (`x-forwarded-for`, `x-real-ip`).
- Captures the direct TCP client connection address via Axum's `ConnectInfo`.
- If the proxy IP and connecting socket IP differ, the logger combines them (e.g. `10.0.0.1 (Socket: 127.0.0.1)`) ensuring full visibility of both the request origin and proxy hop.
### 3. Historical State Snapshotting (Replayability)
Log entries include a serialized JSON `payload` field containing the state of the affected entity. For edits, it saves the state transition. For deletions, it saves the final snapshot before removal, ensuring data remains reconstructible in the event of an audit inquiry.
### 4. Admin Audit Panel
Authorized administrators can review, search, and audit system activities at `/auth/audit`. The dashboard allows filtering entries by:
* **Username**: Filter logs to a specific actor.
* **Event Type**: Filter by actions (e.g. `Login`, `Create`, `Update`, `Delete`).
* **Entity Type & ID**: Pinpoint history for a specific resource (e.g. `Task`, `Developer`).
* **Timeline**: Filter logs by start and end timestamps.
---
## Core Features Included
### Self-Provisioning Administrator
On startup, the application checks if the `users` collection in the MongoDB database is empty. If no users are found, it generates a secure, random 16-character alphanumeric password, hashes it with bcrypt, and creates a default `admin` account. The credentials are logged directly to standard output:
On startup, if the MongoDB `users` collection is empty, Stick automatically provisions an administrator account with username `admin`, generates a secure random 16-character alphanumeric password, and outputs the credentials directly to the console logs:
```text
======================================================
CREATED INITIAL ADMINISTRATOR ACCOUNT:
@@ -39,20 +93,11 @@ Password: [GeneratedPassword]
======================================================
```
### Decoupled Identity vs. Domain Entities
Stick strictly separates infrastructure/user models from business domain models:
* **Users** (`User`): Manage authentication, roles (`is_admin`), and settings under `/auth`.
* **Developers** (`Developer`): Plain domain entities managed under `/developers` that represent team members.
### User Management Panel (Admin Only)
Accessible at `/auth/users`. Administrators can view all users, register new ones, toggle administrative roles, reset passwords, or delete standard accounts (with safety blocks preventing administrators from deleting their own active profile or revoking their own privileges).
### User Management Panel (Administrators Only)
Accessible under `/auth/users` by logged-in administrators. The panel allows:
* Viewing all registered users and their administrative permissions.
* Editing user profiles, resetting passwords, and toggling administrator roles.
* Deleting users (with safeguards preventing administrators from deleting their own active accounts or revoking their own admin permissions).
* Registering new users.
### Self-Service Account Settings
Any authenticated user can change their own password by clicking their username in the navigation bar, which directs them to `/auth/password`.
### Self-Service Password Reset
Any logged-in user can change their password at `/auth/password` by clicking their username in the navigation bar.
---
@@ -61,7 +106,7 @@ Any authenticated user can change their own password by clicking their username
### Prerequisites
* **Rust**: Toolchain v1.75+ (for native async traits).
* **Node.js & npm**: Required to build Tailwind CSS.
* **MongoDB**: Running locally on `mongodb://localhost:27017`.
* **MongoDB**: Running locally on `mongodb://127.0.0.1:27017`.
### Local Setup
1. Copy the environment configuration:
@@ -77,16 +122,16 @@ Any authenticated user can change their own password by clicking their username
```bash
cargo run
```
The server will start listening at `http://127.0.0.1:3000`.
The server will start listening at `http://127.0.0.1:3009` (as configured in `.env`).
### Running with Docker
A multi-stage `Dockerfile` is provided to compile Tailwind, compile the Rust binary, and bundle a lightweight Debian run container.
A multi-stage `Dockerfile` compiles Tailwind, compiles the Rust binary, and bundle a lightweight Debian runtime container.
1. Build the image:
```bash
docker build -t stick .
```
2. Start the container (assumes MongoDB is running on the host machine):
2. Start the container:
```bash
docker run --name stick-app --rm --network="host" \
-e DATABASE_URL="mongodb://127.0.0.1:27017" \
@@ -99,31 +144,59 @@ A multi-stage `Dockerfile` is provided to compile Tailwind, compile the Rust bin
---
## Developer Guide: Adding a Feature Slice
To add a new feature (e.g. `projects`):
## Developer Cookbook
### 1. Adding a Feature Slice (e.g. `projects`)
1. Create a module folder: `src/projects/`.
2. Define models in `models.rs` and database access functions in `repository.rs`.
2. Define models in `models.rs` and database access operations in `repository.rs`.
3. Add request handlers in `handlers.rs`.
4. Create a router configuration in `src/projects/mod.rs` exposing a routing module setup:
4. Create `src/projects/mod.rs` to expose the router:
```rust
pub fn router<S>() -> Router<S>
where
Config: axum::extract::FromRef<S>,
MongoUserRepository: axum::extract::FromRef<S>,
crate::common::config::Config: axum::extract::FromRef<S>,
mongodb::Database: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()
.route("/projects", get(handlers::get_projects))
}
```
5. Place HTML layouts under `templates/projects/` extending the `base.html` layout.
6. Register and merge the router in `src/main.rs`:
5. Place HTML templates under `templates/projects/` extending the `base.html` layout.
6. Register the module in `src/main.rs` and merge the sub-router:
```rust
let app = Router::new()
.merge(main_view::router())
.merge(auth::router())
.merge(projects::router()) // Merged domain router
.merge(projects::router()) // Merge domain router
.with_state(state);
```
### 2. Logging Operations with `AuditLogger`
To log any action in a handler, inject `logger: AuditLogger` and invoke `.log()` after a successful DB mutation:
```rust
use crate::audit::AuditLogger;
use serde_json::json;
pub async fn delete_task_handler(
State(repo): State<MongoTaskRepository>,
logger: AuditLogger, // <-- Automatically injected
Path(task_id): Path<ObjectId>,
) -> Result<impl IntoResponse, AppError> {
// 1. Fetch task and perform mutation
let task = repo.get_by_id(&task_id).await?;
repo.delete(&task_id).await?;
// 2. Audit the event (single line, fully context-aware)
logger.log(
"Delete", // Event action
"Task", // Entity type
Some(task_id), // Target entity ID
Some(format!("Deleted task '{}'", task.title)), // Description
Some(json!(task)), // Serialized payload for replayability
).await;
Ok(Redirect::to("/tasks"))
}
```