use axum::http::HeaderMap; use models::admin::admin_audit_log; use sea_orm::ActiveModelTrait; use sea_orm::Set; use serde_json::Value; use time::OffsetDateTime; use uuid::Uuid; use crate::errors::AdminError; use crate::services::service::AdminService; use crate::share::roles::AdminActor; #[derive(Debug, Clone, Default)] pub struct AuditCtx { pub ip: Option, pub user_agent: Option, pub request_id: Option, } impl AuditCtx { pub fn from_headers(headers: &HeaderMap) -> Self { let ip = headers .get("x-forwarded-for") .and_then(|value| value.to_str().ok()) .and_then(|value| value.split(',').next()) .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_owned) .or_else(|| { headers .get("x-real-ip") .and_then(|value| value.to_str().ok()) .map(str::to_owned) }); let user_agent = headers .get(axum::http::header::USER_AGENT) .and_then(|value| value.to_str().ok()) .map(str::to_owned); Self { ip, user_agent, request_id: None, } } pub fn with_request_id(mut self, request_id: Option) -> Self { self.request_id = request_id; self } } pub struct AuditRecord<'a> { pub action: &'a str, pub target: Option<(&'a str, String)>, pub detail: Value, } impl AdminService { pub async fn audit( &self, actor: Option<&AdminActor>, record: AuditRecord<'_>, ctx: &AuditCtx, ) -> Result<(), AdminError> { let (target_type, target_id) = match record.target { Some((kind, id)) => (Some(kind.to_string()), Some(id)), None => (None, None), }; admin_audit_log::ActiveModel { id: Set(Uuid::now_v7()), admin_id: Set(actor.map(|actor| actor.id)), admin_username: Set(actor .map(|actor| actor.username.clone()) .unwrap_or_else(|| "system".to_string())), action: Set(record.action.to_string()), target_type: Set(target_type), target_id: Set(target_id), detail: Set(record.detail), ip: Set(ctx.ip.clone()), user_agent: Set(ctx.user_agent.clone()), request_id: Set(ctx.request_id.clone()), created_at: Set(OffsetDateTime::now_utc()), } .insert(self.db().write()) .await?; Ok(()) } }