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:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user