pub type SessionStore = axum_session::Session<crate::pool::SessionPool>;
#[derive(Debug, Clone, Default)]
pub struct AuditCtx {
pub config: crate::config::AuditConfig,
pub ip: Option<String>,
pub user_agent: Option<String>,
}
impl AuditCtx {
pub async fn record(
&self,
db: &crate::pool::Pool,
event_type: &str,
actor_id: Option<i64>,
target_id: Option<i64>,
details: Option<&str>,
) {
crate::auth::audit::record_or_log(
db,
crate::auth::audit::RecordInput {
event_type,
actor_id,
target_id,
ip: self.ip.as_deref(),
user_agent: self.user_agent.as_deref(),
details,
},
)
.await
}
}
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for AuditCtx {
type Rejection = std::convert::Infallible;
fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
let config = parts
.extensions
.get::<crate::config::AuditConfig>()
.cloned()
.unwrap_or_default();
let ip = if config.capture_ip {
parts
.extensions
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
.map(|ci| ci.0.ip().to_string())
.or_else(|| {
parts
.headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_string())
})
.or_else(|| {
parts
.headers
.get("x-real-ip")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
} else {
None
};
let user_agent = if config.capture_user_agent {
parts
.headers
.get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
} else {
None
};
std::future::ready(Ok(AuditCtx {
config,
ip,
user_agent,
}))
}
}