package dev.lions.unionflow.server.security; import com.fasterxml.jackson.databind.ObjectMapper; import dev.lions.unionflow.server.entity.AuditTrailOperation; import dev.lions.unionflow.server.repository.AuditTrailOperationRepository; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import java.time.LocalDateTime; import java.util.UUID; import org.jboss.logging.Logger; /** * Service d'audit trail enrichi (SYSCOHADA + AUDSCGIE OHADA + Instruction BCEAO 003-03-2025). * *

Enregistre toutes les opérations sensibles (financières, lifecycle membres, configurations) * avec leur contexte multi-org complet (rôle actif, organisation active, vérifications SoD). * *

Usage typique dans un service métier : * *

{@code
 *   @Inject AuditTrailService auditTrail;
 *
 *   public Cotisation enregistrerPaiement(...) {
 *       Cotisation c = ...
 *       auditTrail.log("Cotisation", c.getId(), "PAYMENT_CONFIRMED",
 *                      "Paiement confirmé via " + provider, c);
 *       return c;
 *   }
 * }
* * @since 2026-04-25 */ @ApplicationScoped public class AuditTrailService { private static final Logger LOG = Logger.getLogger(AuditTrailService.class); @Inject AuditTrailOperationRepository repository; @Inject OrganisationContextHolder context; @Inject io.micrometer.core.instrument.MeterRegistry registry; @Inject jakarta.enterprise.event.Event auditEvent; private final ObjectMapper objectMapper = new ObjectMapper(); /** * Enregistre une entrée d'audit trail à partir du contexte courant. * * @param entityType nom de l'entité (ex: "Cotisation", "Membre", "EcritureComptable") * @param entityId UUID de l'entité ciblée (peut être null pour actions globales) * @param actionType type d'action (cf. CHECK SQL : CREATE, UPDATE, DELETE, APPROVE, ...) * @param description description libre courte (≤ 500 caractères) * @param payloadApres entité après modification (sérialisée JSON, peut être null) */ @Transactional public void log(String entityType, UUID entityId, String actionType, String description, Object payloadApres) { log(entityType, entityId, actionType, description, null, payloadApres, null, null, null); } /** * Enregistre une entrée d'audit trail avec snapshot avant/après et résultat SoD. */ @Transactional public void log(String entityType, UUID entityId, String actionType, String description, Object payloadAvant, Object payloadApres, Object metadata, Boolean sodCheckPassed, String sodViolations) { try { AuditTrailOperation entry = AuditTrailOperation.builder() .userId(context.getCurrentUserId()) .userEmail(context.getCurrentUserEmail()) .roleActif(context.getRoleActif()) .organisationActiveId(context.getOrganisationId()) .actionType(actionType) .entityType(entityType) .entityId(entityId) .description(description) .payloadAvant(toJson(payloadAvant)) .payloadApres(toJson(payloadApres)) .metadata(toJson(metadata)) .sodCheckPassed(sodCheckPassed) .sodViolations(sodViolations) .operationAt(LocalDateTime.now()) .build(); repository.persist(entry); // Sprint 16.A — Métriques Prometheus métier (transparency) registry.counter("unionflow.audit.operations", "action", actionType == null ? "UNKNOWN" : actionType, "entity", entityType == null ? "UNKNOWN" : entityType).increment(); if (Boolean.FALSE.equals(sodCheckPassed)) { registry.counter("unionflow.audit.sod_violations", "entity", entityType == null ? "UNKNOWN" : entityType).increment(); } // Sprint 16.C — Fire CDI event pour observers (notifications, etc.) auditEvent.fire(new AuditOperationLoggedEvent( entry.getId(), entry.getUserId(), entry.getUserEmail(), entry.getOrganisationActiveId(), actionType, entityType, entityId, description, sodCheckPassed, entry.getOperationAt())); } catch (Exception e) { // Fail-soft : l'audit trail ne doit jamais bloquer une opération métier. // Les violations sont loguées et peuvent être détectées via les logs applicatifs. LOG.errorf(e, "Audit trail log failed: entityType=%s entityId=%s actionType=%s description=%s", entityType, entityId, actionType, description); } } /** Variante sans payload — pour les actions simples (LOGIN, LOGOUT, EXPORT...). */ @Transactional public void logSimple(String entityType, UUID entityId, String actionType, String description) { log(entityType, entityId, actionType, description, null); } private String toJson(Object o) { if (o == null) return null; try { return objectMapper.writeValueAsString(o); } catch (Exception e) { LOG.warnf("Audit trail JSON serialization failed for %s : %s", o.getClass().getSimpleName(), e.getMessage()); return null; } } }