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
+96 -1
View File
@@ -4,6 +4,7 @@ use axum::{
http::StatusCode,
response::{Html, IntoResponse, Redirect, Response},
};
use crate::audit::AuditLogger;
use mongodb::bson::oid::ObjectId;
use serde::Deserialize;
use crate::common::errors::AppError;
@@ -56,6 +57,7 @@ struct DeveloperSearchResultsTemplate {
pub async fn get_list(
State(dev_repo): State<MongoDeveloperRepository>,
audit: AuditLogger,
user_opt: Option<AuthenticatedUser>,
) -> Result<Response, AppError> {
let Some(user) = user_opt else {
@@ -65,6 +67,14 @@ pub async fn get_list(
let _ = dev_repo.ensure_seeded(&user.user_id).await;
let developers = dev_repo.find_all_by_user(&user.user_id).await?;
audit.log(
"View",
"Developer",
None,
Some("Viewed list of all developers".to_string()),
None
).await;
Ok(HtmlTemplate(DeveloperListTemplate {
username: user.username,
authenticated: true,
@@ -82,6 +92,7 @@ pub struct CreateDevForm {
pub async fn post_create(
State(dev_repo): State<MongoDeveloperRepository>,
audit: AuditLogger,
user_opt: Option<AuthenticatedUser>,
Form(payload): Form<CreateDevForm>,
) -> Result<Response, AppError> {
@@ -101,13 +112,28 @@ pub async fn post_create(
.filter(|s| !s.is_empty())
.collect();
dev_repo.create(&user.user_id, name, email, skills).await?;
let new_dev = dev_repo.create(&user.user_id, name, email, skills).await?;
let payload_val = serde_json::json!({
"name": new_dev.name,
"email": new_dev.email,
"skills": new_dev.skills,
});
audit.log(
"Create",
"Developer",
new_dev.id,
Some(format!("Created developer: {}", name)),
Some(payload_val)
).await;
Ok(Redirect::to("/developers").into_response())
}
pub async fn get_edit(
State(dev_repo): State<MongoDeveloperRepository>,
audit: AuditLogger,
user_opt: Option<AuthenticatedUser>,
Path(dev_id_str): Path<String>,
) -> Result<Response, AppError> {
@@ -123,6 +149,14 @@ pub async fn get_edit(
return Err(AppError::Unauthorized("Developer not found".to_string()));
};
audit.log(
"View",
"Developer",
Some(dev_id),
Some(format!("Viewed edit page for developer: {}", developer.name)),
None
).await;
Ok(HtmlTemplate(DeveloperEditTemplate {
username: user.username,
authenticated: true,
@@ -133,6 +167,7 @@ pub async fn get_edit(
pub async fn post_update(
State(dev_repo): State<MongoDeveloperRepository>,
audit: AuditLogger,
user_opt: Option<AuthenticatedUser>,
Path(dev_id_str): Path<String>,
Form(payload): Form<CreateDevForm>,
@@ -156,13 +191,41 @@ pub async fn post_update(
.filter(|s| !s.is_empty())
.collect();
let dev_before = dev_repo.find_by_id(&dev_id, &user.user_id).await?;
dev_repo.update(&dev_id, &user.user_id, name, email, skills).await?;
let dev_after = dev_repo.find_by_id(&dev_id, &user.user_id).await?;
if let (Some(dbef), Some(daft)) = (dev_before, dev_after) {
let payload_val = serde_json::json!({
"before": {
"name": dbef.name,
"email": dbef.email,
"skills": dbef.skills,
},
"after": {
"name": daft.name,
"email": daft.email,
"skills": daft.skills,
}
});
audit.log(
"Update",
"Developer",
Some(dev_id),
Some(format!("Updated developer details for: {}", name)),
Some(payload_val)
).await;
}
Ok(Redirect::to("/developers").into_response())
}
pub async fn post_delete(
State(dev_repo): State<MongoDeveloperRepository>,
audit: AuditLogger,
user_opt: Option<AuthenticatedUser>,
Path(dev_id_str): Path<String>,
) -> Result<Response, AppError> {
@@ -173,8 +236,26 @@ pub async fn post_delete(
let dev_id = ObjectId::parse_str(&dev_id_str)
.map_err(|_| AppError::BadRequest("Invalid developer identifier".to_string()))?;
let dev_to_delete = dev_repo.find_by_id(&dev_id, &user.user_id).await?;
dev_repo.delete(&dev_id, &user.user_id).await?;
if let Some(dd) = dev_to_delete {
let payload_val = serde_json::json!({
"name": dd.name,
"email": dd.email,
"skills": dd.skills,
});
audit.log(
"Delete",
"Developer",
Some(dev_id),
Some(format!("Deleted developer: {}", dd.name)),
Some(payload_val)
).await;
}
Ok(Redirect::to("/developers").into_response())
}
@@ -185,6 +266,7 @@ pub struct SearchQuery {
pub async fn get_search(
State(dev_repo): State<MongoDeveloperRepository>,
audit: AuditLogger,
user_opt: Option<AuthenticatedUser>,
Query(params): Query<SearchQuery>,
) -> Result<Response, AppError> {
@@ -200,6 +282,19 @@ pub async fn get_search(
let matched_devs = dev_repo.search_by_name(&user.user_id, query_str).await?;
let payload_val = serde_json::json!({
"query": query_str,
"results_count": matched_devs.len(),
});
audit.log(
"Search",
"Developer",
None,
Some(format!("Searched developers with query: '{}'", query_str)),
Some(payload_val)
).await;
Ok(HtmlTemplate(DeveloperSearchResultsTemplate {
developers: matched_devs,
})
+1
View File
@@ -13,6 +13,7 @@ pub fn router<S>() -> Router<S>
where
Config: axum::extract::FromRef<S>,
MongoDeveloperRepository: axum::extract::FromRef<S>,
mongodb::Database: axum::extract::FromRef<S>,
S: Clone + Send + Sync + 'static,
{
Router::new()