fad06fd
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<String>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
}
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<String>) -> 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(())
}
}