use axum::{
extract::Request,
middleware::Next,
response::Response,
};
use crate::middleware::auth::{AuthContext, TenantId};
pub async fn audit_middleware(request: Request, next: Next) -> Response {
let tenant_id = request
.extensions()
.get::<AuthContext>()
.and_then(|c| c.tenant_id.clone())
.or_else(|| request.extensions().get::<TenantId>().map(|t| t.0.clone()))
.or_else(|| {
request
.headers()
.get("x-tenant-id")
.and_then(|v| v.to_str().ok())
.map(str::to_string)
});
let actor = request
.extensions()
.get::<AuthContext>()
.and_then(|c| c.subject.clone());
let method = request.method().to_string();
let resource = request.uri().path().to_string();
let response = next.run(request).await;
let outcome = if response.status().is_success() {
"success"
} else {
"failure"
};
tracing::info!(
target: "sunbeam_g2v::audit",
tenant_id = tenant_id.as_deref(),
actor = actor.as_deref(),
action = method.as_str(),
resource = resource.as_str(),
outcome = outcome,
status = response.status().as_u16(),
"audit event"
);
response
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
#[tokio::test]
async fn audit_middleware_forwards_request() {
use axum::routing::get;
use axum::Router;
use tower::ServiceExt;
let app = Router::new()
.route("/test", get(|| async { "ok" }))
.layer(axum::middleware::from_fn(audit_middleware));
let response = app
.oneshot(Request::builder().uri("/test").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), 200);
}
}