- Rename namespace/kind to folder/type on entries, audit_log, and history tables; add notes. Unique key is (user_id, folder, name). - Service layer and MCP tools support name-first lookup with optional folder when multiple entries share the same name. - secrets_delete dry_run uses the same disambiguation as real deletes. - Add scripts/migrate-v0.3.0.sql for manual DB migration. Refresh README and AGENTS.md. Made-with: Cursor
89 lines
2.3 KiB
Rust
89 lines
2.3 KiB
Rust
use serde_json::{Value, json};
|
|
use sqlx::{PgPool, Postgres, Transaction};
|
|
use uuid::Uuid;
|
|
|
|
pub const ACTION_LOGIN: &str = "login";
|
|
pub const FOLDER_AUTH: &str = "auth";
|
|
|
|
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,
|
|
entry_type: &str,
|
|
provider: &str,
|
|
user_id: Uuid,
|
|
client_ip: Option<&str>,
|
|
user_agent: Option<&str>,
|
|
) {
|
|
let detail = login_detail(provider, client_ip, user_agent);
|
|
let result: Result<_, sqlx::Error> = sqlx::query(
|
|
"INSERT INTO audit_log (user_id, action, folder, type, name, detail) \
|
|
VALUES ($1, $2, $3, $4, $5, $6)",
|
|
)
|
|
.bind(user_id)
|
|
.bind(ACTION_LOGIN)
|
|
.bind(FOLDER_AUTH)
|
|
.bind(entry_type)
|
|
.bind(provider)
|
|
.bind(&detail)
|
|
.execute(pool)
|
|
.await;
|
|
|
|
if let Err(e) = result {
|
|
tracing::warn!(error = %e, entry_type, provider, "failed to write login audit log");
|
|
} else {
|
|
tracing::debug!(entry_type, provider, ?user_id, "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,
|
|
folder: &str,
|
|
entry_type: &str,
|
|
name: &str,
|
|
detail: Value,
|
|
) {
|
|
let result: Result<_, sqlx::Error> = sqlx::query(
|
|
"INSERT INTO audit_log (user_id, action, folder, type, name, detail) \
|
|
VALUES ($1, $2, $3, $4, $5, $6)",
|
|
)
|
|
.bind(user_id)
|
|
.bind(action)
|
|
.bind(folder)
|
|
.bind(entry_type)
|
|
.bind(name)
|
|
.bind(&detail)
|
|
.execute(&mut **tx)
|
|
.await;
|
|
|
|
if let Err(e) = result {
|
|
tracing::warn!(error = %e, "failed to write audit log");
|
|
} else {
|
|
tracing::debug!(action, folder, entry_type, name, "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");
|
|
}
|
|
}
|