Skip to main content

agentdb/
audit.rs

1use crate::error::{AgentDbError, Result};
2use crate::schema::now_ms;
3use rusqlite::params;
4use rusqlite::Connection;
5use serde_json::Value;
6use std::sync::{Arc, Mutex};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct AuditEntry {
11    pub id: String,
12    pub timestamp: i64,
13    pub actor: Option<String>,
14    pub action: String,
15    pub table_name: String,
16    pub record_id: String,
17    pub old_value: Option<Value>,
18    pub new_value: Option<Value>,
19    pub reason: Option<String>,
20}
21
22pub struct AuditStore {
23    conn: Arc<Mutex<Connection>>,
24}
25
26impl AuditStore {
27    pub(crate) fn new(conn: Arc<Mutex<Connection>>) -> Self {
28        Self { conn }
29    }
30
31    #[allow(clippy::too_many_arguments)]
32    pub fn log(
33        &self,
34        actor: Option<&str>,
35        action: &str,
36        table_name: &str,
37        record_id: &str,
38        old_value: Option<Value>,
39        new_value: Option<Value>,
40        reason: Option<&str>,
41    ) -> Result<String> {
42        let id = Uuid::new_v4().to_string();
43        let conn = self.conn.lock().unwrap();
44        let old_str = old_value.as_ref().map(|v| v.to_string());
45        let new_str = new_value.as_ref().map(|v| v.to_string());
46        let now = now_ms();
47        conn.execute(
48            "INSERT INTO _adb_audit_log (id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason)
49             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
50            params![id, now, actor, action, table_name, record_id, old_str, new_str, reason],
51        )?;
52        Ok(id)
53    }
54
55    pub fn query_by_record(
56        &self,
57        table_name: &str,
58        record_id: &str,
59        limit: Option<usize>,
60    ) -> Result<Vec<AuditEntry>> {
61        let conn = self.conn.lock().unwrap();
62        let lim = limit.unwrap_or(100) as i64;
63        let mut stmt = conn.prepare(
64            "SELECT id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason
65             FROM _adb_audit_log
66             WHERE table_name = ?1 AND record_id = ?2
67             ORDER BY timestamp DESC LIMIT ?3",
68        )?;
69        let rows = stmt.query_map(params![table_name, record_id, lim], parse_audit_row)?;
70        rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
71    }
72
73    pub fn query_by_actor(&self, actor: &str, limit: Option<usize>) -> Result<Vec<AuditEntry>> {
74        let conn = self.conn.lock().unwrap();
75        let lim = limit.unwrap_or(100) as i64;
76        let mut stmt = conn.prepare(
77            "SELECT id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason
78             FROM _adb_audit_log
79             WHERE actor = ?1
80             ORDER BY timestamp DESC LIMIT ?2",
81        )?;
82        let rows = stmt.query_map(params![actor, lim], parse_audit_row)?;
83        rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
84    }
85
86    pub fn query_recent(&self, limit: Option<usize>) -> Result<Vec<AuditEntry>> {
87        let conn = self.conn.lock().unwrap();
88        let lim = limit.unwrap_or(100) as i64;
89        let mut stmt = conn.prepare(
90            "SELECT id, timestamp, actor, action, table_name, record_id, old_value, new_value, reason
91             FROM _adb_audit_log
92             ORDER BY timestamp DESC LIMIT ?1",
93        )?;
94        let rows = stmt.query_map(params![lim], parse_audit_row)?;
95        rows.map(|r| r.map_err(AgentDbError::Sqlite)).collect()
96    }
97}
98
99fn parse_audit_row(row: &rusqlite::Row) -> rusqlite::Result<AuditEntry> {
100    let old_str: Option<String> = row.get(6)?;
101    let new_str: Option<String> = row.get(7)?;
102    Ok(AuditEntry {
103        id: row.get(0)?,
104        timestamp: row.get(1)?,
105        actor: row.get(2)?,
106        action: row.get(3)?,
107        table_name: row.get(4)?,
108        record_id: row.get(5)?,
109        old_value: old_str.and_then(|s| serde_json::from_str(&s).ok()),
110        new_value: new_str.and_then(|s| serde_json::from_str(&s).ok()),
111        reason: row.get(8)?,
112    })
113}