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}