1use std::sync::Arc;
2
3use crate::db::{ConnExt, Database};
4use crate::error::Result;
5use crate::id;
6
7use super::backend::AuditLogBackend;
8use super::entry::AuditEntry;
9
10#[derive(Clone)]
20pub struct AuditLog(Arc<dyn AuditLogBackend>);
21
22impl AuditLog {
23 pub fn new(db: Database) -> Self {
25 Self(Arc::new(SqliteAuditBackend { db }))
26 }
27
28 pub fn from_backend(backend: Arc<dyn AuditLogBackend>) -> Self {
30 Self(backend)
31 }
32
33 pub async fn record(&self, entry: &AuditEntry) -> Result<()> {
40 self.0.record(entry).await
41 }
42
43 pub async fn record_silent(&self, entry: &AuditEntry) {
45 if let Err(e) = self.0.record(entry).await {
46 tracing::error!(
47 error = %e,
48 action = %entry.action(),
49 actor = %entry.actor(),
50 "audit log write failed"
51 );
52 }
53 }
54
55 #[cfg(any(test, feature = "test-helpers"))]
60 pub fn memory() -> (Self, Arc<MemoryAuditBackend>) {
61 let backend = Arc::new(MemoryAuditBackend {
62 entries: std::sync::Mutex::new(Vec::new()),
63 });
64 (Self(backend.clone()), backend)
65 }
66}
67
68struct SqliteAuditBackend {
69 db: Database,
70}
71
72impl AuditLogBackend for SqliteAuditBackend {
73 fn record<'a>(
74 &'a self,
75 entry: &'a AuditEntry,
76 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
77 Box::pin(async move {
78 let id = id::ulid();
79 let metadata_json = entry
80 .metadata_value()
81 .map(|v| v.to_string())
82 .unwrap_or_else(|| "{}".to_string());
83
84 let (ip, user_agent, device_name, device_type, fingerprint) =
85 match entry.client_info_value() {
86 Some(ci) => (
87 ci.ip_value().map(String::from),
88 ci.user_agent_value().map(String::from),
89 ci.device_name_value().map(String::from),
90 ci.device_type_value().map(String::from),
91 ci.fingerprint_value().map(String::from),
92 ),
93 None => (None, None, None, None, None),
94 };
95
96 self.db
97 .conn()
98 .execute_raw(
99 "INSERT INTO audit_log \
100 (id, actor, action, resource_type, resource_id, metadata, \
101 ip, user_agent, device_name, device_type, fingerprint, tenant_id) \
102 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
103 libsql::params![
104 id,
105 entry.actor(),
106 entry.action(),
107 entry.resource_type(),
108 entry.resource_id(),
109 metadata_json,
110 ip,
111 user_agent,
112 device_name,
113 device_type,
114 fingerprint,
115 entry.tenant_id_value(),
116 ],
117 )
118 .await
119 .map_err(crate::error::Error::from)?;
120
121 Ok(())
122 })
123 }
124}
125
126#[cfg(any(test, feature = "test-helpers"))]
128pub struct MemoryAuditBackend {
129 entries: std::sync::Mutex<Vec<AuditEntry>>,
130}
131
132#[cfg(any(test, feature = "test-helpers"))]
133impl MemoryAuditBackend {
134 pub fn entries(&self) -> Vec<AuditEntry> {
136 self.entries.lock().unwrap().clone()
137 }
138}
139
140#[cfg(any(test, feature = "test-helpers"))]
141impl AuditLogBackend for MemoryAuditBackend {
142 fn record<'a>(
143 &'a self,
144 entry: &'a AuditEntry,
145 ) -> std::pin::Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
146 self.entries.lock().unwrap().push(entry.clone());
147 Box::pin(async { Ok(()) })
148 }
149}