koprogo_api/infrastructure/
audit.rs

1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Audit log event types
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum AuditEventType {
8    // Authentication events
9    UserLogin,
10    UserLogout,
11    UserRegistration,
12    TokenRefresh,
13    AuthenticationFailed,
14
15    // Data modification events
16    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    // Board management events
42    BoardMemberElected,
43    BoardMemberRemoved,
44    BoardMemberMandateRenewed,
45    BoardDecisionCreated,
46    BoardDecisionUpdated,
47    BoardDecisionCompleted,
48    BoardDecisionNotesAdded,
49
50    // Voting events (Issue #46 - Phase 2)
51    ResolutionCreated,
52    ResolutionDeleted,
53    VoteCast,
54    VoteChanged,
55    VotingClosed,
56
57    // Ticketing events (Issue #85 - Phase 2)
58    TicketCreated,
59    TicketAssigned,
60    TicketStatusChanged,
61    TicketResolved,
62    TicketClosed,
63    TicketCancelled,
64    TicketReopened,
65    TicketDeleted,
66
67    // Notification events (Issue #86 - Phase 2)
68    NotificationCreated,
69    NotificationRead,
70    NotificationDeleted,
71    NotificationPreferenceUpdated,
72
73    // Payment events (Issue #84 - Phase 2)
74    PaymentCreated,
75    PaymentProcessing,
76    PaymentRequiresAction,
77    PaymentSucceeded,
78    PaymentFailed,
79    PaymentCancelled,
80    PaymentRefunded,
81    PaymentDeleted,
82
83    // Payment method events (Issue #84 - Phase 2)
84    PaymentMethodCreated,
85    PaymentMethodUpdated,
86    PaymentMethodSetDefault,
87    PaymentMethodDeactivated,
88    PaymentMethodReactivated,
89    PaymentMethodDeleted,
90
91    // Convocation events (Issue #88 - Phase 2)
92    ConvocationCreated,
93    ConvocationScheduled,
94    ConvocationSent,
95    ConvocationCancelled,
96    ConvocationDeleted,
97    ConvocationReminderSent,
98    ConvocationAttendanceUpdated,
99    ConvocationProxySet,
100
101    // Quote events (Contractor Quotes Module - Issue #91 - Phase 2)
102    QuoteCreated,
103    QuoteSubmitted,
104    QuoteUnderReview,
105    QuoteAccepted,
106    QuoteRejected,
107    QuoteWithdrawn,
108    QuoteExpired,
109    QuoteRatingUpdated,
110    QuoteComparisonPerformed,
111    QuoteDeleted,
112
113    // SEL events (Local Exchange Trading System - Issue #49 - Phase 2)
114    ExchangeCreated,
115    ExchangeRequested,
116    ExchangeStarted,
117    ExchangeCompleted,
118    ExchangeCancelled,
119    ExchangeProviderRated,
120    ExchangeRequesterRated,
121    ExchangeDeleted,
122    CreditBalanceUpdated,
123    CreditBalanceCreated,
124
125    // Notice events (Community Notice Board - Issue #49 - Phase 2)
126    NoticeCreated,
127    NoticeUpdated,
128    NoticePublished,
129    NoticeArchived,
130    NoticePinned,
131    NoticeUnpinned,
132    NoticeExpirationSet,
133    NoticeExpired,
134    NoticeDeleted,
135
136    // Skill events (Skills Directory - Issue #49 - Phase 3)
137    SkillCreated,
138    SkillUpdated,
139    SkillMarkedAvailable,
140    SkillMarkedUnavailable,
141    SkillDeleted,
142
143    // Shared Object events (Object Sharing Library - Issue #49 - Phase 4)
144    SharedObjectCreated,
145    SharedObjectUpdated,
146    SharedObjectMarkedAvailable,
147    SharedObjectMarkedUnavailable,
148    SharedObjectBorrowed,
149    SharedObjectReturned,
150    SharedObjectDeleted,
151
152    // Resource Booking events (Resource Booking Calendar - Issue #49 - Phase 5)
153    ResourceBookingCreated,
154    ResourceBookingUpdated,
155    ResourceBookingCancelled,
156    ResourceBookingCompleted,
157    ResourceBookingNoShow,
158    ResourceBookingConfirmed,
159    ResourceBookingDeleted,
160
161    // Gamification events (Achievements & Challenges - Issue #49 - Phase 6)
162    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    // Payment reminder events
176    PaymentReminderCreated,
177    PaymentReminderSent,
178    PaymentReminderOpened,
179    PaymentReminderPaid,
180    PaymentReminderCancelled,
181    PaymentReminderEscalated,
182    PaymentReminderTrackingAdded,
183    PaymentRemindersBulkCreated,
184    PaymentReminderDeleted,
185
186    // État Daté events (Belgian legal requirement for property sales)
187    EtatDateCreated,
188    EtatDateInProgress,
189    EtatDateGenerated,
190    EtatDateDelivered,
191    EtatDateFinancialUpdate,
192    EtatDateAdditionalDataUpdate,
193    EtatDateDeleted,
194
195    // Budget events (Annual budget management)
196    BudgetCreated,
197    BudgetUpdated,
198    BudgetSubmitted,
199    BudgetApproved,
200    BudgetRejected,
201    BudgetArchived,
202    BudgetDeleted,
203
204    // Work Report events (Digital Maintenance Logbook - Issue #134)
205    WorkReportCreated,
206    WorkReportUpdated,
207    WorkReportDeleted,
208    WorkReportPhotoAdded,
209    WorkReportDocumentAdded,
210
211    // Technical Inspection events (Digital Maintenance Logbook - Issue #134)
212    TechnicalInspectionCreated,
213    TechnicalInspectionUpdated,
214    TechnicalInspectionDeleted,
215    TechnicalInspectionReportAdded,
216    TechnicalInspectionPhotoAdded,
217    TechnicalInspectionCertificateAdded,
218
219    // Security events
220    UnauthorizedAccess,
221    RateLimitExceeded,
222    InvalidToken,
223
224    // Two-Factor Authentication events (Issue #78 - Security Hardening)
225    TwoFactorSetupInitiated,
226    TwoFactorEnabled,
227    TwoFactorDisabled,
228    TwoFactorVerified,
229    TwoFactorVerificationFailed,
230    BackupCodeUsed,
231    BackupCodesRegenerated,
232    TwoFactorReverificationRequired,
233
234    // IoT events (Linky/Ores Smart Meter Integration - Issue #133 - IoT Phase 0)
235    IoTReadingCreated,
236    IoTReadingsBulkCreated,
237    LinkyDeviceConfigured,
238    LinkyDataSynced,
239    LinkyDeviceDeleted,
240    LinkySyncToggled,
241
242    // GDPR events (Data Privacy Compliance - Article 30: Records of Processing)
243    GdprDataExported,
244    GdprDataExportFailed,
245    GdprDataErased,
246    GdprDataErasureFailed,
247    GdprErasureCheckRequested,
248    // GDPR Article 16: Right to Rectification
249    GdprDataRectified,
250    GdprDataRectificationFailed,
251    // GDPR Article 18: Right to Restriction of Processing
252    GdprProcessingRestricted,
253    GdprProcessingRestrictionFailed,
254    // GDPR Article 21: Right to Object (Marketing)
255    GdprMarketingOptOut,
256    GdprMarketingOptIn,
257    GdprMarketingPreferenceChangeFailed,
258
259    // Accounting events
260    AccountCreated,
261    AccountUpdated,
262    AccountDeleted,
263    BelgianPCMNSeeded,
264
265    // Financial reporting events
266    ReportGenerated,
267}
268
269/// Audit log entry
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct AuditLogEntry {
272    /// Unique ID for this audit entry
273    pub id: Uuid,
274    /// Timestamp of the event
275    pub timestamp: chrono::DateTime<chrono::Utc>,
276    /// Type of event
277    pub event_type: AuditEventType,
278    /// User ID who performed the action (if authenticated)
279    pub user_id: Option<Uuid>,
280    /// Organization ID (for multi-tenant isolation)
281    pub organization_id: Option<Uuid>,
282    /// Resource type affected (e.g., "Building", "Unit")
283    pub resource_type: Option<String>,
284    /// Resource ID affected
285    pub resource_id: Option<Uuid>,
286    /// IP address of the client
287    pub ip_address: Option<String>,
288    /// User agent string
289    pub user_agent: Option<String>,
290    /// Additional metadata as JSON
291    pub metadata: Option<serde_json::Value>,
292    /// Success or failure
293    pub success: bool,
294    /// Error message if failed
295    pub error_message: Option<String>,
296}
297
298impl AuditLogEntry {
299    /// Create a new audit log entry
300    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    /// Set resource information
322    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    /// Set client information
329    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    /// Set metadata
340    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
341        self.metadata = Some(metadata);
342        self
343    }
344
345    /// Mark as failed with error message
346    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    /// Add details to metadata as a string
353    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    /// Log this entry (currently to stdout, can be extended to database/file)
360    pub fn log(&self) {
361        // Redact sensitive information for logging (GDPR compliance)
362        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        // TODO: In production, write full (unredacted) audit data to:
392        // - Database table (audit_logs) with encryption at rest
393        // - Rotating log files in secure location with restricted access
394        // - SIEM system (Security Information and Event Management)
395        // Note: Full audit data (including IP, error messages) should only be
396        // stored in secure, access-controlled systems for compliance and forensics
397    }
398}
399
400/// Helper function to log audit events asynchronously
401///
402/// This is a convenience function for background audit logging
403/// without database persistence (logs to stdout/file only).
404///
405/// Parameters:
406/// - event_type: Type of audit event
407/// - user_id: Optional user ID who performed the action
408/// - organization_id: Optional organization ID for multi-tenant isolation
409/// - details: Optional details string
410/// - metadata: Optional additional metadata as JSON
411pub 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/// Helper macro to create and log audit entries
432#[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}