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 AcpCreated,
20 AcpUpdated,
21 AcpModuleEnabled,
25 AcpModuleDisabled,
26 AcpArchived,
27 JournalEntryCreated,
28 JournalEntryDeleted,
29 UnitCreated,
30 UnitUpdated,
31 UnitDeleted,
32 UnitAssignedToOwner,
33 UnitOwnerCreated,
34 UnitOwnerUpdated,
35 UnitOwnerDeleted,
36 OwnerCreated,
37 OwnerUpdated,
38 ExpenseCreated,
39 ExpenseMarkedPaid,
40 ExpenseMarkedOverdue,
47 ExpenseCancelled,
50 ExpenseReactivated,
53 ExpenseUnpaid,
58 InvoiceUpdated,
59 InvoiceSubmitted,
60 InvoiceApproved,
61 InvoiceRejected,
62 MeetingCreated,
63 MeetingCompleted,
64 MeetingCancelled,
76 MeetingRescheduled,
79 MeetingQuorumValidated,
80 MeetingMinutesSent,
81 DocumentUploaded,
82 DocumentDeleted,
83
84 BoardMemberElected,
86 BoardMemberRemoved,
87 BoardMemberMandateRenewed,
88 BoardDecisionCreated,
89 BoardDecisionUpdated,
90 BoardDecisionCompleted,
91 BoardDecisionNotesAdded,
92 CdcAlertCreated,
94 CdcMembersElected,
96
97 ResolutionCreated,
99 ResolutionDeleted,
100 VoteCast,
101 VoteChanged,
102 VotingClosed,
103
104 TicketCreated,
106 TicketAssigned,
107 TicketStatusChanged,
108 TicketResolved,
109 TicketClosed,
110 TicketCancelled,
111 TicketReopened,
112 TicketDeleted,
113 TicketWorkOrderSent,
114 TicketUpdated,
116
117 NotificationCreated,
119 NotificationRead,
120 NotificationDeleted,
121 NotificationPreferenceUpdated,
122
123 PaymentCreated,
125 PaymentProcessing,
126 PaymentRequiresAction,
127 PaymentSucceeded,
128 PaymentFailed,
129 PaymentCancelled,
130 PaymentRefunded,
131 PaymentDeleted,
132
133 PaymentMethodCreated,
135 PaymentMethodUpdated,
136 PaymentMethodSetDefault,
137 PaymentMethodDeactivated,
138 PaymentMethodReactivated,
139 PaymentMethodDeleted,
140
141 ConvocationCreated,
143 ConvocationScheduled,
144 ConvocationSent,
145 ConvocationCancelled,
146 ConvocationDeleted,
147 ConvocationReminderSent,
148 ConvocationAttendanceUpdated,
149 ConvocationProxySet,
150 SecondConvocationScheduled,
151
152 QuoteCreated,
154 QuoteSubmitted,
155 QuoteUnderReview,
156 QuoteAccepted,
157 QuoteRejected,
158 QuoteWithdrawn,
159 QuoteExpired,
160 QuoteRatingUpdated,
161 QuoteComparisonPerformed,
162 QuoteDeleted,
163
164 ExchangeCreated,
166 ExchangeRequested,
167 ExchangeStarted,
168 ExchangeCompleted,
169 ExchangeCancelled,
170 ExchangeProviderRated,
171 ExchangeRequesterRated,
172 ExchangeDeleted,
173 CreditBalanceUpdated,
174 CreditBalanceCreated,
175
176 NoticeCreated,
178 NoticeUpdated,
179 NoticePublished,
180 NoticeArchived,
181 NoticePinned,
182 NoticeUnpinned,
183 NoticeExpirationSet,
184 NoticeExpired,
185 NoticeDeleted,
186
187 SkillCreated,
189 SkillUpdated,
190 SkillMarkedAvailable,
191 SkillMarkedUnavailable,
192 SkillDeleted,
193
194 SharedObjectCreated,
196 SharedObjectUpdated,
197 SharedObjectMarkedAvailable,
198 SharedObjectMarkedUnavailable,
199 SharedObjectBorrowed,
200 SharedObjectReturned,
201 SharedObjectDeleted,
202
203 ResourceBookingCreated,
205 ResourceBookingUpdated,
206 ResourceBookingCancelled,
207 ResourceBookingCompleted,
208 ResourceBookingNoShow,
209 ResourceBookingConfirmed,
210 ResourceBookingDeleted,
211
212 AchievementCreated,
214 AchievementUpdated,
215 AchievementDeleted,
216 AchievementAwarded,
217 ChallengeCreated,
218 ChallengeActivated,
219 ChallengeUpdated,
220 ChallengeCompleted,
221 ChallengeCancelled,
222 ChallengeDeleted,
223 ChallengeProgressIncremented,
224 ChallengeProgressCompleted,
225
226 PaymentReminderCreated,
228 PaymentReminderSent,
229 PaymentReminderOpened,
230 PaymentReminderPaid,
231 PaymentReminderCancelled,
232 PaymentReminderEscalated,
233 PaymentReminderTrackingAdded,
234 PaymentRemindersBulkCreated,
235 PaymentReminderDeleted,
236
237 EtatDateCreated,
239 EtatDateInProgress,
240 EtatDateGenerated,
241 EtatDateDelivered,
242 EtatDateFinancialUpdate,
243 EtatDateAdditionalDataUpdate,
244 EtatDateDeleted,
245
246 NotaryLinkIssued,
248 NotaryLinkConsulted,
249 NotaryLinkRenewed,
250 NotaryLinkRevoked,
251
252 BudgetCreated,
254 BudgetUpdated,
255 BudgetSubmitted,
256 BudgetApproved,
257 BudgetRejected,
258 BudgetArchived,
259 BudgetDeleted,
260
261 WorkReportCreated,
263 WorkReportUpdated,
264 WorkReportDeleted,
265 WorkReportPhotoAdded,
266 WorkReportDocumentAdded,
267
268 TechnicalInspectionCreated,
270 TechnicalInspectionUpdated,
271 TechnicalInspectionDeleted,
272 TechnicalInspectionReportAdded,
273 TechnicalInspectionPhotoAdded,
274 TechnicalInspectionCertificateAdded,
275
276 UnauthorizedAccess,
278 RateLimitExceeded,
279 InvalidToken,
280
281 TwoFactorSetupInitiated,
283 TwoFactorEnabled,
284 TwoFactorDisabled,
285 TwoFactorVerified,
286 TwoFactorVerificationFailed,
287 BackupCodeUsed,
288 BackupCodesRegenerated,
289 TwoFactorReverificationRequired,
290
291 IoTReadingCreated,
293 IoTReadingsBulkCreated,
294 LinkyDeviceConfigured,
295 LinkyDataSynced,
296 LinkyDeviceDeleted,
297 LinkySyncToggled,
298
299 GdprDataExported,
301 GdprDataExportFailed,
302 GdprDataErased,
303 GdprDataErasureFailed,
304 GdprErasureCheckRequested,
305 GdprDataRectified,
307 GdprDataRectificationFailed,
308 GdprProcessingRestricted,
310 GdprProcessingRestrictionFailed,
311 GdprMarketingOptOut,
313 GdprMarketingOptIn,
314 GdprMarketingPreferenceChangeFailed,
315
316 ConsentRecorded,
318 ConsentStatusChecked,
319
320 SecurityIncidentReported,
322
323 AccountCreated,
325 AccountUpdated,
326 AccountDeleted,
327 BelgianPCMNSeeded,
328
329 ReportGenerated,
331
332 PortfolioCreated,
334 PortfolioUpdated,
335 PortfolioDeleted,
336 PortfolioBuildingAdded,
337 PortfolioBuildingRemoved,
338 PortfolioShared,
339 PortfolioUnshared,
340
341 UnknownLegacyEvent,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct AuditLogEntry {
358 pub id: Uuid,
360 pub timestamp: chrono::DateTime<chrono::Utc>,
362 pub event_type: AuditEventType,
364 pub user_id: Option<Uuid>,
366 pub organization_id: Option<Uuid>,
368 pub resource_type: Option<String>,
370 pub resource_id: Option<Uuid>,
372 pub ip_address: Option<String>,
374 pub user_agent: Option<String>,
376 pub metadata: Option<serde_json::Value>,
378 pub success: bool,
380 pub error_message: Option<String>,
382}
383
384impl AuditLogEntry {
385 pub fn new(
387 event_type: AuditEventType,
388 user_id: Option<Uuid>,
389 organization_id: Option<Uuid>,
390 ) -> Self {
391 Self {
392 id: Uuid::new_v4(),
393 timestamp: Utc::now(),
394 event_type,
395 user_id,
396 organization_id,
397 resource_type: None,
398 resource_id: None,
399 ip_address: None,
400 user_agent: None,
401 metadata: None,
402 success: true,
403 error_message: None,
404 }
405 }
406
407 pub fn with_resource(mut self, resource_type: &str, resource_id: Uuid) -> Self {
409 self.resource_type = Some(resource_type.to_string());
410 self.resource_id = Some(resource_id);
411 self
412 }
413
414 pub fn with_client_info(
416 mut self,
417 ip_address: Option<String>,
418 user_agent: Option<String>,
419 ) -> Self {
420 self.ip_address = ip_address;
421 self.user_agent = user_agent;
422 self
423 }
424
425 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
427 self.metadata = Some(metadata);
428 self
429 }
430
431 pub fn with_error(mut self, error_message: String) -> Self {
433 self.success = false;
434 self.error_message = Some(error_message);
435 self
436 }
437
438 pub fn with_details(mut self, details: String) -> Self {
440 let details_json = serde_json::json!({ "details": details });
441 self.metadata = Some(details_json);
442 self
443 }
444
445 pub fn log(&self) {
447 let redact_presence = |present| if present { "[REDACTED]" } else { "None" };
449 let redacted_user = redact_presence(self.user_id.is_some());
450 let redacted_org = redact_presence(self.organization_id.is_some());
451 let redacted_resource_id = redact_presence(self.resource_id.is_some());
452 let redacted_ip = redact_presence(self.ip_address.is_some());
453 let redacted_error = self.error_message.as_ref().map(|_| "[REDACTED]");
454
455 let log_message = format!(
456 "[AUDIT] {} | {:?} | User: {} | Org: {} | Resource: {}/{} | Success: {} | IP: {}",
457 self.timestamp.format("%Y-%m-%d %H:%M:%S"),
458 self.event_type,
459 redacted_user,
460 redacted_org,
461 self.resource_type.as_deref().unwrap_or("None"),
462 redacted_resource_id,
463 self.success,
464 redacted_ip
465 );
466
467 if self.success {
468 log::info!("{}", log_message);
469 } else {
470 log::warn!(
471 "{} | Error: {}",
472 log_message,
473 redacted_error.unwrap_or("None")
474 );
475 }
476
477 }
484}
485
486pub async fn log_audit_event(
498 event_type: AuditEventType,
499 user_id: Option<Uuid>,
500 organization_id: Option<Uuid>,
501 details: Option<String>,
502 metadata: Option<serde_json::Value>,
503) {
504 let mut entry = AuditLogEntry::new(event_type, user_id, organization_id);
505
506 if let Some(details_str) = details {
507 entry = entry.with_details(details_str);
508 }
509
510 if let Some(meta) = metadata {
511 entry.metadata = Some(meta);
512 }
513
514 entry.log();
515}
516
517#[macro_export]
519macro_rules! audit_log {
520 ($event_type:expr, $user_id:expr, $org_id:expr) => {
521 $crate::infrastructure::audit::AuditLogEntry::new($event_type, $user_id, $org_id).log()
522 };
523 ($event_type:expr, $user_id:expr, $org_id:expr, $resource_type:expr, $resource_id:expr) => {
524 $crate::infrastructure::audit::AuditLogEntry::new($event_type, $user_id, $org_id)
525 .with_resource($resource_type, $resource_id)
526 .log()
527 };
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_audit_log_creation() {
536 let user_id = Uuid::new_v4();
537 let org_id = Uuid::new_v4();
538 let building_id = Uuid::new_v4();
539
540 let entry =
541 AuditLogEntry::new(AuditEventType::BuildingCreated, Some(user_id), Some(org_id))
542 .with_resource("Building", building_id)
543 .with_client_info(Some("192.168.1.1".to_string()), None);
544
545 assert_eq!(entry.user_id, Some(user_id));
546 assert_eq!(entry.organization_id, Some(org_id));
547 assert_eq!(entry.resource_id, Some(building_id));
548 assert!(entry.success);
549 }
550
551 #[test]
552 fn test_audit_log_with_error() {
553 let entry = AuditLogEntry::new(AuditEventType::AuthenticationFailed, None, None)
554 .with_error("Invalid credentials".to_string());
555
556 assert!(!entry.success);
557 assert_eq!(entry.error_message, Some("Invalid credentials".to_string()));
558 }
559}