1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum AuditEventType {
8 UserLogin,
10 UserLogout,
11 UserRegistration,
12 TokenRefresh,
13 AuthenticationFailed,
14
15 BuildingCreated,
17 BuildingUpdated,
18 BuildingDeleted,
19 JournalEntryCreated,
20 JournalEntryDeleted,
21 UnitCreated,
22 UnitUpdated,
23 UnitDeleted,
24 UnitAssignedToOwner,
25 UnitOwnerCreated,
26 UnitOwnerUpdated,
27 UnitOwnerDeleted,
28 OwnerCreated,
29 OwnerUpdated,
30 ExpenseCreated,
31 ExpenseMarkedPaid,
32 InvoiceUpdated,
33 InvoiceSubmitted,
34 InvoiceApproved,
35 InvoiceRejected,
36 MeetingCreated,
37 MeetingCompleted,
38 DocumentUploaded,
39 DocumentDeleted,
40
41 BoardMemberElected,
43 BoardMemberRemoved,
44 BoardMemberMandateRenewed,
45 BoardDecisionCreated,
46 BoardDecisionUpdated,
47 BoardDecisionCompleted,
48 BoardDecisionNotesAdded,
49
50 ResolutionCreated,
52 ResolutionDeleted,
53 VoteCast,
54 VoteChanged,
55 VotingClosed,
56
57 TicketCreated,
59 TicketAssigned,
60 TicketStatusChanged,
61 TicketResolved,
62 TicketClosed,
63 TicketCancelled,
64 TicketReopened,
65 TicketDeleted,
66
67 NotificationCreated,
69 NotificationRead,
70 NotificationDeleted,
71 NotificationPreferenceUpdated,
72
73 PaymentCreated,
75 PaymentProcessing,
76 PaymentRequiresAction,
77 PaymentSucceeded,
78 PaymentFailed,
79 PaymentCancelled,
80 PaymentRefunded,
81 PaymentDeleted,
82
83 PaymentMethodCreated,
85 PaymentMethodUpdated,
86 PaymentMethodSetDefault,
87 PaymentMethodDeactivated,
88 PaymentMethodReactivated,
89 PaymentMethodDeleted,
90
91 ConvocationCreated,
93 ConvocationScheduled,
94 ConvocationSent,
95 ConvocationCancelled,
96 ConvocationDeleted,
97 ConvocationReminderSent,
98 ConvocationAttendanceUpdated,
99 ConvocationProxySet,
100
101 QuoteCreated,
103 QuoteSubmitted,
104 QuoteUnderReview,
105 QuoteAccepted,
106 QuoteRejected,
107 QuoteWithdrawn,
108 QuoteExpired,
109 QuoteRatingUpdated,
110 QuoteComparisonPerformed,
111 QuoteDeleted,
112
113 ExchangeCreated,
115 ExchangeRequested,
116 ExchangeStarted,
117 ExchangeCompleted,
118 ExchangeCancelled,
119 ExchangeProviderRated,
120 ExchangeRequesterRated,
121 ExchangeDeleted,
122 CreditBalanceUpdated,
123 CreditBalanceCreated,
124
125 NoticeCreated,
127 NoticeUpdated,
128 NoticePublished,
129 NoticeArchived,
130 NoticePinned,
131 NoticeUnpinned,
132 NoticeExpirationSet,
133 NoticeExpired,
134 NoticeDeleted,
135
136 SkillCreated,
138 SkillUpdated,
139 SkillMarkedAvailable,
140 SkillMarkedUnavailable,
141 SkillDeleted,
142
143 SharedObjectCreated,
145 SharedObjectUpdated,
146 SharedObjectMarkedAvailable,
147 SharedObjectMarkedUnavailable,
148 SharedObjectBorrowed,
149 SharedObjectReturned,
150 SharedObjectDeleted,
151
152 ResourceBookingCreated,
154 ResourceBookingUpdated,
155 ResourceBookingCancelled,
156 ResourceBookingCompleted,
157 ResourceBookingNoShow,
158 ResourceBookingConfirmed,
159 ResourceBookingDeleted,
160
161 AchievementCreated,
163 AchievementUpdated,
164 AchievementDeleted,
165 AchievementAwarded,
166 ChallengeCreated,
167 ChallengeActivated,
168 ChallengeUpdated,
169 ChallengeCompleted,
170 ChallengeCancelled,
171 ChallengeDeleted,
172 ChallengeProgressIncremented,
173 ChallengeProgressCompleted,
174
175 PaymentReminderCreated,
177 PaymentReminderSent,
178 PaymentReminderOpened,
179 PaymentReminderPaid,
180 PaymentReminderCancelled,
181 PaymentReminderEscalated,
182 PaymentReminderTrackingAdded,
183 PaymentRemindersBulkCreated,
184 PaymentReminderDeleted,
185
186 EtatDateCreated,
188 EtatDateInProgress,
189 EtatDateGenerated,
190 EtatDateDelivered,
191 EtatDateFinancialUpdate,
192 EtatDateAdditionalDataUpdate,
193 EtatDateDeleted,
194
195 BudgetCreated,
197 BudgetUpdated,
198 BudgetSubmitted,
199 BudgetApproved,
200 BudgetRejected,
201 BudgetArchived,
202 BudgetDeleted,
203
204 WorkReportCreated,
206 WorkReportUpdated,
207 WorkReportDeleted,
208 WorkReportPhotoAdded,
209 WorkReportDocumentAdded,
210
211 TechnicalInspectionCreated,
213 TechnicalInspectionUpdated,
214 TechnicalInspectionDeleted,
215 TechnicalInspectionReportAdded,
216 TechnicalInspectionPhotoAdded,
217 TechnicalInspectionCertificateAdded,
218
219 UnauthorizedAccess,
221 RateLimitExceeded,
222 InvalidToken,
223
224 TwoFactorSetupInitiated,
226 TwoFactorEnabled,
227 TwoFactorDisabled,
228 TwoFactorVerified,
229 TwoFactorVerificationFailed,
230 BackupCodeUsed,
231 BackupCodesRegenerated,
232 TwoFactorReverificationRequired,
233
234 IoTReadingCreated,
236 IoTReadingsBulkCreated,
237 LinkyDeviceConfigured,
238 LinkyDataSynced,
239 LinkyDeviceDeleted,
240 LinkySyncToggled,
241
242 GdprDataExported,
244 GdprDataExportFailed,
245 GdprDataErased,
246 GdprDataErasureFailed,
247 GdprErasureCheckRequested,
248 GdprDataRectified,
250 GdprDataRectificationFailed,
251 GdprProcessingRestricted,
253 GdprProcessingRestrictionFailed,
254 GdprMarketingOptOut,
256 GdprMarketingOptIn,
257 GdprMarketingPreferenceChangeFailed,
258
259 AccountCreated,
261 AccountUpdated,
262 AccountDeleted,
263 BelgianPCMNSeeded,
264
265 ReportGenerated,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct AuditLogEntry {
272 pub id: Uuid,
274 pub timestamp: chrono::DateTime<chrono::Utc>,
276 pub event_type: AuditEventType,
278 pub user_id: Option<Uuid>,
280 pub organization_id: Option<Uuid>,
282 pub resource_type: Option<String>,
284 pub resource_id: Option<Uuid>,
286 pub ip_address: Option<String>,
288 pub user_agent: Option<String>,
290 pub metadata: Option<serde_json::Value>,
292 pub success: bool,
294 pub error_message: Option<String>,
296}
297
298impl AuditLogEntry {
299 pub fn new(
301 event_type: AuditEventType,
302 user_id: Option<Uuid>,
303 organization_id: Option<Uuid>,
304 ) -> Self {
305 Self {
306 id: Uuid::new_v4(),
307 timestamp: Utc::now(),
308 event_type,
309 user_id,
310 organization_id,
311 resource_type: None,
312 resource_id: None,
313 ip_address: None,
314 user_agent: None,
315 metadata: None,
316 success: true,
317 error_message: None,
318 }
319 }
320
321 pub fn with_resource(mut self, resource_type: &str, resource_id: Uuid) -> Self {
323 self.resource_type = Some(resource_type.to_string());
324 self.resource_id = Some(resource_id);
325 self
326 }
327
328 pub fn with_client_info(
330 mut self,
331 ip_address: Option<String>,
332 user_agent: Option<String>,
333 ) -> Self {
334 self.ip_address = ip_address;
335 self.user_agent = user_agent;
336 self
337 }
338
339 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
341 self.metadata = Some(metadata);
342 self
343 }
344
345 pub fn with_error(mut self, error_message: String) -> Self {
347 self.success = false;
348 self.error_message = Some(error_message);
349 self
350 }
351
352 pub fn with_details(mut self, details: String) -> Self {
354 let details_json = serde_json::json!({ "details": details });
355 self.metadata = Some(details_json);
356 self
357 }
358
359 pub fn log(&self) {
361 let redact_presence = |present| if present { "[REDACTED]" } else { "None" };
363 let redacted_user = redact_presence(self.user_id.is_some());
364 let redacted_org = redact_presence(self.organization_id.is_some());
365 let redacted_resource_id = redact_presence(self.resource_id.is_some());
366 let redacted_ip = redact_presence(self.ip_address.is_some());
367 let redacted_error = self.error_message.as_ref().map(|_| "[REDACTED]");
368
369 let log_message = format!(
370 "[AUDIT] {} | {:?} | User: {} | Org: {} | Resource: {}/{} | Success: {} | IP: {}",
371 self.timestamp.format("%Y-%m-%d %H:%M:%S"),
372 self.event_type,
373 redacted_user,
374 redacted_org,
375 self.resource_type.as_deref().unwrap_or("None"),
376 redacted_resource_id,
377 self.success,
378 redacted_ip
379 );
380
381 if self.success {
382 log::info!("{}", log_message);
383 } else {
384 log::warn!(
385 "{} | Error: {}",
386 log_message,
387 redacted_error.unwrap_or("None")
388 );
389 }
390
391 }
398}
399
400pub async fn log_audit_event(
412 event_type: AuditEventType,
413 user_id: Option<Uuid>,
414 organization_id: Option<Uuid>,
415 details: Option<String>,
416 metadata: Option<serde_json::Value>,
417) {
418 let mut entry = AuditLogEntry::new(event_type, user_id, organization_id);
419
420 if let Some(details_str) = details {
421 entry = entry.with_details(details_str);
422 }
423
424 if let Some(meta) = metadata {
425 entry.metadata = Some(meta);
426 }
427
428 entry.log();
429}
430
431#[macro_export]
433macro_rules! audit_log {
434 ($event_type:expr, $user_id:expr, $org_id:expr) => {
435 $crate::infrastructure::audit::AuditLogEntry::new($event_type, $user_id, $org_id).log()
436 };
437 ($event_type:expr, $user_id:expr, $org_id:expr, $resource_type:expr, $resource_id:expr) => {
438 $crate::infrastructure::audit::AuditLogEntry::new($event_type, $user_id, $org_id)
439 .with_resource($resource_type, $resource_id)
440 .log()
441 };
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn test_audit_log_creation() {
450 let user_id = Uuid::new_v4();
451 let org_id = Uuid::new_v4();
452 let building_id = Uuid::new_v4();
453
454 let entry =
455 AuditLogEntry::new(AuditEventType::BuildingCreated, Some(user_id), Some(org_id))
456 .with_resource("Building", building_id)
457 .with_client_info(Some("192.168.1.1".to_string()), None);
458
459 assert_eq!(entry.user_id, Some(user_id));
460 assert_eq!(entry.organization_id, Some(org_id));
461 assert_eq!(entry.resource_id, Some(building_id));
462 assert!(entry.success);
463 }
464
465 #[test]
466 fn test_audit_log_with_error() {
467 let entry = AuditLogEntry::new(AuditEventType::AuthenticationFailed, None, None)
468 .with_error("Invalid credentials".to_string());
469
470 assert!(!entry.success);
471 assert_eq!(entry.error_message, Some("Invalid credentials".to_string()));
472 }
473}