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;
}
}
}