use serde_json::{Value, json}; use sqlx::{PgPool, Postgres, Transaction}; use uuid::Uuid; pub const ACTION_LOGIN: &str = "login"; pub const NAMESPACE_AUTH: &str = "auth"; /// Return the current OS user as the audit actor (falls back to empty string). pub fn current_actor() -> String { std::env::var("USER").unwrap_or_default() } fn login_detail(provider: &str, client_ip: Option<&str>, user_agent: Option<&str>) -> Value { json!({ "provider": provider, "client_ip": client_ip, "user_agent": user_agent, }) } /// Write a login audit entry without requiring an explicit transaction. pub async fn log_login( pool: &PgPool, kind: &str, provider: &str, user_id: Uuid, client_ip: Option<&str>, user_agent: Option<&str>, ) { let actor = current_actor(); let detail = login_detail(provider, client_ip, user_agent); let result: Result<_, sqlx::Error> = sqlx::query( "INSERT INTO audit_log (user_id, action, namespace, kind, name, detail, actor) \ VALUES ($1, $2, $3, $4, $5, $6, $7)", ) .bind(user_id) .bind(ACTION_LOGIN) .bind(NAMESPACE_AUTH) .bind(kind) .bind(provider) .bind(&detail) .bind(&actor) .execute(pool) .await; if let Err(e) = result { tracing::warn!(error = %e, kind, provider, "failed to write login audit log"); } else { tracing::debug!(kind, provider, ?user_id, actor, "login audit logged"); } } /// Write an audit entry within an existing transaction. pub async fn log_tx( tx: &mut Transaction<'_, Postgres>, user_id: Option, action: &str, namespace: &str, kind: &str, name: &str, detail: Value, ) { let actor = current_actor(); let result: Result<_, sqlx::Error> = sqlx::query( "INSERT INTO audit_log (user_id, action, namespace, kind, name, detail, actor) \ VALUES ($1, $2, $3, $4, $5, $6, $7)", ) .bind(user_id) .bind(action) .bind(namespace) .bind(kind) .bind(name) .bind(&detail) .bind(&actor) .execute(&mut **tx) .await; if let Err(e) = result { tracing::warn!(error = %e, "failed to write audit log"); } else { tracing::debug!(action, namespace, kind, name, actor, "audit logged"); } } #[cfg(test)] mod tests { use super::*; #[test] fn login_detail_includes_expected_fields() { let detail = login_detail("google", Some("127.0.0.1"), Some("Mozilla/5.0")); assert_eq!(detail["provider"], "google"); assert_eq!(detail["client_ip"], "127.0.0.1"); assert_eq!(detail["user_agent"], "Mozilla/5.0"); } }