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:
+71
-1
@@ -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;
|
||||
@@ -49,6 +50,7 @@ struct DashboardTemplate {
|
||||
pub async fn get_dashboard(
|
||||
State(task_repo): State<MongoTaskRepository>,
|
||||
State(dev_repo): State<MongoDeveloperRepository>,
|
||||
audit: AuditLogger,
|
||||
user_opt: Option<AuthenticatedUser>,
|
||||
) -> Result<Response, AppError> {
|
||||
let Some(user) = user_opt else {
|
||||
@@ -70,6 +72,14 @@ pub async fn get_dashboard(
|
||||
});
|
||||
}
|
||||
|
||||
audit.log(
|
||||
"View",
|
||||
"Task",
|
||||
None,
|
||||
Some("Viewed task dashboard".to_string()),
|
||||
None
|
||||
).await;
|
||||
|
||||
Ok(HtmlTemplate(DashboardTemplate {
|
||||
username: user.username,
|
||||
authenticated: true,
|
||||
@@ -87,6 +97,7 @@ pub struct CreateTaskForm {
|
||||
|
||||
pub async fn post_create_task(
|
||||
State(task_repo): State<MongoTaskRepository>,
|
||||
audit: AuditLogger,
|
||||
user_opt: Option<AuthenticatedUser>,
|
||||
Form(payload): Form<CreateTaskForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
@@ -108,15 +119,31 @@ pub async fn post_create_task(
|
||||
_ => None,
|
||||
};
|
||||
|
||||
task_repo
|
||||
let new_task = task_repo
|
||||
.create(&user.user_id, title, description, assigned_to)
|
||||
.await?;
|
||||
|
||||
let payload_val = serde_json::json!({
|
||||
"title": new_task.title,
|
||||
"description": new_task.description,
|
||||
"assigned_to": new_task.assigned_to.map(|id| id.to_hex()),
|
||||
"is_completed": new_task.is_completed,
|
||||
});
|
||||
|
||||
audit.log(
|
||||
"Create",
|
||||
"Task",
|
||||
new_task.id,
|
||||
Some(format!("Created task: {}", title)),
|
||||
Some(payload_val)
|
||||
).await;
|
||||
|
||||
Ok(Redirect::to("/tasks").into_response())
|
||||
}
|
||||
|
||||
pub async fn post_complete_task(
|
||||
State(task_repo): State<MongoTaskRepository>,
|
||||
audit: AuditLogger,
|
||||
user_opt: Option<AuthenticatedUser>,
|
||||
Path(task_id_str): Path<String>,
|
||||
) -> Result<Response, AppError> {
|
||||
@@ -127,13 +154,37 @@ pub async fn post_complete_task(
|
||||
let task_id = ObjectId::parse_str(&task_id_str)
|
||||
.map_err(|_| AppError::BadRequest("Invalid task identifier".to_string()))?;
|
||||
|
||||
let task_before = task_repo.find_by_id(&task_id, &user.user_id).await?;
|
||||
|
||||
task_repo.mark_completed(&task_id, &user.user_id).await?;
|
||||
|
||||
if let Some(tb) = task_before {
|
||||
let payload_val = serde_json::json!({
|
||||
"before": {
|
||||
"title": tb.title,
|
||||
"is_completed": tb.is_completed,
|
||||
},
|
||||
"after": {
|
||||
"title": tb.title,
|
||||
"is_completed": true,
|
||||
}
|
||||
});
|
||||
|
||||
audit.log(
|
||||
"Update",
|
||||
"Task",
|
||||
Some(task_id),
|
||||
Some(format!("Marked task '{}' as completed", tb.title)),
|
||||
Some(payload_val)
|
||||
).await;
|
||||
}
|
||||
|
||||
Ok(Redirect::to("/tasks").into_response())
|
||||
}
|
||||
|
||||
pub async fn post_delete_task(
|
||||
State(task_repo): State<MongoTaskRepository>,
|
||||
audit: AuditLogger,
|
||||
user_opt: Option<AuthenticatedUser>,
|
||||
Path(task_id_str): Path<String>,
|
||||
) -> Result<Response, AppError> {
|
||||
@@ -144,7 +195,26 @@ pub async fn post_delete_task(
|
||||
let task_id = ObjectId::parse_str(&task_id_str)
|
||||
.map_err(|_| AppError::BadRequest("Invalid task identifier".to_string()))?;
|
||||
|
||||
let task_to_delete = task_repo.find_by_id(&task_id, &user.user_id).await?;
|
||||
|
||||
task_repo.delete(&task_id, &user.user_id).await?;
|
||||
|
||||
if let Some(td) = task_to_delete {
|
||||
let payload_val = serde_json::json!({
|
||||
"title": td.title,
|
||||
"description": td.description,
|
||||
"assigned_to": td.assigned_to.map(|id| id.to_hex()),
|
||||
"is_completed": td.is_completed,
|
||||
});
|
||||
|
||||
audit.log(
|
||||
"Delete",
|
||||
"Task",
|
||||
Some(task_id),
|
||||
Some(format!("Deleted task: {}", td.title)),
|
||||
Some(payload_val)
|
||||
).await;
|
||||
}
|
||||
|
||||
Ok(Redirect::to("/tasks").into_response())
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ where
|
||||
Config: axum::extract::FromRef<S>,
|
||||
MongoTaskRepository: axum::extract::FromRef<S>,
|
||||
crate::developers::repository::MongoDeveloperRepository: axum::extract::FromRef<S>,
|
||||
mongodb::Database: axum::extract::FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
Router::new()
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::tasks::models::Task;
|
||||
#[async_trait::async_trait]
|
||||
pub trait TaskRepository {
|
||||
async fn find_all_by_user(&self, user_id: &ObjectId) -> Result<Vec<Task>, AppError>;
|
||||
async fn find_by_id(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<Option<Task>, AppError>;
|
||||
async fn create(&self, user_id: &ObjectId, title: &str, description: Option<&str>, assigned_to: Option<ObjectId>) -> Result<Task, AppError>;
|
||||
async fn mark_completed(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError>;
|
||||
async fn delete(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError>;
|
||||
@@ -28,6 +29,13 @@ impl MongoTaskRepository {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TaskRepository for MongoTaskRepository {
|
||||
async fn find_by_id(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<Option<Task>, AppError> {
|
||||
let collection = self.db.collection::<Task>("tasks");
|
||||
let filter = doc! { "_id": task_id, "user_id": user_id };
|
||||
let task = collection.find_one(filter).await?;
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
async fn find_all_by_user(&self, user_id: &ObjectId) -> Result<Vec<Task>, AppError> {
|
||||
let collection = self.db.collection::<Task>("tasks");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user