feat: Optimisations UX/UI et amélioration import/export CSV
Optimisations majeures de l'interface utilisateur et amélioration du système d'import/export CSV avec rapport d'erreurs détaillé. ## Optimisations UX/UI - Suppression des blocs Actions Rapides redondants dans les pages list/view - Consolidation des actions dans les en-têtes de page - Conversion des filtres en panneau collapsible avec badge Filtres actifs - Suppression du sous-menu Attribution Rôles (redondant avec /users/edit) - Amélioration de la navigation et de l'ergonomie générale - Correction des attributs iconLeft non supportés par fr:fieldInput ## Import/Export CSV - Ajout de ImportResultDTO avec rapport détaillé des erreurs - Création de CsvValidationHelper pour validation robuste des données - Amélioration des messages d'erreur avec numéros de ligne - Support de colonnes flexibles (username,prenom,nom,email) - Validation stricte des formats email ## Corrections techniques - Fix DashboardBeanTest: getRecentActions() → getActionsLast24h() - Fix UserServiceImplTest: retour ImportResultDTO au lieu de int - Amélioration de la gestion d'erreurs dans AuditServiceImpl - Migration Flyway V1.0.0 pour la table audit_logs ## Infrastructure - Mise à jour .gitignore professionnel (exclusion docs de session) - Configuration production sécurisée (variables d'environnement) - Pas de secrets hardcodés dans les fichiers de configuration Testé et validé en environnement de développement.
This commit is contained in:
@@ -22,6 +22,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
|||||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
||||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -407,6 +408,86 @@ public class UserResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporter les utilisateurs en CSV
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/export/csv")
|
||||||
|
@Operation(summary = "Exporter les utilisateurs en CSV")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Fichier CSV généré avec succès"),
|
||||||
|
@APIResponse(responseCode = "400", description = "Realm manquant ou invalide"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "user_manager", "user_viewer"})
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
public Response exportUsersToCSV(@QueryParam("realm") @NotBlank String realmName) {
|
||||||
|
log.info("GET /api/users/export/csv - realm: {}", realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||||
|
.realmName(realmName)
|
||||||
|
.pageSize(10000) // Export complet sans pagination
|
||||||
|
.page(0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String csvContent = userService.exportUsersToCSV(criteria);
|
||||||
|
|
||||||
|
String filename = "users_export_" +
|
||||||
|
LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss")) +
|
||||||
|
".csv";
|
||||||
|
|
||||||
|
return Response.ok(csvContent)
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
|
||||||
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'export CSV des utilisateurs", e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importer des utilisateurs depuis CSV avec rapport détaillé
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/import/csv")
|
||||||
|
@Operation(summary = "Importer des utilisateurs depuis un fichier CSV")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Import terminé avec rapport détaillé"),
|
||||||
|
@APIResponse(responseCode = "400", description = "Fichier CSV vide ou invalide"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "user_manager"})
|
||||||
|
@Consumes(MediaType.TEXT_PLAIN)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response importUsersFromCSV(
|
||||||
|
@QueryParam("realm") @NotBlank String realmName,
|
||||||
|
String csvContent) {
|
||||||
|
log.info("POST /api/users/import/csv - realm: {}", realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (csvContent == null || csvContent.trim().isEmpty()) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(new ErrorResponse("Le contenu CSV est vide"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
dev.lions.user.manager.dto.importexport.ImportResultDTO result = userService.importUsersFromCSV(csvContent, realmName);
|
||||||
|
|
||||||
|
log.info("{} utilisateur(s) importé(s) dans le realm {} ({} erreur(s))",
|
||||||
|
result.getSuccessCount(), realmName, result.getErrorCount());
|
||||||
|
|
||||||
|
return Response.ok(result).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'import CSV des utilisateurs", e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== DTOs internes ====================
|
// ==================== DTOs internes ====================
|
||||||
|
|
||||||
@Schema(description = "Requête de réinitialisation de mot de passe")
|
@Schema(description = "Requête de réinitialisation de mot de passe")
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package dev.lions.user.manager.service.impl;
|
|||||||
|
|
||||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||||
|
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||||
|
import dev.lions.user.manager.server.impl.mapper.AuditLogMapper;
|
||||||
import dev.lions.user.manager.service.AuditService;
|
import dev.lions.user.manager.service.AuditService;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
@@ -19,19 +23,49 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implémentation du service d'audit
|
* Implémentation du service d'audit avec support de la persistance PostgreSQL.
|
||||||
*
|
*
|
||||||
* NOTES:
|
* <p><b>Architecture Hybride:</b></p>
|
||||||
* - Cette implémentation utilise un stockage en mémoire pour le développement
|
* <ul>
|
||||||
* - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache)
|
* <li><b>Cache en mémoire</b> - Pour les logs récents (performances)</li>
|
||||||
* - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés
|
* <li><b>Persistance PostgreSQL</b> - Pour l'historique long terme (activable via config)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>Configuration:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code lions.audit.enabled} - Active/désactive l'audit (défaut: true)</li>
|
||||||
|
* <li>{@code lions.audit.log-to-database} - Active la persistance DB (défaut: false en dev, true en prod)</li>
|
||||||
|
* <li>{@code lions.audit.cache-size} - Taille max du cache mémoire (défaut: 10000)</li>
|
||||||
|
* <li>{@code lions.audit.retention-days} - Durée de rétention en jours (défaut: 365)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>Modes de Fonctionnement:</b></p>
|
||||||
|
* <pre>
|
||||||
|
* 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é
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author Lions Development Team
|
||||||
|
* @version 2.0.0
|
||||||
|
* @since 2026-01-02
|
||||||
*/
|
*/
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AuditServiceImpl implements AuditService {
|
public class AuditServiceImpl implements AuditService {
|
||||||
|
|
||||||
// Stockage en mémoire (à remplacer par une DB en production)
|
// ==================== DÉPENDANCES ====================
|
||||||
private final Map<String, AuditLogDTO> auditLogs = new ConcurrentHashMap<>();
|
|
||||||
|
@Inject
|
||||||
|
AuditLogMapper auditLogMapper;
|
||||||
|
|
||||||
|
// ==================== CONFIGURATION ====================
|
||||||
|
|
||||||
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
|
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
|
||||||
boolean auditEnabled;
|
boolean auditEnabled;
|
||||||
@@ -39,7 +73,24 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false")
|
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false")
|
||||||
boolean logToDatabase;
|
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.
|
||||||
|
* <p>Limité à {@code cacheSize} entrées. Les plus anciens sont supprimés automatiquement.</p>
|
||||||
|
*/
|
||||||
|
private final Map<String, AuditLogDTO> auditLogsCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// ==================== MÉTHODES PRINCIPALES ====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
|
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
|
||||||
if (!auditEnabled) {
|
if (!auditEnabled) {
|
||||||
log.debug("Audit désactivé, log ignoré");
|
log.debug("Audit désactivé, log ignoré");
|
||||||
@@ -56,7 +107,7 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
auditLog.setDateAction(LocalDateTime.now());
|
auditLog.setDateAction(LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log structuré pour les systèmes de logging (Graylog, Elasticsearch, etc.)
|
// Log structuré pour les systèmes de logging externes (Graylog, Elasticsearch, etc.)
|
||||||
log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}",
|
log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}",
|
||||||
auditLog.getTypeAction(),
|
auditLog.getTypeAction(),
|
||||||
auditLog.getActeurUsername(),
|
auditLog.getActeurUsername(),
|
||||||
@@ -65,15 +116,30 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
auditLog.getIpAddress(),
|
auditLog.getIpAddress(),
|
||||||
auditLog.getDescription());
|
auditLog.getDescription());
|
||||||
|
|
||||||
// Stocker en mémoire
|
// Stocker en base de données si activé
|
||||||
auditLogs.put(auditLog.getId(), auditLog);
|
if (logToDatabase) {
|
||||||
|
try {
|
||||||
|
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());
|
||||||
|
|
||||||
// TODO: Si logToDatabase = true, persister dans PostgreSQL via Panache
|
entity.persist();
|
||||||
// Exemple:
|
|
||||||
// if (logToDatabase) {
|
log.debug("Log d'audit persisté en base de données avec ID: {}", entity.id);
|
||||||
// AuditLogEntity entity = AuditLogMapper.toEntity(auditLog);
|
} catch (Exception e) {
|
||||||
// entity.persist();
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
return auditLog;
|
||||||
}
|
}
|
||||||
@@ -88,7 +154,7 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
String description) {
|
String description) {
|
||||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||||
.acteurUserId(acteurUserId)
|
.acteurUserId(acteurUserId)
|
||||||
.acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant
|
.acteurUsername(acteurUserId)
|
||||||
.typeAction(typeAction)
|
.typeAction(typeAction)
|
||||||
.ressourceType(ressourceType)
|
.ressourceType(ressourceType)
|
||||||
.ressourceId(ressourceId != null ? ressourceId : "")
|
.ressourceId(ressourceId != null ? ressourceId : "")
|
||||||
@@ -111,7 +177,7 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
String errorMessage) {
|
String errorMessage) {
|
||||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||||
.acteurUserId(acteurUserId)
|
.acteurUserId(acteurUserId)
|
||||||
.acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant
|
.acteurUsername(acteurUserId)
|
||||||
.typeAction(typeAction)
|
.typeAction(typeAction)
|
||||||
.ressourceType(ressourceType)
|
.ressourceType(ressourceType)
|
||||||
.ressourceId(ressourceId != null ? ressourceId : "")
|
.ressourceId(ressourceId != null ? ressourceId : "")
|
||||||
@@ -123,13 +189,18 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
logAction(auditLog);
|
logAction(auditLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES DE RECHERCHE ====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId,
|
public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId,
|
||||||
LocalDateTime dateDebut,
|
LocalDateTime dateDebut,
|
||||||
LocalDateTime dateFin,
|
LocalDateTime dateFin,
|
||||||
int page,
|
int page,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
return searchLogs(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
|
if (logToDatabase) {
|
||||||
|
return searchLogsFromDatabase(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
|
||||||
|
}
|
||||||
|
return searchLogsFromCache(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -139,7 +210,13 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
LocalDateTime dateFin,
|
LocalDateTime dateFin,
|
||||||
int page,
|
int page,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
return searchLogs(null, dateDebut, dateFin, null, ressourceType, null, page, 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()
|
.stream()
|
||||||
.filter(log -> ressourceId.equals(log.getRessourceId()))
|
.filter(log -> ressourceId.equals(log.getRessourceId()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@@ -152,7 +229,10 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
LocalDateTime dateFin,
|
LocalDateTime dateFin,
|
||||||
int page,
|
int page,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
return searchLogs(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
|
if (logToDatabase) {
|
||||||
|
return searchLogsFromDatabase(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
|
||||||
|
}
|
||||||
|
return searchLogsFromCache(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -161,8 +241,11 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
LocalDateTime dateFin,
|
LocalDateTime dateFin,
|
||||||
int page,
|
int page,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
// Pour l'instant, on retourne tous les logs car on n'a pas de champ realmName dans AuditLogDTO
|
if (logToDatabase) {
|
||||||
return searchLogs(null, dateDebut, dateFin, null, null, null, page, pageSize);
|
List<AuditLogEntity> entities = AuditLogEntity.findByRealm(realmName);
|
||||||
|
return auditLogMapper.toDTOList(entities);
|
||||||
|
}
|
||||||
|
return searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -171,7 +254,10 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
LocalDateTime dateFin,
|
LocalDateTime dateFin,
|
||||||
int page,
|
int page,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
return searchLogs(null, dateDebut, dateFin, null, null, false, page, pageSize);
|
if (logToDatabase) {
|
||||||
|
return searchLogsFromDatabase(null, dateDebut, dateFin, null, null, false, page, pageSize);
|
||||||
|
}
|
||||||
|
return searchLogsFromCache(null, dateDebut, dateFin, null, null, false, page, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -180,29 +266,22 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
LocalDateTime dateFin,
|
LocalDateTime dateFin,
|
||||||
int page,
|
int page,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
// Les actions critiques sont USER_DELETE, ROLE_DELETE, etc.
|
List<AuditLogDTO> allLogs = logToDatabase ?
|
||||||
return auditLogs.values().stream()
|
searchLogsFromDatabase(null, dateDebut, dateFin, null, null, null, page, pageSize) :
|
||||||
|
searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);
|
||||||
|
|
||||||
|
return allLogs.stream()
|
||||||
.filter(log -> {
|
.filter(log -> {
|
||||||
TypeActionAudit type = log.getTypeAction();
|
TypeActionAudit type = log.getTypeAction();
|
||||||
return type == TypeActionAudit.USER_DELETE ||
|
return type == TypeActionAudit.USER_DELETE ||
|
||||||
type == TypeActionAudit.ROLE_DELETE ||
|
type == TypeActionAudit.ROLE_DELETE ||
|
||||||
type == TypeActionAudit.SESSION_REVOKE_ALL;
|
type == TypeActionAudit.SESSION_REVOKE_ALL;
|
||||||
})
|
})
|
||||||
.filter(log -> {
|
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
|
|
||||||
.skip((long) page * pageSize)
|
|
||||||
.limit(pageSize)
|
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== MÉTHODES STATISTIQUES ====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName,
|
public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName,
|
||||||
LocalDateTime dateDebut,
|
LocalDateTime dateDebut,
|
||||||
@@ -223,13 +302,43 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
LocalDateTime dateFin) {
|
LocalDateTime dateFin) {
|
||||||
long successCount = getSuccessCount(dateDebut, dateFin);
|
long successCount = getSuccessCount(dateDebut, dateFin);
|
||||||
long failureCount = getFailureCount(dateDebut, dateFin);
|
long failureCount = getFailureCount(dateDebut, dateFin);
|
||||||
|
|
||||||
Map<String, Long> result = new java.util.HashMap<>();
|
Map<String, Long> result = new java.util.HashMap<>();
|
||||||
result.put("success", successCount);
|
result.put("success", successCount);
|
||||||
result.put("failure", failureCount);
|
result.put("failure", failureCount);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
|
||||||
|
LocalDateTime dateDebut,
|
||||||
|
LocalDateTime dateFin) {
|
||||||
|
Map<String, Object> stats = new java.util.HashMap<>();
|
||||||
|
|
||||||
|
long total = logToDatabase ?
|
||||||
|
AuditLogEntity.findByPeriod(dateDebut, dateFin).size() :
|
||||||
|
auditLogsCache.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
|
||||||
|
stats.put("total", total);
|
||||||
|
stats.put("success", getSuccessCount(dateDebut, dateFin));
|
||||||
|
stats.put("failure", getFailureCount(dateDebut, dateFin));
|
||||||
|
stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin));
|
||||||
|
stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin));
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== EXPORT / PURGE ====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String exportToCSV(@NotBlank String realmName,
|
public String exportToCSV(@NotBlank String realmName,
|
||||||
LocalDateTime dateDebut,
|
LocalDateTime dateDebut,
|
||||||
@@ -239,159 +348,192 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
|
public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
|
||||||
long beforeCount = auditLogs.size();
|
long purgedCount = 0;
|
||||||
auditLogs.entrySet().removeIf(entry ->
|
|
||||||
|
// Purge en base de données si activé
|
||||||
|
if (logToDatabase) {
|
||||||
|
purgedCount = AuditLogEntity.deleteOlderThan(dateLimite);
|
||||||
|
log.info("Supprimé {} logs d'audit de la base de données avant {}", purgedCount, dateLimite);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge du cache mémoire
|
||||||
|
long beforeCacheCount = auditLogsCache.size();
|
||||||
|
auditLogsCache.entrySet().removeIf(entry ->
|
||||||
entry.getValue().getDateAction().isBefore(dateLimite)
|
entry.getValue().getDateAction().isBefore(dateLimite)
|
||||||
);
|
);
|
||||||
long afterCount = auditLogs.size();
|
long cacheRemoved = beforeCacheCount - auditLogsCache.size();
|
||||||
return beforeCount - afterCount;
|
|
||||||
|
log.info("Supprimé {} logs du cache mémoire avant {}", cacheRemoved, dateLimite);
|
||||||
|
|
||||||
|
return purgedCount + cacheRemoved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// ==================== MÉTHODES PRIVÉES ====================
|
||||||
public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
|
|
||||||
LocalDateTime dateDebut,
|
|
||||||
LocalDateTime dateFin) {
|
|
||||||
Map<String, Object> stats = new java.util.HashMap<>();
|
|
||||||
stats.put("total", auditLogs.values().stream()
|
|
||||||
.filter(log -> {
|
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.count());
|
|
||||||
stats.put("success", getSuccessCount(dateDebut, dateFin));
|
|
||||||
stats.put("failure", getFailureCount(dateDebut, dateFin));
|
|
||||||
stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin));
|
|
||||||
stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin));
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode privée helper pour la recherche
|
/**
|
||||||
private List<AuditLogDTO> searchLogs(String acteurUsername, LocalDateTime dateDebut,
|
* Recherche les logs depuis le cache mémoire.
|
||||||
LocalDateTime dateFin, TypeActionAudit typeAction,
|
*/
|
||||||
String ressourceType, Boolean succes,
|
private List<AuditLogDTO> searchLogsFromCache(String acteurUsername, LocalDateTime dateDebut,
|
||||||
int page, int pageSize) {
|
LocalDateTime dateFin, TypeActionAudit typeAction,
|
||||||
log.debug("Recherche de logs d'audit: acteur={}, dateDebut={}, dateFin={}, typeAction={}, succes={}",
|
String ressourceType, Boolean succes,
|
||||||
acteurUsername, dateDebut, dateFin, typeAction, succes);
|
int page, int pageSize) {
|
||||||
|
log.debug("Recherche logs depuis cache mémoire");
|
||||||
|
|
||||||
return auditLogs.values().stream()
|
return auditLogsCache.values().stream()
|
||||||
.filter(log -> {
|
.filter(log -> applyFilters(log, acteurUsername, dateDebut, dateFin, typeAction, ressourceType, succes))
|
||||||
// Filtre par acteur (si spécifié et non "*")
|
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
|
||||||
if (acteurUsername != null && !"*".equals(acteurUsername) &&
|
|
||||||
!acteurUsername.equals(log.getActeurUsername())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par date début
|
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par date fin
|
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par type d'action
|
|
||||||
if (typeAction != null && !typeAction.equals(log.getTypeAction())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par type de ressource
|
|
||||||
if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtre par succès/échec
|
|
||||||
if (succes != null && succes != log.isSuccessful()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) // Tri décroissant par date
|
|
||||||
.skip((long) page * pageSize)
|
.skip((long) page * pageSize)
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthodes privées helper
|
/**
|
||||||
private Map<TypeActionAudit, Long> getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
* Recherche les logs depuis la base de données PostgreSQL.
|
||||||
log.debug("Calcul des statistiques d'actions entre {} et {}", dateDebut, dateFin);
|
*/
|
||||||
|
private List<AuditLogDTO> searchLogsFromDatabase(String acteurUsername, LocalDateTime dateDebut,
|
||||||
|
LocalDateTime dateFin, TypeActionAudit typeAction,
|
||||||
|
String ressourceType, Boolean succes,
|
||||||
|
int page, int pageSize) {
|
||||||
|
log.debug("Recherche logs depuis base de données");
|
||||||
|
|
||||||
return auditLogs.values().stream()
|
List<AuditLogEntity> entities;
|
||||||
|
|
||||||
|
// Optimisation: utiliser les requêtes spécialisées si possible
|
||||||
|
if (acteurUsername != null && typeAction == null && ressourceType == null) {
|
||||||
|
entities = AuditLogEntity.findByAuteur(acteurUsername);
|
||||||
|
} else if (typeAction != null && acteurUsername == null && ressourceType == null) {
|
||||||
|
entities = AuditLogEntity.findByAction(typeAction);
|
||||||
|
} else if (dateDebut != null && dateFin != null) {
|
||||||
|
entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
|
||||||
|
} else {
|
||||||
|
entities = AuditLogEntity.listAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities.stream()
|
||||||
|
.map(auditLogMapper::toDTO)
|
||||||
|
.filter(log -> applyFilters(log, acteurUsername, dateDebut, dateFin, typeAction, ressourceType, succes))
|
||||||
|
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
|
||||||
|
.skip((long) page * pageSize)
|
||||||
|
.limit(pageSize)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique les filtres de recherche à un log.
|
||||||
|
*/
|
||||||
|
private boolean applyFilters(AuditLogDTO log, String acteurUsername, LocalDateTime dateDebut,
|
||||||
|
LocalDateTime dateFin, TypeActionAudit typeAction,
|
||||||
|
String ressourceType, Boolean succes) {
|
||||||
|
if (acteurUsername != null && !"*".equals(acteurUsername) &&
|
||||||
|
!acteurUsername.equals(log.getActeurUsername())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeAction != null && !typeAction.equals(log.getTypeAction())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (succes != null && succes != log.isSuccessful()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les entrées les plus anciennes du cache.
|
||||||
|
*/
|
||||||
|
private void cleanOldestCacheEntries() {
|
||||||
|
int toRemove = auditLogsCache.size() - (cacheSize * 90 / 100); // Garder 90%
|
||||||
|
|
||||||
|
if (toRemove > 0) {
|
||||||
|
List<String> oldestKeys = auditLogsCache.entrySet().stream()
|
||||||
|
.sorted((a, b) -> a.getValue().getDateAction().compareTo(b.getValue().getDateAction()))
|
||||||
|
.limit(toRemove)
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
oldestKeys.forEach(auditLogsCache::remove);
|
||||||
|
log.debug("Nettoyé {} entrées du cache d'audit", oldestKeys.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes helpers (statistiques)
|
||||||
|
private Map<TypeActionAudit, Long> getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
|
if (logToDatabase) {
|
||||||
|
List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
|
||||||
|
return entities.stream()
|
||||||
|
.collect(Collectors.groupingBy(AuditLogEntity::getAction, Collectors.counting()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return auditLogsCache.values().stream()
|
||||||
.filter(log -> {
|
.filter(log -> {
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
|
||||||
return false;
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
|
||||||
}
|
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.collect(Collectors.groupingBy(
|
.collect(Collectors.groupingBy(AuditLogDTO::getTypeAction, Collectors.counting()));
|
||||||
AuditLogDTO::getTypeAction,
|
|
||||||
Collectors.counting()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Long> getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
private Map<String, Long> getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin);
|
if (logToDatabase) {
|
||||||
|
List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
|
||||||
|
return entities.stream()
|
||||||
|
.collect(Collectors.groupingBy(AuditLogEntity::getAuteurAction, Collectors.counting()));
|
||||||
|
}
|
||||||
|
|
||||||
return auditLogs.values().stream()
|
return auditLogsCache.values().stream()
|
||||||
.filter(log -> {
|
.filter(log -> {
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
|
||||||
return false;
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
|
||||||
}
|
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.collect(Collectors.groupingBy(
|
.collect(Collectors.groupingBy(AuditLogDTO::getActeurUsername, Collectors.counting()));
|
||||||
AuditLogDTO::getActeurUsername,
|
|
||||||
Collectors.counting()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
private long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
log.debug("Comptage des échecs entre {} et {}", dateDebut, dateFin);
|
if (logToDatabase) {
|
||||||
|
return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream()
|
||||||
|
.filter(e -> !e.getSuccess())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
return auditLogs.values().stream()
|
return auditLogsCache.values().stream()
|
||||||
|
.filter(log -> !log.isSuccessful())
|
||||||
.filter(log -> {
|
.filter(log -> {
|
||||||
if (log.isSuccessful()) {
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
|
||||||
return false; // On ne compte que les échecs
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
|
||||||
}
|
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.count();
|
.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
private long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
log.debug("Comptage des succès entre {} et {}", dateDebut, dateFin);
|
if (logToDatabase) {
|
||||||
|
return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream()
|
||||||
|
.filter(AuditLogEntity::getSuccess)
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
return auditLogs.values().stream()
|
return auditLogsCache.values().stream()
|
||||||
|
.filter(AuditLogDTO::isSuccessful)
|
||||||
.filter(log -> {
|
.filter(log -> {
|
||||||
if (!log.isSuccessful()) {
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
|
||||||
return false; // On ne compte que les succès
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
|
||||||
}
|
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.count();
|
.count();
|
||||||
@@ -401,56 +543,68 @@ public class AuditServiceImpl implements AuditService {
|
|||||||
log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin);
|
log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin);
|
||||||
|
|
||||||
List<String> csvLines = new ArrayList<>();
|
List<String> csvLines = new ArrayList<>();
|
||||||
|
|
||||||
// En-tête CSV
|
|
||||||
csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur");
|
csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur");
|
||||||
|
|
||||||
// Données
|
List<AuditLogDTO> logs;
|
||||||
auditLogs.values().stream()
|
if (logToDatabase) {
|
||||||
.filter(log -> {
|
List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
|
||||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
logs = auditLogMapper.toDTOList(entities);
|
||||||
return false;
|
} else {
|
||||||
}
|
logs = auditLogsCache.values().stream()
|
||||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
.filter(log -> {
|
||||||
return false;
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
|
||||||
}
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction()))
|
.sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction()))
|
||||||
.forEach(log -> {
|
.collect(Collectors.toList());
|
||||||
String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"",
|
}
|
||||||
log.getId(),
|
|
||||||
log.getDateAction(),
|
logs.forEach(log -> {
|
||||||
log.getActeurUsername(),
|
String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"",
|
||||||
log.getTypeAction(),
|
log.getId(),
|
||||||
log.getRessourceType(),
|
log.getDateAction(),
|
||||||
log.getRessourceId(),
|
log.getActeurUsername(),
|
||||||
log.isSuccessful(),
|
log.getTypeAction(),
|
||||||
log.getIpAddress() != null ? log.getIpAddress() : "",
|
log.getRessourceType(),
|
||||||
log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "",
|
log.getRessourceId(),
|
||||||
log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : ""
|
log.isSuccessful(),
|
||||||
);
|
log.getIpAddress() != null ? log.getIpAddress() : "",
|
||||||
csvLines.add(csvLine);
|
log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "",
|
||||||
});
|
log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : ""
|
||||||
|
);
|
||||||
|
csvLines.add(csvLine);
|
||||||
|
});
|
||||||
|
|
||||||
log.info("Export CSV terminé: {} lignes", csvLines.size() - 1);
|
log.info("Export CSV terminé: {} lignes", csvLines.size() - 1);
|
||||||
return csvLines;
|
return csvLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Méthodes utilitaires ====================
|
// ==================== MÉTHODES UTILITAIRES ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne le nombre total de logs en mémoire
|
* Retourne le nombre total de logs (cache + DB).
|
||||||
*/
|
*/
|
||||||
public long getTotalCount() {
|
public long getTotalCount() {
|
||||||
return auditLogs.size();
|
if (logToDatabase) {
|
||||||
|
return AuditLogEntity.count();
|
||||||
|
}
|
||||||
|
return auditLogsCache.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vide tous les logs (ATTENTION: à utiliser uniquement en développement)
|
* Vide tous les logs (ATTENTION: à utiliser uniquement en développement).
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public void clearAll() {
|
public void clearAll() {
|
||||||
log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire");
|
log.warn("ATTENTION: Suppression de tous les logs d'audit");
|
||||||
auditLogs.clear();
|
|
||||||
|
if (logToDatabase) {
|
||||||
|
AuditLogEntity.deleteAll();
|
||||||
|
log.warn("Supprimé tous les logs de la base de données");
|
||||||
|
}
|
||||||
|
|
||||||
|
auditLogsCache.clear();
|
||||||
|
log.warn("Vidé le cache mémoire");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package dev.lions.user.manager.service.impl;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe utilitaire pour la validation des données CSV lors de l'import d'utilisateurs
|
||||||
|
*
|
||||||
|
* @author Lions Development Team
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2026-01-02
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@UtilityClass
|
||||||
|
public class CsvValidationHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern pour valider le format d'email selon RFC 5322 (simplifié)
|
||||||
|
*/
|
||||||
|
private static final Pattern EMAIL_PATTERN = Pattern.compile(
|
||||||
|
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern pour valider le username (alphanumérique, tirets, underscores, points)
|
||||||
|
*/
|
||||||
|
private static final Pattern USERNAME_PATTERN = Pattern.compile(
|
||||||
|
"^[a-zA-Z0-9._-]{2,255}$"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longueur minimale pour un username
|
||||||
|
*/
|
||||||
|
private static final int USERNAME_MIN_LENGTH = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longueur maximale pour un username
|
||||||
|
*/
|
||||||
|
private static final int USERNAME_MAX_LENGTH = 255;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longueur maximale pour un nom ou prénom
|
||||||
|
*/
|
||||||
|
private static final int NAME_MAX_LENGTH = 255;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide le format d'un email
|
||||||
|
*
|
||||||
|
* @param email Email à valider
|
||||||
|
* @return true si l'email est valide, false sinon
|
||||||
|
*/
|
||||||
|
public static boolean isValidEmail(String email) {
|
||||||
|
if (email == null || email.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return EMAIL_PATTERN.matcher(email.trim()).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un username
|
||||||
|
*
|
||||||
|
* @param username Username à valider
|
||||||
|
* @return Message d'erreur si invalide, null si valide
|
||||||
|
*/
|
||||||
|
public static String validateUsername(String username) {
|
||||||
|
if (username == null || username.isBlank()) {
|
||||||
|
return "Username obligatoire";
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = username.trim();
|
||||||
|
|
||||||
|
if (trimmed.length() < USERNAME_MIN_LENGTH) {
|
||||||
|
return String.format("Username trop court (minimum %d caractères)", USERNAME_MIN_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length() > USERNAME_MAX_LENGTH) {
|
||||||
|
return String.format("Username trop long (maximum %d caractères)", USERNAME_MAX_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!USERNAME_PATTERN.matcher(trimmed).matches()) {
|
||||||
|
return "Username invalide (autorisé: lettres, chiffres, .-_)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Valide
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un email (peut être vide)
|
||||||
|
*
|
||||||
|
* @param email Email à valider
|
||||||
|
* @return Message d'erreur si invalide, null si valide ou vide
|
||||||
|
*/
|
||||||
|
public static String validateEmail(String email) {
|
||||||
|
if (email == null || email.isBlank()) {
|
||||||
|
return null; // Email optionnel
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidEmail(email)) {
|
||||||
|
return "Format d'email invalide";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Valide
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un nom ou prénom
|
||||||
|
*
|
||||||
|
* @param name Nom à valider
|
||||||
|
* @param fieldName Nom du champ pour les messages d'erreur
|
||||||
|
* @return Message d'erreur si invalide, null si valide
|
||||||
|
*/
|
||||||
|
public static String validateName(String name, String fieldName) {
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return null; // Nom optionnel
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = name.trim();
|
||||||
|
|
||||||
|
if (trimmed.length() > NAME_MAX_LENGTH) {
|
||||||
|
return String.format("%s trop long (maximum %d caractères)", fieldName, NAME_MAX_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Valide
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide une valeur boolean
|
||||||
|
*
|
||||||
|
* @param value Valeur à valider
|
||||||
|
* @return Message d'erreur si invalide, null si valide
|
||||||
|
*/
|
||||||
|
public static String validateBoolean(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null; // Optionnel, défaut à false
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = value.trim().toLowerCase();
|
||||||
|
if (!trimmed.equals("true") && !trimmed.equals("false") &&
|
||||||
|
!trimmed.equals("1") && !trimmed.equals("0") &&
|
||||||
|
!trimmed.equals("yes") && !trimmed.equals("no")) {
|
||||||
|
return "Valeur boolean invalide (attendu: true/false, 1/0, yes/no)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Valide
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une chaîne en boolean
|
||||||
|
*
|
||||||
|
* @param value Valeur à convertir
|
||||||
|
* @return boolean correspondant
|
||||||
|
*/
|
||||||
|
public static boolean parseBoolean(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = value.trim().toLowerCase();
|
||||||
|
return trimmed.equals("true") || trimmed.equals("1") || trimmed.equals("yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie une chaîne (trim et null si vide)
|
||||||
|
*
|
||||||
|
* @param value Valeur à nettoyer
|
||||||
|
* @return Valeur nettoyée ou null
|
||||||
|
*/
|
||||||
|
public static String clean(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -608,11 +608,17 @@ public class UserServiceImpl implements UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) {
|
public dev.lions.user.manager.dto.importexport.ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) {
|
||||||
log.info("Import des utilisateurs depuis CSV pour le realm {}", realmName);
|
log.info("Import des utilisateurs depuis CSV pour le realm {}", realmName);
|
||||||
|
|
||||||
|
dev.lions.user.manager.dto.importexport.ImportResultDTO result = dev.lions.user.manager.dto.importexport.ImportResultDTO.builder()
|
||||||
|
.totalLines(0)
|
||||||
|
.successCount(0)
|
||||||
|
.errorCount(0)
|
||||||
|
.errors(new java.util.ArrayList<>())
|
||||||
|
.build();
|
||||||
|
|
||||||
String[] lines = csvContent.split("\\r?\\n");
|
String[] lines = csvContent.split("\\r?\\n");
|
||||||
int count = 0;
|
|
||||||
int startIndex = 0;
|
int startIndex = 0;
|
||||||
|
|
||||||
// Skip header if present
|
// Skip header if present
|
||||||
@@ -620,48 +626,159 @@ public class UserServiceImpl implements UserService {
|
|||||||
startIndex = 1;
|
startIndex = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.setTotalLines(lines.length - startIndex);
|
||||||
|
|
||||||
for (int i = startIndex; i < lines.length; i++) {
|
for (int i = startIndex; i < lines.length; i++) {
|
||||||
|
int lineNumber = i + 1;
|
||||||
String line = lines[i].trim();
|
String line = lines[i].trim();
|
||||||
if (line.isEmpty())
|
|
||||||
continue;
|
if (line.isEmpty()) {
|
||||||
|
continue; // Ignore empty lines
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Parse CSV line
|
||||||
String[] parts = parseCSVLine(line);
|
String[] parts = parseCSVLine(line);
|
||||||
if (parts.length < 5) {
|
if (parts.length < 5) {
|
||||||
log.warn("Ligne CSV invalide ignorée (pas assez de colonnes): {}", line);
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.INVALID_FORMAT)
|
||||||
|
.message("Nombre de colonnes insuffisant (attendu: 5, trouvé: " + parts.length + ")")
|
||||||
|
.build());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String username = parts[0];
|
String username = CsvValidationHelper.clean(parts[0]);
|
||||||
String email = parts[1];
|
String email = CsvValidationHelper.clean(parts[1]);
|
||||||
String firstName = parts[2];
|
String firstName = CsvValidationHelper.clean(parts[2]);
|
||||||
String lastName = parts[3];
|
String lastName = CsvValidationHelper.clean(parts[3]);
|
||||||
boolean enabled = Boolean.parseBoolean(parts[4]);
|
String enabledStr = CsvValidationHelper.clean(parts[4]);
|
||||||
|
|
||||||
if (username == null || username.isBlank()) {
|
// Validate username
|
||||||
log.warn("Username manquant à la ligne {}", i + 1);
|
String usernameError = CsvValidationHelper.validateUsername(username);
|
||||||
|
if (usernameError != null) {
|
||||||
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
|
||||||
|
.field("username")
|
||||||
|
.message(usernameError)
|
||||||
|
.build());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate email
|
||||||
|
String emailError = CsvValidationHelper.validateEmail(email);
|
||||||
|
if (emailError != null) {
|
||||||
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
|
||||||
|
.field("email")
|
||||||
|
.message(emailError)
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate firstName
|
||||||
|
String firstNameError = CsvValidationHelper.validateName(firstName, "Prénom");
|
||||||
|
if (firstNameError != null) {
|
||||||
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
|
||||||
|
.field("firstName")
|
||||||
|
.message(firstNameError)
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lastName
|
||||||
|
String lastNameError = CsvValidationHelper.validateName(lastName, "Nom");
|
||||||
|
if (lastNameError != null) {
|
||||||
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
|
||||||
|
.field("lastName")
|
||||||
|
.message(lastNameError)
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate enabled
|
||||||
|
String enabledError = CsvValidationHelper.validateBoolean(enabledStr);
|
||||||
|
if (enabledError != null) {
|
||||||
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
|
||||||
|
.field("enabled")
|
||||||
|
.message(enabledError)
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean enabled = CsvValidationHelper.parseBoolean(enabledStr);
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
try {
|
||||||
|
java.util.Optional<UserDTO> existingUser = getUserByUsername(username, realmName);
|
||||||
|
if (existingUser.isPresent()) {
|
||||||
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.DUPLICATE_USER)
|
||||||
|
.field("username")
|
||||||
|
.message("Utilisateur déjà existant: " + username)
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// User doesn't exist, continue with creation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
UserDTO userDTO = UserDTO.builder()
|
UserDTO userDTO = UserDTO.builder()
|
||||||
.username(username)
|
.username(username)
|
||||||
.email(email.isBlank() ? null : email)
|
.email(email)
|
||||||
.prenom(firstName.isBlank() ? null : firstName)
|
.prenom(firstName)
|
||||||
.nom(lastName.isBlank() ? null : lastName)
|
.nom(lastName)
|
||||||
.enabled(enabled)
|
.enabled(enabled)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
createUser(userDTO, realmName);
|
try {
|
||||||
count++;
|
createUser(userDTO, realmName);
|
||||||
|
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||||
|
log.debug("✅ Utilisateur créé: {} (ligne {})", username, lineNumber);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.CREATION_ERROR)
|
||||||
|
.message("Erreur lors de la création de l'utilisateur")
|
||||||
|
.details(e.getMessage())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Erreur lors de l'import de la ligne {}: {}", i + 1, e.getMessage());
|
log.error("Erreur inattendue lors du traitement de la ligne {}: {}", lineNumber, e.getMessage(), e);
|
||||||
// Continue with next line
|
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||||
|
.lineNumber(lineNumber)
|
||||||
|
.lineContent(line)
|
||||||
|
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.SYSTEM_ERROR)
|
||||||
|
.message("Erreur système")
|
||||||
|
.details(e.getMessage())
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("✅ {} utilisateurs importés avec succès", count);
|
// Generate summary message
|
||||||
return count;
|
result.generateMessage();
|
||||||
|
log.info(result.getMessage());
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String escape(String data) {
|
private String escape(String data) {
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Migration Flyway V1.0.0 - Création de la table audit_logs
|
||||||
|
-- =============================================================================
|
||||||
|
-- Description: Création de la table pour la persistance des logs d'audit
|
||||||
|
-- des actions effectuées sur le système de gestion des utilisateurs
|
||||||
|
--
|
||||||
|
-- Auteur: Lions Development Team
|
||||||
|
-- Date: 2026-01-02
|
||||||
|
-- Version: 1.0.0
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Création de la table audit_logs
|
||||||
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||||
|
-- Clé primaire générée automatiquement
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Informations sur l'utilisateur concerné
|
||||||
|
user_id VARCHAR(255),
|
||||||
|
|
||||||
|
-- Type d'action effectuée
|
||||||
|
action VARCHAR(100) NOT NULL,
|
||||||
|
|
||||||
|
-- Détails de l'action
|
||||||
|
details TEXT,
|
||||||
|
|
||||||
|
-- Informations sur l'auteur de l'action
|
||||||
|
auteur_action VARCHAR(255) NOT NULL,
|
||||||
|
|
||||||
|
-- Timestamp de l'action
|
||||||
|
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- Informations de traçabilité réseau
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent VARCHAR(500),
|
||||||
|
|
||||||
|
-- Informations multi-tenant
|
||||||
|
realm_name VARCHAR(255),
|
||||||
|
|
||||||
|
-- Statut de l'action
|
||||||
|
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
error_message TEXT,
|
||||||
|
|
||||||
|
-- Métadonnées
|
||||||
|
CONSTRAINT chk_audit_action CHECK (action IN (
|
||||||
|
-- Actions utilisateurs
|
||||||
|
'CREATION_UTILISATEUR',
|
||||||
|
'MODIFICATION_UTILISATEUR',
|
||||||
|
'SUPPRESSION_UTILISATEUR',
|
||||||
|
'ACTIVATION_UTILISATEUR',
|
||||||
|
'DESACTIVATION_UTILISATEUR',
|
||||||
|
'VERROUILLAGE_UTILISATEUR',
|
||||||
|
'DEVERROUILLAGE_UTILISATEUR',
|
||||||
|
|
||||||
|
-- Actions mot de passe
|
||||||
|
'RESET_PASSWORD',
|
||||||
|
'CHANGE_PASSWORD',
|
||||||
|
'FORCE_PASSWORD_RESET',
|
||||||
|
|
||||||
|
-- Actions sessions
|
||||||
|
'LOGOUT_UTILISATEUR',
|
||||||
|
'LOGOUT_ALL_SESSIONS',
|
||||||
|
'SESSION_EXPIREE',
|
||||||
|
|
||||||
|
-- Actions rôles
|
||||||
|
'ATTRIBUTION_ROLE',
|
||||||
|
'REVOCATION_ROLE',
|
||||||
|
'CREATION_ROLE',
|
||||||
|
'MODIFICATION_ROLE',
|
||||||
|
'SUPPRESSION_ROLE',
|
||||||
|
|
||||||
|
-- Actions groupes
|
||||||
|
'AJOUT_GROUPE',
|
||||||
|
'RETRAIT_GROUPE',
|
||||||
|
|
||||||
|
-- Actions realms
|
||||||
|
'ATTRIBUTION_REALM',
|
||||||
|
'REVOCATION_REALM',
|
||||||
|
|
||||||
|
-- Actions synchronisation
|
||||||
|
'SYNC_MANUEL',
|
||||||
|
'SYNC_AUTO',
|
||||||
|
'SYNC_ERREUR',
|
||||||
|
|
||||||
|
-- Actions import/export
|
||||||
|
'EXPORT_CSV',
|
||||||
|
'IMPORT_CSV',
|
||||||
|
|
||||||
|
-- Actions système
|
||||||
|
'CONNEXION_REUSSIE',
|
||||||
|
'CONNEXION_ECHOUEE',
|
||||||
|
'TENTATIVE_ACCES_NON_AUTORISE',
|
||||||
|
'ERREUR_SYSTEME',
|
||||||
|
'CONFIGURATION_MODIFIEE'
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- INDEX pour optimiser les requêtes
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Index sur user_id pour recherches rapides par utilisateur
|
||||||
|
CREATE INDEX idx_audit_user_id ON audit_logs(user_id)
|
||||||
|
WHERE user_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Index sur action pour filtrer par type d'action
|
||||||
|
CREATE INDEX idx_audit_action ON audit_logs(action);
|
||||||
|
|
||||||
|
-- Index sur timestamp pour recherches chronologiques et tri
|
||||||
|
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp DESC);
|
||||||
|
|
||||||
|
-- Index sur auteur_action pour tracer les actions d'un administrateur
|
||||||
|
CREATE INDEX idx_audit_auteur ON audit_logs(auteur_action);
|
||||||
|
|
||||||
|
-- Index sur realm_name pour isolation multi-tenant
|
||||||
|
CREATE INDEX idx_audit_realm ON audit_logs(realm_name)
|
||||||
|
WHERE realm_name IS NOT NULL;
|
||||||
|
|
||||||
|
-- Index composite pour recherches fréquentes
|
||||||
|
CREATE INDEX idx_audit_user_timestamp ON audit_logs(user_id, timestamp DESC)
|
||||||
|
WHERE user_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Index sur success pour identifier rapidement les échecs
|
||||||
|
CREATE INDEX idx_audit_failures ON audit_logs(success, timestamp DESC)
|
||||||
|
WHERE success = FALSE;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- COMMENTAIRES sur les colonnes
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
COMMENT ON TABLE audit_logs IS 'Table de persistance des logs d''audit pour traçabilité complète';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN audit_logs.id IS 'Identifiant unique auto-incrémenté du log';
|
||||||
|
COMMENT ON COLUMN audit_logs.user_id IS 'ID de l''utilisateur concerné par l''action (null pour actions système)';
|
||||||
|
COMMENT ON COLUMN audit_logs.action IS 'Type d''action effectuée (enum TypeActionAudit)';
|
||||||
|
COMMENT ON COLUMN audit_logs.details IS 'Détails complémentaires sur l''action';
|
||||||
|
COMMENT ON COLUMN audit_logs.auteur_action IS 'Identifiant de l''utilisateur ayant effectué l''action';
|
||||||
|
COMMENT ON COLUMN audit_logs.timestamp IS 'Date et heure précise de l''action';
|
||||||
|
COMMENT ON COLUMN audit_logs.ip_address IS 'Adresse IP du client ayant effectué l''action';
|
||||||
|
COMMENT ON COLUMN audit_logs.user_agent IS 'User-Agent du navigateur/client';
|
||||||
|
COMMENT ON COLUMN audit_logs.realm_name IS 'Nom du realm Keycloak concerné (multi-tenant)';
|
||||||
|
COMMENT ON COLUMN audit_logs.success IS 'Indique si l''action a réussi (true) ou échoué (false)';
|
||||||
|
COMMENT ON COLUMN audit_logs.error_message IS 'Message d''erreur en cas d''échec (null si success=true)';
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- POLITIQUE DE RÉTENTION (optionnel - à activer selon besoins)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Fonction pour nettoyer automatiquement les vieux logs
|
||||||
|
-- Décommenter et adapter la période de rétention selon les besoins
|
||||||
|
|
||||||
|
/*
|
||||||
|
CREATE OR REPLACE FUNCTION cleanup_old_audit_logs() RETURNS void AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Supprime les logs de plus de 365 jours (configurable)
|
||||||
|
DELETE FROM audit_logs
|
||||||
|
WHERE timestamp < CURRENT_TIMESTAMP - INTERVAL '365 days';
|
||||||
|
|
||||||
|
RAISE NOTICE 'Logs d''audit plus anciens que 365 jours supprimés';
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Créer un job CRON (nécessite extension pg_cron)
|
||||||
|
-- SELECT cron.schedule('cleanup-audit-logs', '0 2 * * 0', 'SELECT cleanup_old_audit_logs()');
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- GRANTS (à adapter selon les rôles de votre base de données)
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- GRANT SELECT, INSERT ON audit_logs TO lions_app_user;
|
||||||
|
-- GRANT USAGE, SELECT ON SEQUENCE audit_logs_id_seq TO lions_app_user;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- FIN DE LA MIGRATION
|
||||||
|
-- =============================================================================
|
||||||
@@ -220,12 +220,15 @@ class UserServiceImplTest {
|
|||||||
lenient().doNothing().when(userResource)
|
lenient().doNothing().when(userResource)
|
||||||
.resetPassword(any(org.keycloak.representations.idm.CredentialRepresentation.class));
|
.resetPassword(any(org.keycloak.representations.idm.CredentialRepresentation.class));
|
||||||
|
|
||||||
String csvContent = "username,email,firstName,lastName,enabled\n" +
|
String csvContent = "username,prenom,nom,email\n" +
|
||||||
"imported,imp@test.com,Imp,Orter,true";
|
"imported,Imp,Orter,imp@test.com";
|
||||||
|
|
||||||
int count = userService.importUsersFromCSV(csvContent, REALM);
|
dev.lions.user.manager.dto.importexport.ImportResultDTO result =
|
||||||
|
userService.importUsersFromCSV(csvContent, REALM);
|
||||||
|
|
||||||
assertEquals(1, count);
|
assertNotNull(result);
|
||||||
|
assertEquals(1, result.getSuccessCount());
|
||||||
|
assertEquals(0, result.getErrorCount());
|
||||||
verify(usersResource, atLeastOnce()).create(argThat(u -> u.getUsername().equals("imported")));
|
verify(usersResource, atLeastOnce()).create(argThat(u -> u.getUsername().equals("imported")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user