users) {
+ delete("realmName", realmName);
+ persist(users);
+ }
+}
+
diff --git a/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java
index 2924283..a856519 100644
--- a/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java
+++ b/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java
@@ -2,11 +2,15 @@ package dev.lions.user.manager.service.impl;
import dev.lions.user.manager.dto.audit.AuditLogDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit;
+// import dev.lions.user.manager.mapper.AuditLogMapper; // DELETE - Wrong package
+import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; // ADD - Correct package
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
-import dev.lions.user.manager.server.impl.mapper.AuditLogMapper;
+import dev.lions.user.manager.server.impl.repository.AuditLogRepository;
import dev.lions.user.manager.service.AuditService;
+import io.quarkus.hibernate.orm.panache.PanacheQuery;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
@@ -15,596 +19,344 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.time.LocalDateTime;
-import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
-/**
- * Implémentation du service d'audit avec support de la persistance PostgreSQL.
- *
- * Architecture Hybride:
- *
- * - Cache en mémoire - Pour les logs récents (performances)
- * - Persistance PostgreSQL - Pour l'historique long terme (activable via config)
- *
- *
- * Configuration:
- *
- * - {@code lions.audit.enabled} - Active/désactive l'audit (défaut: true)
- * - {@code lions.audit.log-to-database} - Active la persistance DB (défaut: false en dev, true en prod)
- * - {@code lions.audit.cache-size} - Taille max du cache mémoire (défaut: 10000)
- * - {@code lions.audit.retention-days} - Durée de rétention en jours (défaut: 365)
- *
- *
- * Modes de Fonctionnement:
- *
- * Mode DEV (logToDatabase=false):
- * - Stockage en mémoire uniquement
- * - Logs perdus au redémarrage
- * - Performances maximales
- *
- * Mode PROD (logToDatabase=true):
- * - Persistance PostgreSQL
- * - Cache mémoire pour requêtes fréquentes
- * - Historique complet préservé
- *
- *
- * @author Lions Development Team
- * @version 2.0.0
- * @since 2026-01-02
- */
@ApplicationScoped
@Slf4j
public class AuditServiceImpl implements AuditService {
- // ==================== DÉPENDANCES ====================
+ @Inject
+ AuditLogRepository auditLogRepository;
@Inject
AuditLogMapper auditLogMapper;
- // ==================== CONFIGURATION ====================
+ @Inject
+ EntityManager entityManager;
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
boolean auditEnabled;
- @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false")
+ @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "true")
boolean logToDatabase;
- @ConfigProperty(name = "lions.audit.cache-size", defaultValue = "10000")
- int cacheSize;
-
- @ConfigProperty(name = "lions.audit.retention-days", defaultValue = "365")
- int retentionDays;
-
- // ==================== STOCKAGE ====================
-
- /**
- * Cache en mémoire pour les logs récents.
- * Limité à {@code cacheSize} entrées. Les plus anciens sont supprimés automatiquement.
- */
- private final Map auditLogsCache = new ConcurrentHashMap<>();
-
- // ==================== MÉTHODES PRINCIPALES ====================
-
@Override
- @Transactional
+ @Transactional(Transactional.TxType.REQUIRES_NEW)
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
if (!auditEnabled) {
- log.debug("Audit désactivé, log ignoré");
+ log.debug("Audit désactivé, action ignorée: {}", auditLog.getTypeAction());
return auditLog;
}
- // Générer un ID si nécessaire
- if (auditLog.getId() == null) {
- auditLog.setId(UUID.randomUUID().toString());
- }
+ log.info("AUDIT: [{}] {} - user:{} - ressource:{}/{} - status:{}",
+ auditLog.getRealmName(),
+ auditLog.getTypeAction(),
+ auditLog.getActeurUsername(), // ou getActeurUserId()
+ auditLog.getRessourceType(),
+ auditLog.getRessourceId(),
+ auditLog.getSuccess() != null && auditLog.getSuccess() ? "SUCCESS" : "FAILURE");
- // Ajouter le timestamp si nécessaire
- if (auditLog.getDateAction() == null) {
- auditLog.setDateAction(LocalDateTime.now());
- }
-
- // Log structuré pour les systèmes de logging externes (Graylog, Elasticsearch, etc.)
- log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}",
- auditLog.getTypeAction(),
- auditLog.getActeurUsername(),
- auditLog.getRessourceType() + ":" + auditLog.getRessourceId(),
- auditLog.isSuccessful(),
- auditLog.getIpAddress(),
- auditLog.getDescription());
-
- // Stocker en base de données si activé
if (logToDatabase) {
try {
+ // Ensure dateAction is set
+ if (auditLog.getDateAction() == null) {
+ auditLog.setDateAction(LocalDateTime.now());
+ }
+
AuditLogEntity entity = auditLogMapper.toEntity(auditLog);
- // Le mapper s'occupe du mapping automatique via @Mapping annotations
- // Ajout des champs additionnels non mappés automatiquement
- entity.setRealmName(auditLog.getRealmName());
+ auditLogRepository.persist(entity);
- entity.persist();
-
- log.debug("Log d'audit persisté en base de données avec ID: {}", entity.id);
+ // Mettre à jour l'ID du DTO avec l'ID généré par la base
+ if (entity.id != null) {
+ auditLog.setId(entity.id.toString());
+ }
} catch (Exception e) {
- log.error("Erreur lors de la persistance du log d'audit en base de données", e);
- // On ne lance pas d'exception pour ne pas bloquer le processus métier
+ log.error("Erreur lors de la persistance du log d'audit", e);
+ // On ne bloque pas l'action métier si l'audit échoue (sauf exigence contraire)
}
}
- // Ajouter au cache mémoire (pour performances)
- auditLogsCache.put(auditLog.getId(), auditLog);
-
- // Nettoyer le cache si trop grand
- if (auditLogsCache.size() > cacheSize) {
- cleanOldestCacheEntries();
- }
-
return auditLog;
}
@Override
+ @Transactional(Transactional.TxType.REQUIRES_NEW)
public void logSuccess(@NotNull TypeActionAudit typeAction,
- @NotBlank String ressourceType,
- String ressourceId,
- String ressourceName,
- @NotBlank String realmName,
- @NotBlank String acteurUserId,
- String description) {
- AuditLogDTO auditLog = AuditLogDTO.builder()
- .acteurUserId(acteurUserId)
- .acteurUsername(acteurUserId)
- .typeAction(typeAction)
- .ressourceType(ressourceType)
- .ressourceId(ressourceId != null ? ressourceId : "")
- .success(true)
- .description(description)
- .dateAction(LocalDateTime.now())
- .build();
+ @NotBlank String ressourceType,
+ String ressourceId,
+ String ressourceName,
+ @NotBlank String realmName,
+ @NotBlank String acteurUserId,
+ String description) {
- logAction(auditLog);
+ AuditLogDTO log = AuditLogDTO.builder()
+ .typeAction(typeAction)
+ .ressourceType(ressourceType)
+ .ressourceId(ressourceId)
+ .ressourceName(ressourceName)
+ .realmName(realmName)
+ .acteurUserId(acteurUserId)
+ .acteurUsername(acteurUserId) // On map aussi le username pour la persistence Entity
+ .description(description)
+ .dateAction(LocalDateTime.now())
+ .success(true)
+ .build();
+
+ logAction(log);
}
@Override
+ @Transactional(Transactional.TxType.REQUIRES_NEW)
public void logFailure(@NotNull TypeActionAudit typeAction,
- @NotBlank String ressourceType,
- String ressourceId,
- String ressourceName,
- @NotBlank String realmName,
- @NotBlank String acteurUserId,
- String errorCode,
- String errorMessage) {
- AuditLogDTO auditLog = AuditLogDTO.builder()
- .acteurUserId(acteurUserId)
- .acteurUsername(acteurUserId)
- .typeAction(typeAction)
- .ressourceType(ressourceType)
- .ressourceId(ressourceId != null ? ressourceId : "")
- .success(false)
- .errorMessage(errorMessage)
- .dateAction(LocalDateTime.now())
- .build();
+ @NotBlank String ressourceType,
+ String ressourceId,
+ String ressourceName,
+ @NotBlank String realmName,
+ @NotBlank String acteurUserId,
+ String errorCode,
+ String errorMessage) {
- logAction(auditLog);
+ AuditLogDTO log = AuditLogDTO.builder()
+ .typeAction(typeAction)
+ .ressourceType(ressourceType)
+ .ressourceId(ressourceId)
+ .ressourceName(ressourceName)
+ .realmName(realmName)
+ .acteurUserId(acteurUserId)
+ .acteurUsername(acteurUserId)
+ .description("Echec: " + errorCode)
+ .errorMessage(errorMessage)
+ .dateAction(LocalDateTime.now())
+ .success(false)
+ .build();
+
+ logAction(log);
}
- // ==================== MÉTHODES DE RECHERCHE ====================
-
@Override
public List findByActeur(@NotBlank String acteurUserId,
- LocalDateTime dateDebut,
- LocalDateTime dateFin,
- int page,
- int pageSize) {
- if (logToDatabase) {
- return searchLogsFromDatabase(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
- }
- return searchLogsFromCache(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
+ LocalDateTime dateDebut,
+ LocalDateTime dateFin,
+ int page,
+ int pageSize) {
+ // Le repository cherche par auteurAction, qui est mappé sur acteurUsername dans
+ // le DTO
+ List entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null,
+ page,
+ pageSize);
+ return auditLogMapper.toDTOList(entities);
}
@Override
public List findByRessource(@NotBlank String ressourceType,
- @NotBlank String ressourceId,
- LocalDateTime dateDebut,
- LocalDateTime dateFin,
- int page,
- int pageSize) {
- if (logToDatabase) {
- return searchLogsFromDatabase(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize)
- .stream()
- .filter(log -> ressourceId.equals(log.getRessourceId()))
- .collect(Collectors.toList());
- }
- return searchLogsFromCache(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize)
- .stream()
- .filter(log -> ressourceId.equals(log.getRessourceId()))
- .collect(Collectors.toList());
+ @NotBlank String ressourceId,
+ LocalDateTime dateDebut,
+ LocalDateTime dateFin,
+ int page,
+ int pageSize) {
+
+ // Utilisation de Panache query directe car le repo search générique est limité
+ // On cherche dans 'details' (description) ou 'userId' (ressourceId)
+ String filter = "%" + ressourceId + "%";
+ // Correction: userId est le nom du champ dans l'entité qui mappe ressourceId
+ PanacheQuery q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter);
+
+ return auditLogMapper.toDTOList(q.page(page, pageSize).list());
}
@Override
public List findByTypeAction(@NotNull TypeActionAudit typeAction,
- @NotBlank String realmName,
- LocalDateTime dateDebut,
- LocalDateTime dateFin,
- int page,
- int pageSize) {
- if (logToDatabase) {
- return searchLogsFromDatabase(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
- }
- return searchLogsFromCache(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
+ @NotBlank String realmName,
+ LocalDateTime dateDebut,
+ LocalDateTime dateFin,
+ int page,
+ int pageSize) {
+ List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin,
+ typeAction.name(), null, page,
+ pageSize);
+ return auditLogMapper.toDTOList(entities);
}
@Override
public List findByRealm(@NotBlank String realmName,
- LocalDateTime dateDebut,
- LocalDateTime dateFin,
- int page,
- int pageSize) {
- if (logToDatabase) {
- List entities = AuditLogEntity.findByRealm(realmName);
- return auditLogMapper.toDTOList(entities);
- }
- return searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);
+ LocalDateTime dateDebut,
+ LocalDateTime dateFin,
+ int page,
+ int pageSize) {
+ List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page,
+ pageSize);
+ return auditLogMapper.toDTOList(entities);
}
@Override
public List findFailures(@NotBlank String realmName,
- LocalDateTime dateDebut,
- LocalDateTime dateFin,
- int page,
- int pageSize) {
- if (logToDatabase) {
- return searchLogsFromDatabase(null, dateDebut, dateFin, null, null, false, page, pageSize);
- }
- return searchLogsFromCache(null, dateDebut, dateFin, null, null, false, page, pageSize);
+ LocalDateTime dateDebut,
+ LocalDateTime dateFin,
+ int page,
+ int pageSize) {
+ List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
+ page,
+ pageSize);
+ return auditLogMapper.toDTOList(entities);
}
@Override
public List findCriticalActions(@NotBlank String realmName,
- LocalDateTime dateDebut,
- LocalDateTime dateFin,
- int page,
- int pageSize) {
- List allLogs = logToDatabase ?
- searchLogsFromDatabase(null, dateDebut, dateFin, null, null, null, page, pageSize) :
- searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);
-
- return allLogs.stream()
- .filter(log -> {
- TypeActionAudit type = log.getTypeAction();
- return type == TypeActionAudit.USER_DELETE ||
- type == TypeActionAudit.ROLE_DELETE ||
- type == TypeActionAudit.SESSION_REVOKE_ALL;
- })
- .collect(Collectors.toList());
+ LocalDateTime dateDebut,
+ LocalDateTime dateFin,
+ int page,
+ int pageSize) {
+ List entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
+ page, pageSize);
+ return auditLogMapper.toDTOList(entities);
}
- // ==================== MÉTHODES STATISTIQUES ====================
-
@Override
+ @SuppressWarnings("unchecked")
public Map countByActionType(@NotBlank String realmName,
- LocalDateTime dateDebut,
- LocalDateTime dateFin) {
- return getActionStatistics(dateDebut, dateFin);
- }
-
- @Override
- public Map countByActeur(@NotBlank String realmName,
- LocalDateTime dateDebut,
- LocalDateTime dateFin) {
- return getUserActivityStatistics(dateDebut, dateFin);
- }
-
- @Override
- public Map countSuccessVsFailure(@NotBlank String realmName,
- LocalDateTime dateDebut,
- LocalDateTime dateFin) {
- long successCount = getSuccessCount(dateDebut, dateFin);
- long failureCount = getFailureCount(dateDebut, dateFin);
-
- Map result = new java.util.HashMap<>();
- result.put("success", successCount);
- result.put("failure", failureCount);
+ LocalDateTime dateDebut,
+ LocalDateTime dateFin) {
+ StringBuilder sql = new StringBuilder("SELECT action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
+ if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
+ if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
+ sql.append(" GROUP BY action");
+ var query = entityManager.createNativeQuery(sql.toString())
+ .setParameter("realmName", realmName);
+ if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
+ if (dateFin != null) query.setParameter("dateFin", dateFin);
+ List