Skip to main content

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    AcpCreated,
20    AcpUpdated,
21    /// Story 5.1 (#585) — activation/désactivation d'un module pour une ACP.
22    /// Tracé parce qu'éteindre un module fait disparaître des écrans pour
23    /// tout le monde : il faut pouvoir dire qui l'a fait et quand.
24    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    /// La dépense est marquée en retard : elle n'est PAS payée.
41    ///
42    /// Distincte de `ExpenseMarkedPaid`. `mark_expense_overdue` journalisait
43    /// `ExpenseMarkedPaid` — même défaut que `MeetingCancelled` ci-dessous,
44    /// une transition qui recopie l'évènement de la transition voisine.
45    /// Constaté en instruisant #881.
46    ExpenseMarkedOverdue,
47    /// La dépense est annulée : elle N'A PAS été payée, et ne le sera plus
48    /// sur cette écriture. Voir `ExpenseMarkedOverdue`. #881.
49    ExpenseCancelled,
50    /// Une dépense annulée redevient active : elle n'est toujours pas payée.
51    /// Voir `ExpenseMarkedOverdue`. #881.
52    ExpenseReactivated,
53    /// Un paiement enregistré est défait : le paiement n'a plus lieu. Le
54    /// défaut d'origine (#881) journalisait `ExpenseMarkedPaid` sur ce
55    /// geste précis — le registre affirmait l'inverse de ce qui venait de se
56    /// passer.
57    ExpenseUnpaid,
58    InvoiceUpdated,
59    InvoiceSubmitted,
60    InvoiceApproved,
61    InvoiceRejected,
62    MeetingCreated,
63    MeetingCompleted,
64    /// L'assemblée est annulée : elle N'A PAS eu lieu.
65    ///
66    /// Distincte de `MeetingCompleted`, qui affirme le contraire. Les deux
67    /// gestionnaires — annulation et report — journalisaient `MeetingCompleted`,
68    /// copié-collé depuis la clôture. Le registre de l'ACP disait donc qu'une
69    /// assemblée s'était tenue là où elle avait été annulée ou déplacée.
70    ///
71    /// Ce registre n'est pas décoratif : l'Art. 3.89 § 5 7° impose au syndic
72    /// de tenir le dossier de la copropriété, et un procès-verbal introuvable
73    /// pour une assemblée déclarée « tenue » est une non-conformité qui se
74    /// découvre au pire moment. Constaté en instruisant #780.
75    MeetingCancelled,
76    /// L'assemblée est reportée à une autre date : elle n'a pas eu lieu à la
77    /// date initiale, et se tiendra plus tard. Voir `MeetingCancelled`.
78    MeetingRescheduled,
79    MeetingQuorumValidated,
80    MeetingMinutesSent,
81    DocumentUploaded,
82    DocumentDeleted,
83
84    // Board management events
85    BoardMemberElected,
86    BoardMemberRemoved,
87    BoardMemberMandateRenewed,
88    BoardDecisionCreated,
89    BoardDecisionUpdated,
90    BoardDecisionCompleted,
91    BoardDecisionNotesAdded,
92    /// Story 4.7 — le conseil de copropriété alerte la prochaine AG.
93    CdcAlertCreated,
94    /// Story 4.7 — élection des membres du conseil à l'issue d'une AG clôturée.
95    CdcMembersElected,
96
97    // Voting events (Issue #46 - Phase 2)
98    ResolutionCreated,
99    ResolutionDeleted,
100    VoteCast,
101    VoteChanged,
102    VotingClosed,
103
104    // Ticketing events (Issue #85 - Phase 2)
105    TicketCreated,
106    TicketAssigned,
107    TicketStatusChanged,
108    TicketResolved,
109    TicketClosed,
110    TicketCancelled,
111    TicketReopened,
112    TicketDeleted,
113    TicketWorkOrderSent,
114    /// Story 3.6 (FR31 / INV-24) — PATCH field edit within the 5-min window.
115    TicketUpdated,
116
117    // Notification events (Issue #86 - Phase 2)
118    NotificationCreated,
119    NotificationRead,
120    NotificationDeleted,
121    NotificationPreferenceUpdated,
122
123    // Payment events (Issue #84 - Phase 2)
124    PaymentCreated,
125    PaymentProcessing,
126    PaymentRequiresAction,
127    PaymentSucceeded,
128    PaymentFailed,
129    PaymentCancelled,
130    PaymentRefunded,
131    PaymentDeleted,
132
133    // Payment method events (Issue #84 - Phase 2)
134    PaymentMethodCreated,
135    PaymentMethodUpdated,
136    PaymentMethodSetDefault,
137    PaymentMethodDeactivated,
138    PaymentMethodReactivated,
139    PaymentMethodDeleted,
140
141    // Convocation events (Issue #88 - Phase 2)
142    ConvocationCreated,
143    ConvocationScheduled,
144    ConvocationSent,
145    ConvocationCancelled,
146    ConvocationDeleted,
147    ConvocationReminderSent,
148    ConvocationAttendanceUpdated,
149    ConvocationProxySet,
150    SecondConvocationScheduled,
151
152    // Quote events (Contractor Quotes Module - Issue #91 - Phase 2)
153    QuoteCreated,
154    QuoteSubmitted,
155    QuoteUnderReview,
156    QuoteAccepted,
157    QuoteRejected,
158    QuoteWithdrawn,
159    QuoteExpired,
160    QuoteRatingUpdated,
161    QuoteComparisonPerformed,
162    QuoteDeleted,
163
164    // SEL events (Local Exchange Trading System - Issue #49 - Phase 2)
165    ExchangeCreated,
166    ExchangeRequested,
167    ExchangeStarted,
168    ExchangeCompleted,
169    ExchangeCancelled,
170    ExchangeProviderRated,
171    ExchangeRequesterRated,
172    ExchangeDeleted,
173    CreditBalanceUpdated,
174    CreditBalanceCreated,
175
176    // Notice events (Community Notice Board - Issue #49 - Phase 2)
177    NoticeCreated,
178    NoticeUpdated,
179    NoticePublished,
180    NoticeArchived,
181    NoticePinned,
182    NoticeUnpinned,
183    NoticeExpirationSet,
184    NoticeExpired,
185    NoticeDeleted,
186
187    // Skill events (Skills Directory - Issue #49 - Phase 3)
188    SkillCreated,
189    SkillUpdated,
190    SkillMarkedAvailable,
191    SkillMarkedUnavailable,
192    SkillDeleted,
193
194    // Shared Object events (Object Sharing Library - Issue #49 - Phase 4)
195    SharedObjectCreated,
196    SharedObjectUpdated,
197    SharedObjectMarkedAvailable,
198    SharedObjectMarkedUnavailable,
199    SharedObjectBorrowed,
200    SharedObjectReturned,
201    SharedObjectDeleted,
202
203    // Resource Booking events (Resource Booking Calendar - Issue #49 - Phase 5)
204    ResourceBookingCreated,
205    ResourceBookingUpdated,
206    ResourceBookingCancelled,
207    ResourceBookingCompleted,
208    ResourceBookingNoShow,
209    ResourceBookingConfirmed,
210    ResourceBookingDeleted,
211
212    // Gamification events (Achievements & Challenges - Issue #49 - Phase 6)
213    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    // Payment reminder events
227    PaymentReminderCreated,
228    PaymentReminderSent,
229    PaymentReminderOpened,
230    PaymentReminderPaid,
231    PaymentReminderCancelled,
232    PaymentReminderEscalated,
233    PaymentReminderTrackingAdded,
234    PaymentRemindersBulkCreated,
235    PaymentReminderDeleted,
236
237    // État Daté events (Belgian legal requirement for property sales)
238    EtatDateCreated,
239    EtatDateInProgress,
240    EtatDateGenerated,
241    EtatDateDelivered,
242    EtatDateFinancialUpdate,
243    EtatDateAdditionalDataUpdate,
244    EtatDateDeleted,
245
246    // Notary link events (#845 — ADR 0048, ADR 0051)
247    NotaryLinkIssued,
248    NotaryLinkConsulted,
249    NotaryLinkRenewed,
250    NotaryLinkRevoked,
251
252    // Budget events (Annual budget management)
253    BudgetCreated,
254    BudgetUpdated,
255    BudgetSubmitted,
256    BudgetApproved,
257    BudgetRejected,
258    BudgetArchived,
259    BudgetDeleted,
260
261    // Work Report events (Digital Maintenance Logbook - Issue #134)
262    WorkReportCreated,
263    WorkReportUpdated,
264    WorkReportDeleted,
265    WorkReportPhotoAdded,
266    WorkReportDocumentAdded,
267
268    // Technical Inspection events (Digital Maintenance Logbook - Issue #134)
269    TechnicalInspectionCreated,
270    TechnicalInspectionUpdated,
271    TechnicalInspectionDeleted,
272    TechnicalInspectionReportAdded,
273    TechnicalInspectionPhotoAdded,
274    TechnicalInspectionCertificateAdded,
275
276    // Security events
277    UnauthorizedAccess,
278    RateLimitExceeded,
279    InvalidToken,
280
281    // Two-Factor Authentication events (Issue #78 - Security Hardening)
282    TwoFactorSetupInitiated,
283    TwoFactorEnabled,
284    TwoFactorDisabled,
285    TwoFactorVerified,
286    TwoFactorVerificationFailed,
287    BackupCodeUsed,
288    BackupCodesRegenerated,
289    TwoFactorReverificationRequired,
290
291    // IoT events (Linky/Ores Smart Meter Integration - Issue #133 - IoT Phase 0)
292    IoTReadingCreated,
293    IoTReadingsBulkCreated,
294    LinkyDeviceConfigured,
295    LinkyDataSynced,
296    LinkyDeviceDeleted,
297    LinkySyncToggled,
298
299    // GDPR events (Data Privacy Compliance - Article 30: Records of Processing)
300    GdprDataExported,
301    GdprDataExportFailed,
302    GdprDataErased,
303    GdprDataErasureFailed,
304    GdprErasureCheckRequested,
305    // GDPR Article 16: Right to Rectification
306    GdprDataRectified,
307    GdprDataRectificationFailed,
308    // GDPR Article 18: Right to Restriction of Processing
309    GdprProcessingRestricted,
310    GdprProcessingRestrictionFailed,
311    // GDPR Article 21: Right to Object (Marketing)
312    GdprMarketingOptOut,
313    GdprMarketingOptIn,
314    GdprMarketingPreferenceChangeFailed,
315
316    // GDPR Article 7: Consent Management (Issue #337)
317    ConsentRecorded,
318    ConsentStatusChecked,
319
320    // GDPR Article 33: Security Incidents & APD Notification (Issue #317)
321    SecurityIncidentReported,
322
323    // Accounting events
324    AccountCreated,
325    AccountUpdated,
326    AccountDeleted,
327    BelgianPCMNSeeded,
328
329    // Financial reporting events
330    ReportGenerated,
331
332    // Portfolio events (Story 2.1 — ADR-0011, Slice 2 Refonte UX multi-rôle ACP)
333    PortfolioCreated,
334    PortfolioUpdated,
335    PortfolioDeleted,
336    PortfolioBuildingAdded,
337    PortfolioBuildingRemoved,
338    PortfolioShared,
339    PortfolioUnshared,
340
341    /// Type d'évènement lu en base et inconnu du code courant.
342    ///
343    /// Utilisé UNIQUEMENT à la relecture, jamais à l'écriture. Il existe pour
344    /// qu'une ligne d'audit ancienne ou écrite par une version plus récente
345    /// se lise pour ce qu'elle est — inconnue — plutôt que de se déguiser en
346    /// autre chose.
347    ///
348    /// La relecture repliait auparavant tout type non reconnu sur
349    /// `UnauthorizedAccess`. Voir `string_to_event_type` : la table de
350    /// correspondance couvrait 29 des 223 variantes, si bien que le registre
351    /// relisait 194 types d'évènements comme des accès non autorisés.
352    UnknownLegacyEvent,
353}
354
355/// Audit log entry
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct AuditLogEntry {
358    /// Unique ID for this audit entry
359    pub id: Uuid,
360    /// Timestamp of the event
361    pub timestamp: chrono::DateTime<chrono::Utc>,
362    /// Type of event
363    pub event_type: AuditEventType,
364    /// User ID who performed the action (if authenticated)
365    pub user_id: Option<Uuid>,
366    /// Organization ID (for multi-tenant isolation)
367    pub organization_id: Option<Uuid>,
368    /// Resource type affected (e.g., "Building", "Unit")
369    pub resource_type: Option<String>,
370    /// Resource ID affected
371    pub resource_id: Option<Uuid>,
372    /// IP address of the client
373    pub ip_address: Option<String>,
374    /// User agent string
375    pub user_agent: Option<String>,
376    /// Additional metadata as JSON
377    pub metadata: Option<serde_json::Value>,
378    /// Success or failure
379    pub success: bool,
380    /// Error message if failed
381    pub error_message: Option<String>,
382}
383
384impl AuditLogEntry {
385    /// Create a new audit log entry
386    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    /// Set resource information
408    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    /// Set client information
415    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    /// Set metadata
426    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
427        self.metadata = Some(metadata);
428        self
429    }
430
431    /// Mark as failed with error message
432    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    /// Add details to metadata as a string
439    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    /// Log this entry (currently to stdout, can be extended to database/file)
446    pub fn log(&self) {
447        // Redact sensitive information for logging (GDPR compliance)
448        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        // TODO: In production, write full (unredacted) audit data to:
478        // - Database table (audit_logs) with encryption at rest
479        // - Rotating log files in secure location with restricted access
480        // - SIEM system (Security Information and Event Management)
481        // Note: Full audit data (including IP, error messages) should only be
482        // stored in secure, access-controlled systems for compliance and forensics
483    }
484}
485
486/// Helper function to log audit events asynchronously
487///
488/// This is a convenience function for background audit logging
489/// without database persistence (logs to stdout/file only).
490///
491/// Parameters:
492/// - event_type: Type of audit event
493/// - user_id: Optional user ID who performed the action
494/// - organization_id: Optional organization ID for multi-tenant isolation
495/// - details: Optional details string
496/// - metadata: Optional additional metadata as JSON
497pub 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/// Helper macro to create and log audit entries
518#[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}