78 lines
3.1 KiB
Rust

use actix_web::{HttpResponse, Scope, web};
use crate::adapter::http::middleware::extractor::AuthClaims;
use crate::adapter::persistence::Database;
use crate::domain::common::error::Error;
use crate::domain::common::error::database::DatabaseError;
use crate::interface::port::audit::AuditRepo;
pub fn initialize() -> Scope {
web::scope("/audit")
.route("", web::get().to(list_audit_logs))
.route("/verify", web::get().to(verify_chain))
}
async fn list_audit_logs(_auth: AuthClaims, db: web::Data<Database>) -> HttpResponse {
match db.list_audit_logs() {
Ok(entries) => {
let json: Vec<serde_json::Value> = entries
.into_iter()
.map(|e| {
serde_json::json!({
"id": e.id,
"actor": e.actor,
"action": e.action,
"detail": e.detail,
"created_at": e.created_at,
})
})
.collect();
HttpResponse::Ok().json(json)
}
Err(_) => HttpResponse::Ok().json(serde_json::json!([])),
}
}
/// `GET /api/audit/verify` — walk the WORM hash chain and report whether
/// every row_hash still matches `H(ts || actor || action || detail ||
/// prev_hash)`. Surfaces over HTTP the same verification the CLI's
/// `--verify-audit-log` flag performs, so auditors can check chain
/// integrity without shell access. Any mismatch returns the offending
/// row id inside `error` so the dashboard can link straight to it.
async fn verify_chain(_auth: AuthClaims, audit: web::Data<dyn AuditRepo>) -> HttpResponse {
match audit.verify_audit_log_chain() {
Ok(count) => HttpResponse::Ok().json(serde_json::json!({
"chain_intact": true,
"verified": count,
})),
Err(e) => {
// Tamper detection is a successful verify outcome, not a server
// failure — return 200 with `chain_intact: false` so frontend
// retry/error handling treats real chain corruption as a
// distinct condition from transient DB connectivity issues.
// Reserve 500 for actual DB/IO failures.
let prev_mismatch = matches!(&e, Error::Database(DatabaseError::AuditPrevHashMismatch { .. }));
let row_mismatch = matches!(&e, Error::Database(DatabaseError::AuditRowHashMismatch { .. }));
if prev_mismatch || row_mismatch {
let kind = if prev_mismatch {
"prev_hash_mismatch"
} else {
"row_hash_mismatch"
};
HttpResponse::Ok().json(serde_json::json!({
"chain_intact": false,
"verified": 0,
"kind": kind,
"detail": e.to_string(),
}))
} else {
HttpResponse::InternalServerError().json(serde_json::json!({
"chain_intact": null,
"verified": 0,
"error": e.to_string(),
}))
}
}
}
}