use crate::utils::{
aliases::{ADb, StrMap},
constante::session_key::session::CSRF_TOKEN_KEY,
pk::Pk,
};
use sea_orm::{ActiveValue::Set, entity::prelude::*};
use serde_json::Value;
#[derive(Clone, Debug, DeriveEntityModel, serde::Serialize)]
#[sea_orm(table_name = "eihwaz_history")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub resource_key: String,
pub object_pk: String,
pub action: String,
pub user_id: Pk,
pub username: String,
pub created_at: chrono::NaiveDateTime,
pub summary: Option<String>,
pub batch_id: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
pub struct AdminActionLog<'a> {
pub user_id: Pk,
pub username: &'a str,
pub resource_key: &'a str,
pub object_pk: &'a str,
pub action: &'a str,
pub summary: Option<String>,
pub batch_id: Option<String>,
}
pub async fn log_admin_action(db: &ADb, log: AdminActionLog<'_>) {
let now = chrono::Utc::now().naive_utc();
let resource_key = log.resource_key.to_string();
let object_pk = log.object_pk.to_string();
let entry = ActiveModel {
resource_key: Set(resource_key.clone()),
object_pk: Set(object_pk.clone()),
action: Set(log.action.to_string()),
user_id: Set(log.user_id),
username: Set(log.username.to_string()),
created_at: Set(now),
summary: Set(log.summary),
batch_id: Set(log.batch_id),
..Default::default()
};
if let Err(e) = Entity::insert(entry).exec(db.as_ref()).await {
tracing::warn!(
resource = %resource_key,
object_pk = %object_pk,
error = %e,
"history insert failed (audit row lost)"
);
}
}
pub const REDACTED: &str = "••••••";
const SENSITIVE_HINTS: &[&str] = &[
"password",
"passwd",
"pwd",
"secret",
"token",
"api_key",
"apikey",
"private_key",
];
pub fn is_sensitive_key(key: &str) -> bool {
let k = key.to_lowercase();
SENSITIVE_HINTS.iter().any(|hint| k.contains(hint))
}
pub fn redact_sensitive(changes: &mut serde_json::Map<String, Value>) {
for (key, entry) in changes.iter_mut() {
if !is_sensitive_key(key) {
continue;
}
if let Value::Object(fields) = entry {
for v in fields.values_mut() {
*v = Value::String(REDACTED.to_string());
}
} else {
*entry = Value::String(REDACTED.to_string());
}
}
}
pub fn diff_fields(old: &Value, body: &StrMap) -> Option<String> {
let Value::Object(map) = old else {
return None;
};
let mut changes = std::collections::BTreeMap::new();
for (k, new_val) in body {
if k == CSRF_TOKEN_KEY || k == "__original_updated_at" {
continue;
}
let old_str = match map.get(k) {
Some(Value::String(s)) => s.clone(),
Some(v) => v.to_string(),
None => continue,
};
if old_str != *new_val {
changes.insert(
k.clone(),
serde_json::json!({"old": old_str, "new": new_val}),
);
}
}
if changes.is_empty() {
return None;
}
let mut changes: serde_json::Map<String, Value> = changes.into_iter().collect();
redact_sensitive(&mut changes);
serde_json::to_string(&changes).ok()
}