1use crate::Orm;
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Serialize, Deserialize)]
5pub struct AuditLog {
6 pub id: i32,
7 pub model_type: String,
8 pub model_id: i32,
9 pub event: String,
10 pub old_values: Option<String>,
11 pub new_values: Option<String>,
12 pub created_at: Option<String>,
13}
14
15pub async fn log_audit(
16 model_type: &str,
17 model_id: i32,
18 event: &str,
19 mut old_values: Option<String>,
20 mut new_values: Option<String>,
21) -> Result<(), crate::Error> {
22 const MAX_PAYLOAD_LEN: usize = 5 * 1024 * 1024; if model_type.len() > 255 || event.len() > 50 {
25 return Err(crate::Error::Validation(
26 "Audit model_type or event string too long".to_string(),
27 ));
28 }
29
30 if let Some(val) = &old_values
31 && val.len() > MAX_PAYLOAD_LEN
32 {
33 old_values = Some(r#"{"error":"payload_too_large"}"#.to_string());
34 }
35
36 if let Some(val) = &new_values
37 && val.len() > MAX_PAYLOAD_LEN
38 {
39 new_values = Some(r#"{"error":"payload_too_large"}"#.to_string());
40 }
41
42 let pool = Orm::pool();
43 let driver = Orm::driver();
44
45 if driver == "postgres" {
46 sqlx::query(
47 "INSERT INTO rullst_audits (model_type, model_id, event, old_values, new_values) VALUES ($1, $2, $3, $4, $5)"
48 )
49 .bind(model_type)
50 .bind(model_id)
51 .bind(event)
52 .bind(old_values)
53 .bind(new_values)
54 .execute(pool)
55 .await?;
56 } else {
57 sqlx::query(
58 "INSERT INTO rullst_audits (model_type, model_id, event, old_values, new_values) VALUES (?, ?, ?, ?, ?)"
59 )
60 .bind(model_type)
61 .bind(model_id)
62 .bind(event)
63 .bind(old_values)
64 .bind(new_values)
65 .execute(pool)
66 .await?;
67 }
68
69 Ok(())
70}
71
72pub fn compute_diff(old_json: &str, new_json: &str) -> (Option<String>, Option<String>) {
73 if old_json == new_json {
74 return (None, None);
75 }
76
77 let old_val: serde_json::Value =
78 serde_json::from_str(old_json).unwrap_or(serde_json::Value::Null);
79 let new_val: serde_json::Value =
80 serde_json::from_str(new_json).unwrap_or(serde_json::Value::Null);
81
82 let mut diff_old = serde_json::Map::new();
83 let mut diff_new = serde_json::Map::new();
84
85 fn is_sensitive(key: &str) -> bool {
86 let k = key.to_lowercase();
87 k.contains("password")
88 || k.contains("token")
89 || k.contains("secret")
90 || k.contains("senha")
91 || k.contains("api_key")
92 || k.contains("cvv")
93 || k.contains("ssn")
94 || k.contains("credit_card")
95 || k.contains("auth_code")
96 }
97
98 fn mask_if_sensitive(key: &str, value: serde_json::Value) -> serde_json::Value {
99 if is_sensitive(key) {
100 serde_json::Value::String("***".to_string())
101 } else {
102 value
103 }
104 }
105
106 if let (serde_json::Value::Object(old_obj), serde_json::Value::Object(mut new_obj)) =
107 (old_val, new_val)
108 {
109 for (k, v) in old_obj {
110 if let Some(new_v) = new_obj.remove(&k) {
111 #[allow(clippy::collapsible_if)]
112 if v != new_v {
113 diff_new.insert(k.clone(), mask_if_sensitive(&k, new_v));
114 diff_old.insert(k.clone(), mask_if_sensitive(&k, v));
115 }
116 } else {
117 diff_new.insert(k.clone(), serde_json::Value::Null);
118 diff_old.insert(k.clone(), mask_if_sensitive(&k, v));
119 }
120 }
121 for (k, new_v) in new_obj {
122 diff_new.insert(k.clone(), mask_if_sensitive(&k, new_v));
123 diff_old.insert(k, serde_json::Value::Null);
124 }
125 }
126
127 if diff_old.is_empty() && diff_new.is_empty() {
128 return (None, None); }
130
131 let final_old = serde_json::to_string(&diff_old).ok();
132 let final_new = serde_json::to_string(&diff_new).ok();
133
134 (final_old, final_new)
135}
136
137pub async fn log_audit_diff(
138 model_type: &str,
139 model_id: i32,
140 event: &str,
141 old_json: &str,
142 new_json: &str,
143) -> Result<(), crate::Error> {
144 const MAX_PAYLOAD_LEN: usize = 5 * 1024 * 1024; if old_json.len() > MAX_PAYLOAD_LEN || new_json.len() > MAX_PAYLOAD_LEN {
147 return log_audit(
148 model_type,
149 model_id,
150 event,
151 Some(r#"{"error":"payload_too_large_for_diff"}"#.to_string()),
152 Some(r#"{"error":"payload_too_large_for_diff"}"#.to_string()),
153 )
154 .await;
155 }
156
157 let (final_old, final_new) = compute_diff(old_json, new_json);
158 if final_old.is_none() && final_new.is_none() {
159 return Ok(()); }
161 log_audit(model_type, model_id, event, final_old, final_new).await
162}
163
164pub async fn create_audit_table() -> Result<(), crate::Error> {
165 let pool = Orm::pool();
166 let driver = Orm::driver();
167
168 let query = if driver == "postgres" {
169 r#"
170 CREATE TABLE IF NOT EXISTS rullst_audits (
171 id SERIAL PRIMARY KEY,
172 model_type VARCHAR(255) NOT NULL,
173 model_id INT NOT NULL,
174 event VARCHAR(50) NOT NULL,
175 old_values TEXT,
176 new_values TEXT,
177 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
178 )
179 "#
180 } else if driver == "mysql" {
181 r#"
182 CREATE TABLE IF NOT EXISTS rullst_audits (
183 id INT AUTO_INCREMENT PRIMARY KEY,
184 model_type VARCHAR(255) NOT NULL,
185 model_id INT NOT NULL,
186 event VARCHAR(50) NOT NULL,
187 old_values TEXT,
188 new_values TEXT,
189 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
190 )
191 "#
192 } else {
193 r#"
194 CREATE TABLE IF NOT EXISTS rullst_audits (
195 id INTEGER PRIMARY KEY AUTOINCREMENT,
196 model_type TEXT NOT NULL,
197 model_id INTEGER NOT NULL,
198 event TEXT NOT NULL,
199 old_values TEXT,
200 new_values TEXT,
201 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
202 )
203 "#
204 };
205
206 sqlx::query(query).execute(pool).await?;
207 Ok(())
208}
209
210#[cfg(test)]
211mod tests {
212 use super::AuditLog;
213
214 #[test]
215 fn test_audit_log_serialization_round_trip() {
216 let log = AuditLog {
217 id: 1,
218 model_type: "User".to_string(),
219 model_id: 42,
220 event: "created".to_string(),
221 old_values: None,
222 new_values: Some(r#"{"name":"Alice"}"#.to_string()),
223 created_at: Some("2024-01-01T00:00:00Z".to_string()),
224 };
225
226 let json_str = serde_json::to_string(&log).expect("serialize");
227 assert!(json_str.contains("\"model_type\":\"User\""));
228 assert!(json_str.contains("\"event\":\"created\""));
229
230 let deserialized: AuditLog = serde_json::from_str(&json_str).expect("deserialize");
231 assert_eq!(deserialized.id, 1);
232 assert_eq!(deserialized.model_id, 42);
233 assert_eq!(deserialized.event, "created");
234 assert!(deserialized.old_values.is_none());
235 }
236
237 #[test]
238 fn test_audit_log_clone_debug() {
239 let log = AuditLog {
240 id: 5,
241 model_type: "Post".to_string(),
242 model_id: 99,
243 event: "updated".to_string(),
244 old_values: Some(r#"{"title":"Old"}"#.to_string()),
245 new_values: Some(r#"{"title":"New"}"#.to_string()),
246 created_at: None,
247 };
248 let cloned = log.clone();
249 assert_eq!(cloned.model_type, "Post");
250 let _ = format!("{:?}", cloned);
252 }
253
254 #[test]
255 fn test_compute_diff_changes() {
256 let old_json = r#"{"name":"Alice","age":30}"#;
257 let new_json = r#"{"name":"Alice","age":31}"#;
258 let (old_diff, new_diff) = super::compute_diff(old_json, new_json);
259 assert_eq!(old_diff.unwrap(), r#"{"age":30}"#);
260 assert_eq!(new_diff.unwrap(), r#"{"age":31}"#);
261 }
262
263 #[test]
264 fn test_compute_diff_no_changes() {
265 let json = r#"{"name":"Alice","age":30}"#;
266 let (old_diff, new_diff) = super::compute_diff(json, json);
267 assert!(old_diff.is_none());
268 assert!(new_diff.is_none());
269 }
270
271 #[test]
272 fn test_compute_diff_invalid_json() {
273 let (old_diff, new_diff) = super::compute_diff("not json", "{invalid}");
274 assert!(old_diff.is_none());
275 assert!(new_diff.is_none());
276 }
277
278 #[tokio::test]
279 async fn test_log_audit_diff_bypass() {
280 let result = super::log_audit_diff(
282 "User",
283 1,
284 "update",
285 r#"{"name":"Alice"}"#,
286 r#"{"name":"Alice"}"#,
287 )
288 .await;
289 assert!(result.is_ok());
290 }
291
292 #[test]
293 fn test_compute_diff_explicit_null_vs_omitted() {
294 let old_json = r#"{"name":"Alice","age":30}"#;
295 let new_json = r#"{"name":"Alice"}"#;
296 let (old_diff, new_diff) = super::compute_diff(old_json, new_json);
297 assert_eq!(old_diff.unwrap(), r#"{"age":30}"#);
298 assert_eq!(new_diff.unwrap(), r#"{"age":null}"#);
299
300 let old_json2 = r#"{"name":"Alice"}"#;
301 let new_json2 = r#"{"name":"Alice","age":null}"#;
302 let (old_diff2, new_diff2) = super::compute_diff(old_json2, new_json2);
303 assert_eq!(old_diff2.unwrap(), r#"{"age":null}"#);
304 assert_eq!(new_diff2.unwrap(), r#"{"age":null}"#);
305
306 let old_json3 = r#"{"name":"Alice"}"#;
307 let new_json3 = r#"{"name":"Alice","age":30}"#;
308 let (old_diff3, new_diff3) = super::compute_diff(old_json3, new_json3);
309 assert_eq!(old_diff3.unwrap(), r#"{"age":null}"#);
310 assert_eq!(new_diff3.unwrap(), r#"{"age":30}"#);
311 }
312}