- audit_log 查询去掉 detail->>'user_id' 回退分支 - login_detail 不再冗余写入 user_id 到 detail JSON - 迁移 SQL 去掉多余的 ALTER TABLE ADD COLUMN Made-with: Cursor
98 lines
2.6 KiB
Rust
98 lines
2.6 KiB
Rust
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<Uuid>,
|
|
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");
|
|
}
|
|
}
|