koprogo_api/infrastructure/
audit.rs1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub enum AuditEventType {
8 UserLogin,
10 UserLogout,
11 UserRegistration,
12 TokenRefresh,
13 AuthenticationFailed,
14
15 BuildingCreated,
17 BuildingUpdated,
18 BuildingDeleted,
19 UnitCreated,
20 UnitAssignedToOwner,
21 OwnerCreated,
22 OwnerUpdated,
23 ExpenseCreated,
24 ExpenseMarkedPaid,
25 MeetingCreated,
26 MeetingCompleted,
27 DocumentUploaded,
28 DocumentDeleted,
29
30 UnauthorizedAccess,
32 RateLimitExceeded,
33 InvalidToken,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AuditLogEntry {
39 pub id: Uuid,
41 pub timestamp: chrono::DateTime<chrono::Utc>,
43 pub event_type: AuditEventType,
45 pub user_id: Option<Uuid>,
47 pub organization_id: Option<Uuid>,
49 pub resource_type: Option<String>,
51 pub resource_id: Option<Uuid>,
53 pub ip_address: Option<String>,
55 pub user_agent: Option<String>,
57 pub metadata: Option<serde_json::Value>,
59 pub success: bool,
61 pub error_message: Option<String>,
63}
64
65impl AuditLogEntry {
66 pub fn new(
68 event_type: AuditEventType,
69 user_id: Option<Uuid>,
70 organization_id: Option<Uuid>,
71 ) -> Self {
72 Self {
73 id: Uuid::new_v4(),
74 timestamp: Utc::now(),
75 event_type,
76 user_id,
77 organization_id,
78 resource_type: None,
79 resource_id: None,
80 ip_address: None,
81 user_agent: None,
82 metadata: None,
83 success: true,
84 error_message: None,
85 }
86 }
87
88 pub fn with_resource(mut self, resource_type: &str, resource_id: Uuid) -> Self {
90 self.resource_type = Some(resource_type.to_string());
91 self.resource_id = Some(resource_id);
92 self
93 }
94
95 pub fn with_client_info(
97 mut self,
98 ip_address: Option<String>,
99 user_agent: Option<String>,
100 ) -> Self {
101 self.ip_address = ip_address;
102 self.user_agent = user_agent;
103 self
104 }
105
106 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
108 self.metadata = Some(metadata);
109 self
110 }
111
112 pub fn with_error(mut self, error_message: String) -> Self {
114 self.success = false;
115 self.error_message = Some(error_message);
116 self
117 }
118
119 pub fn log(&self) {
121 let redacted_ip = self.ip_address.as_ref().map(|_| "[REDACTED]");
123 let redacted_error = self.error_message.as_ref().map(|_| "[REDACTED]");
124
125 let log_message = format!(
126 "[AUDIT] {} | {:?} | User: {:?} | Org: {:?} | Resource: {:?}/{:?} | Success: {} | IP: {:?}",
127 self.timestamp.format("%Y-%m-%d %H:%M:%S"),
128 self.event_type,
129 self.user_id,
130 self.organization_id,
131 self.resource_type,
132 self.resource_id,
133 self.success,
134 redacted_ip
135 );
136
137 if self.success {
138 log::info!("{}", log_message);
139 } else {
140 log::warn!("{} | Error: {:?}", log_message, redacted_error);
141 }
142
143 }
150}
151
152#[macro_export]
154macro_rules! audit_log {
155 ($event_type:expr, $user_id:expr, $org_id:expr) => {
156 $crate::infrastructure::audit::AuditLogEntry::new($event_type, $user_id, $org_id).log()
157 };
158 ($event_type:expr, $user_id:expr, $org_id:expr, $resource_type:expr, $resource_id:expr) => {
159 $crate::infrastructure::audit::AuditLogEntry::new($event_type, $user_id, $org_id)
160 .with_resource($resource_type, $resource_id)
161 .log()
162 };
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_audit_log_creation() {
171 let user_id = Uuid::new_v4();
172 let org_id = Uuid::new_v4();
173 let building_id = Uuid::new_v4();
174
175 let entry =
176 AuditLogEntry::new(AuditEventType::BuildingCreated, Some(user_id), Some(org_id))
177 .with_resource("Building", building_id)
178 .with_client_info(Some("192.168.1.1".to_string()), None);
179
180 assert_eq!(entry.user_id, Some(user_id));
181 assert_eq!(entry.organization_id, Some(org_id));
182 assert_eq!(entry.resource_id, Some(building_id));
183 assert!(entry.success);
184 }
185
186 #[test]
187 fn test_audit_log_with_error() {
188 let entry = AuditLogEntry::new(AuditEventType::AuthenticationFailed, None, None)
189 .with_error("Invalid credentials".to_string());
190
191 assert!(!entry.success);
192 assert_eq!(entry.error_message, Some("Invalid credentials".to_string()));
193 }
194}