1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::Timestamp;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum AuditAction {
9 Create,
10 Read,
11 Update,
12 Delete,
13 StatusChange,
14 Crystallize,
15 Fulfill,
16 Break,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub enum AuditEntityType {
22 Goal,
23 Decision,
24 Commitment,
25 Dream,
26 Federation,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct AuditEntry {
32 pub timestamp: Timestamp,
33 pub session_id: Uuid,
34 pub operation: String,
35 pub entity_type: AuditEntityType,
36 pub entity_id: String,
37 pub action: AuditAction,
38 pub details: Option<String>,
39 pub success: bool,
40 pub error: Option<String>,
41}
42
43#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct AuditLog {
46 pub entries: Vec<AuditEntry>,
47}
48
49impl AuditLog {
50 pub fn new() -> Self {
51 Self {
52 entries: Vec::new(),
53 }
54 }
55
56 pub fn append(&mut self, entry: AuditEntry) {
57 self.entries.push(entry);
58 }
59
60 #[allow(clippy::too_many_arguments)]
61 pub fn record(
62 &mut self,
63 session_id: Uuid,
64 operation: impl Into<String>,
65 entity_type: AuditEntityType,
66 entity_id: impl Into<String>,
67 action: AuditAction,
68 success: bool,
69 details: Option<String>,
70 error: Option<String>,
71 ) {
72 self.entries.push(AuditEntry {
73 timestamp: Timestamp::now(),
74 session_id,
75 operation: operation.into(),
76 entity_type,
77 entity_id: entity_id.into(),
78 action,
79 details,
80 success,
81 error,
82 });
83 }
84
85 pub fn len(&self) -> usize {
86 self.entries.len()
87 }
88
89 pub fn is_empty(&self) -> bool {
90 self.entries.is_empty()
91 }
92
93 pub fn entries_for_entity(&self, entity_id: &str) -> Vec<&AuditEntry> {
94 self.entries
95 .iter()
96 .filter(|e| e.entity_id == entity_id)
97 .collect()
98 }
99
100 pub fn entries_by_action(&self, action: AuditAction) -> Vec<&AuditEntry> {
101 self.entries.iter().filter(|e| e.action == action).collect()
102 }
103
104 pub fn entries_since(&self, since: Timestamp) -> Vec<&AuditEntry> {
105 self.entries
106 .iter()
107 .filter(|e| e.timestamp.0 >= since.0)
108 .collect()
109 }
110
111 pub fn failures(&self) -> Vec<&AuditEntry> {
112 self.entries.iter().filter(|e| !e.success).collect()
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_audit_log_append_and_query() {
122 let mut log = AuditLog::new();
123 assert!(log.is_empty());
124
125 let session = Uuid::new_v4();
126 log.record(
127 session,
128 "create_goal",
129 AuditEntityType::Goal,
130 "goal-123",
131 AuditAction::Create,
132 true,
133 Some("Created goal".into()),
134 None,
135 );
136
137 assert_eq!(log.len(), 1);
138 assert!(!log.is_empty());
139 assert_eq!(log.entries_for_entity("goal-123").len(), 1);
140 assert_eq!(log.entries_by_action(AuditAction::Create).len(), 1);
141 assert!(log.failures().is_empty());
142 }
143
144 #[test]
145 fn test_audit_log_failures() {
146 let mut log = AuditLog::new();
147 let session = Uuid::new_v4();
148
149 log.record(
150 session,
151 "create_goal",
152 AuditEntityType::Goal,
153 "goal-1",
154 AuditAction::Create,
155 true,
156 None,
157 None,
158 );
159
160 log.record(
161 session,
162 "update_goal",
163 AuditEntityType::Goal,
164 "goal-2",
165 AuditAction::Update,
166 false,
167 None,
168 Some("Goal not found".into()),
169 );
170
171 assert_eq!(log.len(), 2);
172 assert_eq!(log.failures().len(), 1);
173 assert_eq!(log.failures()[0].entity_id, "goal-2");
174 }
175
176 #[test]
177 fn test_audit_log_entries_since() {
178 let mut log = AuditLog::new();
179 let session = Uuid::new_v4();
180 let before = Timestamp::now();
181
182 log.record(
183 session,
184 "create_decision",
185 AuditEntityType::Decision,
186 "dec-1",
187 AuditAction::Create,
188 true,
189 None,
190 None,
191 );
192
193 log.record(
194 session,
195 "crystallize_decision",
196 AuditEntityType::Decision,
197 "dec-1",
198 AuditAction::Crystallize,
199 true,
200 None,
201 None,
202 );
203
204 assert_eq!(log.entries_since(before).len(), 2);
205 assert_eq!(log.entries_by_action(AuditAction::Crystallize).len(), 1);
206 }
207
208 #[test]
209 fn test_audit_entity_types_and_actions() {
210 let mut log = AuditLog::new();
211 let session = Uuid::new_v4();
212
213 let actions = [
214 (AuditAction::Create, AuditEntityType::Goal),
215 (AuditAction::Read, AuditEntityType::Decision),
216 (AuditAction::Update, AuditEntityType::Commitment),
217 (AuditAction::Delete, AuditEntityType::Dream),
218 (AuditAction::StatusChange, AuditEntityType::Goal),
219 (AuditAction::Crystallize, AuditEntityType::Decision),
220 (AuditAction::Fulfill, AuditEntityType::Commitment),
221 (AuditAction::Break, AuditEntityType::Federation),
222 ];
223
224 for (i, (action, entity_type)) in actions.iter().enumerate() {
225 log.record(
226 session,
227 format!("op_{}", i),
228 *entity_type,
229 format!("id-{}", i),
230 *action,
231 true,
232 None,
233 None,
234 );
235 }
236
237 assert_eq!(log.len(), 8);
238 assert_eq!(log.entries_by_action(AuditAction::Create).len(), 1);
239 assert_eq!(log.entries_by_action(AuditAction::Fulfill).len(), 1);
240 }
241}