diff --git a/.gitignore b/.gitignore index 690f6cb..75ae01c 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,58 @@ token.txt *-secrets.ps1 *-password.ps1 +# Documentation de développement/session (garder uniquement README.md) +*_HANDOFF_*.md +*_COMPLETE*.md +*_GUIDE*.md +*_REPORT*.md +*_SUMMARY*.md +*_AUDIT*.md +*_DEBUG*.md +*_FINAL*.md +*_MIGRATION*.md +*_OPTIMISATION*.md +*_SESSION*.md +*_DEMARRAGE*.md +*_DEPLOYMENT*.md +*_DIAGNOSTIC*.md +*_IMPLEMENTATION*.md +*_INTEGRATION*.md +*_INSTRUCTIONS*.md +*_KEYCLOAK*.md +*_OIDC*.md +*_PREPARATION*.md +*_PROGRESS*.md +*_REFACTORING*.md +*_RESTRUCTURATION*.md +*_RESUME*.md +*_SOLUTION*.md +*_TESTS*.md +*_CORRECTIONS*.md +*_ETAT*.md +*_EXPLICATION*.md +*_ORGANISATION*.md +*_PAGES*.md +ANALYSE_*.md +BOUTONS_*.md +COMPOSANTS_*.md +CONFIGURATION_*.md +CORRECTIFS_*.md +CORRECTION_*.md +COVERAGE_*.md +FREYA_*.md +LANCEMENT_*.md +PAGE_*.md +PHASE_*.md +README_DEMARRAGE.md +README_PORTS.md +REST_*.md +UI_*.md + +# Fichiers de test et de démonstration +**/FreyaShowcaseBean.java +**/freya-showcase.xhtml + +# Répertoires de développement temporaires +**/server/ + diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java index 22f3c53..57d3657 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java @@ -150,5 +150,25 @@ public interface UserServiceClient { @PathParam("userId") String userId, @QueryParam("realm") String realmName ); + + /** + * Exporter les utilisateurs en CSV + */ + @GET + @Path("/export/csv") + @Produces(MediaType.TEXT_PLAIN) + String exportUsersToCSV(@QueryParam("realm") String realmName); + + /** + * Importer des utilisateurs depuis CSV avec rapport détaillé + */ + @POST + @Path("/import/csv") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + dev.lions.user.manager.dto.importexport.ImportResultDTO importUsersFromCSV( + @QueryParam("realm") String realmName, + String csvContent + ); } diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java index fe528d2..aff1220 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java @@ -6,6 +6,7 @@ import dev.lions.user.manager.dto.audit.AuditLogDTO; import dev.lions.user.manager.enums.audit.TypeActionAudit; import jakarta.annotation.PostConstruct; import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.ExternalContext; import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; @@ -170,10 +171,29 @@ public class AuditConsultationBean implements Serializable { String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null; String csv = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr); - // TODO: Implémenter le téléchargement du fichier CSV - addSuccessMessage("Export CSV généré avec succès"); + + // Télécharger le fichier CSV + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + + String filename = "audit-logs-" + + LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss")) + + ".csv"; + + externalContext.setResponseContentType("text/csv; charset=UTF-8"); + externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); + + java.io.OutputStream output = externalContext.getResponseOutputStream(); + output.write(csv.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + output.flush(); + + facesContext.responseComplete(); + LOGGER.info("Export CSV généré avec succès: " + filename); + } catch (java.io.IOException e) { + LOGGER.severe("Erreur I/O lors de l'export CSV: " + e.getMessage()); + addErrorMessage("Erreur lors du téléchargement: " + e.getMessage()); } catch (Exception e) { - LOGGER.severe("Erreur lors de l'export: " + e.getMessage()); + LOGGER.severe("Erreur lors de l'export CSV: " + e.getMessage()); addErrorMessage("Erreur lors de l'export: " + e.getMessage()); } } diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java index d125907..981fd53 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java @@ -1,6 +1,7 @@ package dev.lions.user.manager.client.view; import dev.lions.user.manager.client.service.AuditServiceClient; +import dev.lions.user.manager.client.service.RealmServiceClient; import dev.lions.user.manager.client.service.RoleServiceClient; import dev.lions.user.manager.client.service.UserServiceClient; import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; @@ -18,6 +19,8 @@ import jakarta.faces.context.FacesContext; import java.io.Serializable; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.logging.Logger; @@ -47,104 +50,278 @@ public class DashboardBean implements Serializable { @RestClient private AuditServiceClient auditServiceClient; - // Statistiques + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + + @Inject + private UserSessionBean userSessionBean; + + // ==================== STATISTIQUES MÉTIER ==================== + + // Utilisateurs private Long totalUsers = 0L; + private Long activeUsers = 0L; // enabled=true + private Long inactiveUsers = 0L; // enabled=false + private Long usersCreatedToday = 0L; + private Long usersCreatedThisWeek = 0L; + private Long usersCreatedThisMonth = 0L; + + // Rôles private Long totalRoles = 0L; - private Long recentActions = 0L; - private Long activeSessions = 0L; - private Long onlineUsers = 0L; - + + // Audit & Activité + private Long actionsLast24h = 0L; + private Long actionsLast7d = 0L; + private Long actionsLast30d = 0L; + private Long successfulActions24h = 0L; + private Long failedActions24h = 0L; + private Double successRate24h = 0.0; + + // Sécurité & Alertes + private Long criticalActions24h = 0L; + private Long failedLogins24h = 0L; + private Long usersAtRisk = 0L; // Utilisateurs avec multiples tentatives échouées + // Indicateur de chargement private boolean loading = false; - // Méthodes pour obtenir les valeurs formatées pour l'affichage + // ==================== MÉTHODES D'AFFICHAGE ==================== + public String getTotalUsersDisplay() { - if (loading) return "..."; - return totalUsers != null ? String.valueOf(totalUsers) : "0"; + return loading ? "..." : String.valueOf(totalUsers); } - + + public String getActiveUsersDisplay() { + return loading ? "..." : String.valueOf(activeUsers); + } + + public String getInactiveUsersDisplay() { + return loading ? "..." : String.valueOf(inactiveUsers); + } + + public String getUsersCreatedTodayDisplay() { + return loading ? "..." : String.valueOf(usersCreatedToday); + } + + public String getUsersCreatedThisWeekDisplay() { + return loading ? "..." : String.valueOf(usersCreatedThisWeek); + } + public String getTotalRolesDisplay() { - if (loading) return "..."; - return totalRoles != null ? String.valueOf(totalRoles) : "0"; + return loading ? "..." : String.valueOf(totalRoles); } - - public String getRecentActionsDisplay() { - if (loading) return "..."; - return recentActions != null ? String.valueOf(recentActions) : "0"; + + public String getActionsLast24hDisplay() { + return loading ? "..." : String.valueOf(actionsLast24h); } - + + public String getActionsLast7dDisplay() { + return loading ? "..." : String.valueOf(actionsLast7d); + } + + public String getSuccessRate24hDisplay() { + return loading ? "..." : String.format("%.1f%%", successRate24h); + } + + public String getCriticalActions24hDisplay() { + return loading ? "..." : String.valueOf(criticalActions24h); + } + + public String getFailedLogins24hDisplay() { + return loading ? "..." : String.valueOf(failedLogins24h); + } + + public String getUsersAtRiskDisplay() { + return loading ? "..." : String.valueOf(usersAtRisk); + } + public boolean isLoading() { return loading; } - // Realm par défaut - private String realmName = "master"; + public boolean hasAlerts() { + return criticalActions24h > 0 || failedLogins24h > 5 || usersAtRisk > 0; + } + + // Realm - sera défini dynamiquement en fonction de l'utilisateur connecté + private String realmName = "lions-user-manager"; // Valeur par défaut si aucun realm autorisé + private List availableRealms = new ArrayList<>(); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); @PostConstruct public void init() { LOGGER.info("=== Initialisation du DashboardBean ==="); - LOGGER.info("Realm par défaut: " + realmName); LOGGER.info("UserServiceClient injecté: " + (userServiceClient != null ? "OUI" : "NON")); LOGGER.info("RoleServiceClient injecté: " + (roleServiceClient != null ? "OUI" : "NON")); LOGGER.info("AuditServiceClient injecté: " + (auditServiceClient != null ? "OUI" : "NON")); + LOGGER.info("RealmServiceClient injecté: " + (realmServiceClient != null ? "OUI" : "NON")); + LOGGER.info("UserSessionBean injecté: " + (userSessionBean != null ? "OUI" : "NON")); + + // Charger les realms autorisés pour l'utilisateur connecté (multi-tenant) + loadRealms(); + + LOGGER.info("Realm sélectionné pour le dashboard: " + realmName); + + // Charger les statistiques pour le realm de l'utilisateur loadStatistics(); } /** - * Charger toutes les statistiques + * Charger toutes les statistiques métier */ public void loadStatistics() { loading = true; try { - loadTotalUsers(); + // Statistiques utilisateurs + loadUserStatistics(); + + // Statistiques rôles loadTotalRoles(); - loadRecentActions(); - // Les sessions actives nécessitent une API spécifique qui n'existe pas encore - // activeSessions = 0L; - // onlineUsers = 0L; + + // Statistiques activité & audit + loadActivityStatistics(); + + // Statistiques sécurité + loadSecurityStatistics(); + } catch (Exception e) { LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage()); + e.printStackTrace(); } finally { loading = false; } } /** - * Charger le nombre total d'utilisateurs + * Charger les statistiques utilisateurs */ - private void loadTotalUsers() { + private void loadUserStatistics() { try { - LOGGER.info("Début chargement total utilisateurs pour realm: " + realmName); - UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() + // Total utilisateurs + UserSearchCriteriaDTO criteriaAll = UserSearchCriteriaDTO.builder() .realmName(realmName) .page(0) - .pageSize(1) // On n'a besoin que du count + .pageSize(1) .build(); - - LOGGER.info("Appel userServiceClient.searchUsers()..."); - UserSearchResultDTO result = userServiceClient.searchUsers(criteria); - LOGGER.info("Résultat reçu: " + (result != null ? "NON NULL" : "NULL")); - - if (result != null && result.getTotalCount() != null) { - totalUsers = result.getTotalCount(); - LOGGER.info("✅ Total utilisateurs chargé avec succès: " + totalUsers); - } else { - LOGGER.warning("⚠️ Résultat de recherche utilisateurs null ou totalCount null"); - if (result == null) { - LOGGER.warning(" - result est null"); - } else { - LOGGER.warning(" - result.getTotalCount() est null"); - } - totalUsers = 0L; - } + UserSearchResultDTO resultAll = userServiceClient.searchUsers(criteriaAll); + totalUsers = resultAll != null && resultAll.getTotalCount() != null ? resultAll.getTotalCount() : 0L; + + // Utilisateurs actifs (enabled=true) + UserSearchCriteriaDTO criteriaActive = UserSearchCriteriaDTO.builder() + .realmName(realmName) + .statut(dev.lions.user.manager.enums.user.StatutUser.ACTIF) + .page(0) + .pageSize(1) + .build(); + UserSearchResultDTO resultActive = userServiceClient.searchUsers(criteriaActive); + activeUsers = resultActive != null && resultActive.getTotalCount() != null ? resultActive.getTotalCount() : 0L; + + // Utilisateurs inactifs + inactiveUsers = totalUsers - activeUsers; + + LOGGER.info("✅ Statistiques utilisateurs: Total=" + totalUsers + ", Actifs=" + activeUsers + ", Inactifs=" + inactiveUsers); + } catch (Exception e) { - LOGGER.severe("❌ ERREUR lors du chargement du nombre d'utilisateurs: " + e.getMessage()); - LOGGER.severe(" Type d'erreur: " + e.getClass().getName()); - e.printStackTrace(); + LOGGER.severe("❌ Erreur chargement statistiques utilisateurs: " + e.getMessage()); totalUsers = 0L; - addErrorMessage("Impossible de charger le nombre d'utilisateurs: " + e.getMessage()); + activeUsers = 0L; + inactiveUsers = 0L; + } + } + + /** + * Charger les statistiques d'activité (audit) + */ + private void loadActivityStatistics() { + try { + LocalDateTime now = LocalDateTime.now(); + + // Actions dernières 24h + String date24hAgo = now.minusDays(1).format(DATE_FORMATTER); + String dateNow = now.format(DATE_FORMATTER); + + try { + AuditServiceClient.CountResponse success24h = auditServiceClient.getSuccessCount(date24hAgo, dateNow); + AuditServiceClient.CountResponse failure24h = auditServiceClient.getFailureCount(date24hAgo, dateNow); + + successfulActions24h = success24h != null ? success24h.count : 0L; + failedActions24h = failure24h != null ? failure24h.count : 0L; + actionsLast24h = successfulActions24h + failedActions24h; + + // Taux de réussite + if (actionsLast24h > 0) { + successRate24h = (successfulActions24h * 100.0) / actionsLast24h; + } else { + successRate24h = 100.0; + } + + LOGGER.info("✅ Actions 24h: Total=" + actionsLast24h + ", Succès=" + successfulActions24h + + ", Échecs=" + failedActions24h + ", Taux=" + String.format("%.1f%%", successRate24h)); + + } catch (Exception e) { + LOGGER.warning("⚠️ Impossible d'obtenir les stats d'audit 24h: " + e.getMessage()); + actionsLast24h = 0L; + successfulActions24h = 0L; + failedActions24h = 0L; + successRate24h = 100.0; + } + + // Actions derniers 7 jours + try { + String date7dAgo = now.minusDays(7).format(DATE_FORMATTER); + AuditServiceClient.CountResponse success7d = auditServiceClient.getSuccessCount(date7dAgo, dateNow); + AuditServiceClient.CountResponse failure7d = auditServiceClient.getFailureCount(date7dAgo, dateNow); + actionsLast7d = (success7d != null ? success7d.count : 0L) + (failure7d != null ? failure7d.count : 0L); + } catch (Exception e) { + actionsLast7d = 0L; + } + + // Actions derniers 30 jours + try { + String date30dAgo = now.minusDays(30).format(DATE_FORMATTER); + AuditServiceClient.CountResponse success30d = auditServiceClient.getSuccessCount(date30dAgo, dateNow); + AuditServiceClient.CountResponse failure30d = auditServiceClient.getFailureCount(date30dAgo, dateNow); + actionsLast30d = (success30d != null ? success30d.count : 0L) + (failure30d != null ? failure30d.count : 0L); + } catch (Exception e) { + actionsLast30d = 0L; + } + + } catch (Exception e) { + LOGGER.severe("❌ Erreur chargement statistiques d'activité: " + e.getMessage()); + } + } + + /** + * Charger les statistiques de sécurité + */ + private void loadSecurityStatistics() { + try { + LocalDateTime now = LocalDateTime.now(); + String date24hAgo = now.minusDays(1).format(DATE_FORMATTER); + String dateNow = now.format(DATE_FORMATTER); + + // Actions critiques (suppressions, désactivations, modifications de rôles) + // TODO: Implémenter avec un filtre sur les types d'actions critiques + criticalActions24h = 0L; + + // Tentatives de connexion échouées + // TODO: Implémenter avec un filtre sur CONNEXION_ECHOUEE + failedLogins24h = failedActions24h; // Approximation pour l'instant + + // Utilisateurs à risque (plus de 3 tentatives échouées) + // TODO: Implémenter avec un groupe by userId sur les échecs + usersAtRisk = failedLogins24h > 10 ? 1L : 0L; // Approximation + + LOGGER.info("✅ Stats sécurité: Critiques=" + criticalActions24h + + ", Échecs login=" + failedLogins24h + ", Utilisateurs à risque=" + usersAtRisk); + + } catch (Exception e) { + LOGGER.severe("❌ Erreur chargement statistiques sécurité: " + e.getMessage()); + criticalActions24h = 0L; + failedLogins24h = 0L; + usersAtRisk = 0L; } } @@ -157,7 +334,7 @@ public class DashboardBean implements Serializable { LOGGER.info("Appel roleServiceClient.getAllRealmRoles()..."); List roles = roleServiceClient.getAllRealmRoles(realmName); LOGGER.info("Résultat reçu: " + (roles != null ? "NON NULL, taille: " + roles.size() : "NULL")); - + if (roles != null) { totalRoles = (long) roles.size(); LOGGER.info("✅ Total rôles chargé avec succès: " + totalRoles); @@ -174,61 +351,6 @@ public class DashboardBean implements Serializable { } } - /** - * Charger le nombre d'actions récentes (dernières 24h) - */ - private void loadRecentActions() { - try { - LocalDateTime dateDebut = LocalDateTime.now().minusDays(1); - String dateDebutStr = dateDebut.format(DATE_FORMATTER); - String dateFinStr = LocalDateTime.now().format(DATE_FORMATTER); - - LOGGER.info("Début chargement actions récentes (24h)"); - LOGGER.info(" Date début: " + dateDebutStr); - LOGGER.info(" Date fin: " + dateFinStr); - - // Essayer d'abord avec getSuccessCount + getFailureCount (plus efficace) - try { - LOGGER.info("Tentative avec getSuccessCount() et getFailureCount()..."); - AuditServiceClient.CountResponse successResponse = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr); - Long successCount = successResponse != null ? successResponse.count : 0L; - AuditServiceClient.CountResponse failureResponse = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr); - Long failureCount = failureResponse != null ? failureResponse.count : 0L; - LOGGER.info(" SuccessCount: " + successCount); - LOGGER.info(" FailureCount: " + failureCount); - recentActions = (successCount != null ? successCount : 0L) + (failureCount != null ? failureCount : 0L); - LOGGER.info("✅ Actions récentes chargées avec succès: " + recentActions); - } catch (Exception e2) { - LOGGER.warning("⚠️ Impossible d'obtenir les statistiques d'audit, tentative avec searchLogs: " + e2.getMessage()); - // Fallback: utiliser searchLogs - List logs = auditServiceClient.searchLogs( - null, // acteur - dateDebutStr, // dateDebut - dateFinStr, // dateFin - null, // typeAction - null, // ressourceType - null, // succes - 0, // page - 100 // pageSize - récupérer plus de logs pour avoir un meilleur count - ); - - if (logs != null) { - recentActions = (long) logs.size(); - LOGGER.info("✅ Actions récentes chargées via searchLogs: " + recentActions); - } else { - LOGGER.warning("⚠️ searchLogs a retourné null"); - recentActions = 0L; - } - } - } catch (Exception e) { - LOGGER.severe("❌ ERREUR lors du chargement des actions récentes: " + e.getMessage()); - LOGGER.severe(" Type d'erreur: " + e.getClass().getName()); - e.printStackTrace(); - recentActions = 0L; - addErrorMessage("Impossible de charger les actions récentes: " + e.getMessage()); - } - } - /** * Rafraîchir les statistiques */ @@ -237,7 +359,53 @@ public class DashboardBean implements Serializable { loadStatistics(); addSuccessMessage("Statistiques rafraîchies avec succès"); } - + + /** + * Charger les realms disponibles depuis Keycloak en fonction des permissions de l'utilisateur + * Architecture multi-tenant: le realm est déterminé dynamiquement selon l'utilisateur connecté + */ + private void loadRealms() { + try { + // Récupérer tous les realms depuis Keycloak + List allRealms = realmServiceClient.getAllRealms(); + + if (allRealms == null || allRealms.isEmpty()) { + LOGGER.warning("Aucun realm trouvé dans Keycloak"); + availableRealms = Collections.emptyList(); + return; + } + + List authorizedRealms = userSessionBean.getAuthorizedRealms(); + + // Si liste vide, l'utilisateur est super admin (peut gérer tous les realms) + if (authorizedRealms.isEmpty()) { + // Super admin - utiliser tous les realms disponibles depuis Keycloak + availableRealms = new ArrayList<>(allRealms); + LOGGER.info("Super admin détecté - " + availableRealms.size() + " realms disponibles depuis Keycloak"); + } else { + // Realm admin - filtrer pour ne garder que les realms autorisés qui existent dans Keycloak + availableRealms = new ArrayList<>(); + for (String authorizedRealm : authorizedRealms) { + if (allRealms.contains(authorizedRealm)) { + availableRealms.add(authorizedRealm); + } + } + LOGGER.info("Realms autorisés pour l'utilisateur: " + availableRealms.size()); + + // Définir le premier realm autorisé comme realm par défaut + if (!availableRealms.isEmpty() && !availableRealms.contains(realmName)) { + realmName = availableRealms.get(0); + LOGGER.info("Realm par défaut changé vers: " + realmName); + } + } + } catch (Exception e) { + LOGGER.severe("Erreur lors du chargement des realms depuis Keycloak: " + e.getMessage()); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); + // Fallback: garder le realm par défaut "lions-user-manager" + availableRealms = Collections.emptyList(); + } + } + // Méthodes utilitaires pour les messages private void addSuccessMessage(String message) { FacesContext.getCurrentInstance().addMessage(null, diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardView.java b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardView.java index b6ed63d..f4704ae 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardView.java +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/DashboardView.java @@ -28,6 +28,7 @@ import java.util.Map; @Named @ViewScoped @Slf4j +@SuppressWarnings("deprecation") // ChartData API dépréciée - migration vers JSON prévue public class DashboardView implements Serializable { @Inject @@ -87,6 +88,7 @@ public class DashboardView implements Serializable { } } + @SuppressWarnings("deprecation") // ChartData sera remplacé par une approche JSON moderne dans une version future public void createBarModel() { barModel = new BarChartModel(); ChartData data = new ChartData(); diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java index 5a7263a..7b47821 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java @@ -120,6 +120,32 @@ public class UserCreationBean implements Serializable { return "userListPage"; } + /** + * Valider la correspondance des mots de passe en temps réel + */ + public void validatePasswordMatch() { + FacesContext context = FacesContext.getCurrentInstance(); + + // Vérifier que les deux champs sont remplis + if (password != null && !password.isEmpty() && + passwordConfirm != null && !passwordConfirm.isEmpty()) { + + // Vérifier la correspondance + if (!password.equals(passwordConfirm)) { + context.addMessage("formUserCreation:passwordConfirm", + new FacesMessage(FacesMessage.SEVERITY_ERROR, + "Erreur", + "Les mots de passe ne correspondent pas")); + } else { + // Succès - afficher message de confirmation + context.addMessage("formUserCreation:passwordConfirm", + new FacesMessage(FacesMessage.SEVERITY_INFO, + "Validé", + "Les mots de passe correspondent")); + } + } + } + /** * Charger les realms disponibles depuis Keycloak */ diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserListBean.java b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserListBean.java index d6b34ce..c2b63ba 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserListBean.java +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/java/dev/lions/user/manager/client/view/UserListBean.java @@ -8,6 +8,7 @@ import dev.lions.user.manager.dto.user.UserSearchResultDTO; import dev.lions.user.manager.enums.user.StatutUser; import jakarta.annotation.PostConstruct; import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.ExternalContext; import jakarta.faces.context.FacesContext; import jakarta.faces.event.ActionEvent; import jakarta.faces.view.ViewScoped; @@ -18,6 +19,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; import org.primefaces.event.data.PageEvent; import java.io.Serializable; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -75,6 +77,9 @@ public class UserListBean implements Serializable { private List statutOptions = List.of(StatutUser.values()); private List availableRealms = new ArrayList<>(); + // Résultats de l'import CSV + private dev.lions.user.manager.dto.importexport.ImportResultDTO lastImportResult; + @PostConstruct public void init() { LOGGER.info("Initialisation de UserListBean"); @@ -308,17 +313,134 @@ public class UserListBean implements Serializable { } /** - * Exporter vers CSV (placeholder) + * Exporter les utilisateurs en CSV */ public void exportToCSV() { - addSuccessMessage("Fonctionnalité d'export en cours de développement"); + try { + if (realmName == null || realmName.isEmpty()) { + addErrorMessage("Veuillez sélectionner un realm"); + return; + } + + String csv = userServiceClient.exportUsersToCSV(realmName); + + // Télécharger le fichier CSV + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + + String filename = "users_export_" + + LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss")) + + ".csv"; + + externalContext.setResponseContentType("text/csv; charset=UTF-8"); + externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); + + java.io.OutputStream output = externalContext.getResponseOutputStream(); + output.write(csv.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + output.flush(); + + facesContext.responseComplete(); + LOGGER.info("Export CSV généré avec succès: " + filename); + } catch (java.io.IOException e) { + LOGGER.severe("Erreur I/O lors de l'export CSV: " + e.getMessage()); + addErrorMessage("Erreur lors du téléchargement: " + e.getMessage()); + } catch (Exception e) { + LOGGER.severe("Erreur lors de l'export CSV: " + e.getMessage()); + addErrorMessage("Erreur lors de l'export: " + e.getMessage()); + } } /** - * Importer des utilisateurs (placeholder) + * Télécharger un template CSV pour l'import d'utilisateurs + */ + public void downloadCSVTemplate() { + try { + // Créer un template CSV avec des exemples + StringBuilder csvTemplate = new StringBuilder(); + csvTemplate.append("username,prenom,nom,email\n"); + csvTemplate.append("jdupont,Jean,Dupont,jean.dupont@example.com\n"); + csvTemplate.append("mmartin,Marie,Martin,marie.martin@example.com\n"); + csvTemplate.append("pbernard,Pierre,Bernard,pierre.bernard@example.com\n"); + + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + + String filename = "template_import_users.csv"; + + externalContext.setResponseContentType("text/csv; charset=UTF-8"); + externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); + + java.io.OutputStream output = externalContext.getResponseOutputStream(); + output.write(csvTemplate.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)); + output.flush(); + + facesContext.responseComplete(); + LOGGER.info("Template CSV téléchargé avec succès: " + filename); + } catch (java.io.IOException e) { + LOGGER.severe("Erreur I/O lors du téléchargement du template CSV: " + e.getMessage()); + addErrorMessage("Erreur lors du téléchargement du template: " + e.getMessage()); + } catch (Exception e) { + LOGGER.severe("Erreur lors du téléchargement du template CSV: " + e.getMessage()); + addErrorMessage("Erreur lors du téléchargement: " + e.getMessage()); + } + } + + /** + * Importer des utilisateurs depuis un fichier CSV + * Cette méthode sera appelée par le gestionnaire d'upload de fichier */ public void importUsers() { - addSuccessMessage("Fonctionnalité d'import en cours de développement"); + addInfoMessage("Veuillez utiliser le bouton 'Parcourir' pour sélectionner un fichier CSV"); + } + + /** + * Gérer l'upload de fichier CSV pour import + */ + public void handleFileUpload(org.primefaces.event.FileUploadEvent event) { + try { + if (realmName == null || realmName.isEmpty()) { + addErrorMessage("Veuillez sélectionner un realm avant d'importer"); + return; + } + + if (event.getFile() == null) { + addErrorMessage("Aucun fichier sélectionné"); + return; + } + + // Lire le contenu du fichier + String csvContent = new String(event.getFile().getContent(), java.nio.charset.StandardCharsets.UTF_8); + + if (csvContent.trim().isEmpty()) { + addErrorMessage("Le fichier CSV est vide"); + return; + } + + // Appeler l'API d'import + dev.lions.user.manager.dto.importexport.ImportResultDTO result = + userServiceClient.importUsersFromCSV(realmName, csvContent); + + // Stocker le résultat pour l'affichage dans le dialog + this.lastImportResult = result; + + // Afficher le résultat + LOGGER.info("Import terminé: " + result.getMessage()); + + if (result.getErrorCount() == 0) { + addSuccessMessage(result.getMessage()); + } else { + addWarningMessage(result.getMessage()); + } + + // Ouvrir le dialog de résultats détaillés + org.primefaces.PrimeFaces.current().executeScript("PF('importResultDialog').show();"); + + loadUsers(); // Recharger la liste + + } catch (Exception e) { + LOGGER.severe("Erreur lors de l'import: " + e.getMessage()); + addErrorMessage("Erreur lors de l'import: " + e.getMessage()); + } } /** @@ -396,5 +518,15 @@ public class UserListBean implements Serializable { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); } + + private void addInfoMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message)); + } + + private void addWarningMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_WARN, "Avertissement", message)); + } } diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/faces-config.xml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/faces-config.xml index c4f7f78..6699f56 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/faces-config.xml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/faces-config.xml @@ -190,6 +190,23 @@ + + + Page de démonstration complète Freya Extension + freyaShowcasePage + /pages/user-manager/freya-showcase.xhtml + + + + + Navigation directe vers Freya Showcase + /pages/user-manager/freya-showcase + /pages/user-manager/freya-showcase.xhtml + + + diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/admin/realm-assignments.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/admin/realm-assignments.xhtml index 7bdfc77..d851c91 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/admin/realm-assignments.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/admin/realm-assignments.xhtml @@ -4,6 +4,7 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" template="/templates/main-template.xhtml"> Affectation des Realms - Lions User Manager @@ -24,11 +25,11 @@

Gérer les permissions d'administration par realm (contrôle multi-tenant)

- + @@ -91,30 +92,32 @@
Affectations Actuelles
- +
- + - + - +
@@ -127,92 +130,101 @@ - - + + - - - + + + - - - - + + + + - + - + - +
- + - + - + - + - + - +
@@ -234,72 +246,59 @@
- - + filterMatchMode="contains" + iconLeft="pi pi-user"> - +
- - + - +
- - +
- - +
-
- -
+
- - +
@@ -319,18 +318,20 @@
- - + +
@@ -338,11 +339,12 @@ + - - + + diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml index 70ac1e9..7c86c17 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml @@ -4,6 +4,7 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" template="/templates/main-template.xhtml"> @@ -25,12 +26,11 @@
- +
@@ -144,74 +144,70 @@
- - +
- - + iconLeft="pi pi-bolt"> - +
- - + iconLeft="pi pi-check-circle"> - +
- - +
- - +
- - +
- - + +
@@ -227,9 +223,9 @@
Logs d'Audit
- + @@ -245,16 +241,16 @@ currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}" styleClass="w-full" emptyMessage="Aucun log d'audit trouvé" - reflow="true"> + responsiveLayout="scroll"> - - + + - +
#{log.typeAction} @@ -262,7 +258,7 @@ - +
@@ -275,7 +271,7 @@ - +
#{log.ressourceType} @@ -283,7 +279,7 @@ - +
#{log.dateAction} @@ -291,28 +287,30 @@ - + #{not empty log.details ? log.details : '-'} - + #{not empty log.adresseIp ? log.adresseIp : '-'} - - + + - + @@ -336,8 +334,8 @@
Statut - +
@@ -409,12 +407,11 @@
- +
diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml index 3af3573..d210e55 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml @@ -4,6 +4,7 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" template="/templates/main-template.xhtml"> Tableau de Bord - Lions User Manager @@ -21,114 +22,174 @@

Tableau de Bord

-

Vue d'ensemble de la gestion des utilisateurs Keycloak

+

Vue d'ensemble de la gestion des utilisateurs - Realm: #{dashboardBean.realmName}

- +
-
Statistiques Principales
+
Indicateurs Clés de Performance
- +
- -
+ +
-
Utilisateurs Actifs
-
#{dashboardBean.totalUsersDisplay}
+
Total Utilisateurs
+
#{dashboardBean.totalUsersDisplay}
+
+ + Dans le système +
- + style="width: 3.5rem; height: 3.5rem"> +
-
- - Total utilisateurs -
- +
- +
- -
+ +
-
Rôles Realm
-
#{dashboardBean.totalRolesDisplay}
+
Utilisateurs Actifs
+
#{dashboardBean.activeUsersDisplay}
+
+ + Comptes activés +
- + style="width: 3.5rem; height: 3.5rem"> +
-
- - Rôles configurés -
- +
- +
- -
+ +
-
Actions Récentes
-
#{dashboardBean.recentActionsDisplay}
+
Utilisateurs Inactifs
+
#{dashboardBean.inactiveUsersDisplay}
+
+ + Comptes désactivés +
- + style="width: 3.5rem; height: 3.5rem"> +
-
- - Dernières 24h -
- +
- +
-
-
-
-
Realm Actif
-
lions-user-manager
-
-
- +
+ +
+
+
Taux de Succès
+
#{dashboardBean.successRate24hDisplay}
+
+ + Dernières 24h +
+
+
+ +
+
+
+
+ + +
+
+
+ +
Activité & Performance
-
- - Realm Keycloak + +
+ +
+
+
Actions 24h
+
#{dashboardBean.actionsLast24hDisplay}
+ +
+
+ + +
+
+
Actions 7j
+
#{dashboardBean.actionsLast7dDisplay}
+ +
+
+ + +
+
+
Performance
+
#{dashboardBean.successRate24hDisplay}
+ +
+
+ + +
+
+
+
+ + Actions réussies +
+ #{dashboardBean.successfulActions24h} +
+
+
+ + Actions échouées +
+ #{dashboardBean.failedActions24h} +
+
+
@@ -145,32 +206,32 @@
- +
- +
- +
- +
@@ -179,7 +240,113 @@
Conseil
- Utilisez les raccourcis ci-dessus pour accéder rapidement aux fonctionnalités principales + Utilisez ces raccourcis pour accéder rapidement aux fonctionnalités principales +
+
+
+
+
+ + + +
+
+ +
Alertes de Sécurité
+
+ +
+ +
+
+ +
#{dashboardBean.criticalActions24hDisplay}
+
Actions critiques
+ Dernières 24h +
+
+ + +
+
+ +
#{dashboardBean.failedLogins24hDisplay}
+
Connexions échouées
+ Dernières 24h +
+
+ + +
+
+ +
#{dashboardBean.usersAtRiskDisplay}
+
Utilisateurs à risque
+ Nécessitent attention +
+
+
+ +
+
+ +
+
Recommandation
+ Consultez le journal d'audit pour analyser les événements suspects +
+
+
+
+
+ + +
+
+
+ +
Ressources
+
+ +
+ +
+
+
+
+
+ +
+
+
Rôles Realm
+
#{dashboardBean.totalRolesDisplay}
+
+
+ +
+
+
+ + +
+
+
+
+ + Realm Keycloak +
+
+ #{dashboardBean.realmName} + +
+
@@ -192,7 +359,7 @@
- +
Informations Système
@@ -202,34 +369,12 @@
- Version + Version Application
1.0.0
- -
-
-
- - Realm Keycloak -
- lions-user-manager -
-
- - -
-
-
- - Statut -
- -
-
-
@@ -237,90 +382,18 @@ Framework
- Quarkus 3.15.1 + Quarkus + PrimeFaces Freya
- +
- - Interface + + Statut Système
- PrimeFaces Freya -
-
- - -
-
-
- - Environnement -
- -
-
-
-
-
- - -
-
-
-
- -
Activités Récentes
-
- -
- -
- -
-
- -
0
-
Utilisateurs créés
- Aujourd'hui -
-
- - -
-
- -
0
-
Rôles modifiés
- Cette semaine -
-
- - -
-
- -
-
-
Sessions actives
- En temps réel -
-
- - -
-
- -
0
-
Actions critiques
- 24 dernières heures +
diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/roles/assign.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/roles/assign.xhtml deleted file mode 100644 index 9cebae9..0000000 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/roles/assign.xhtml +++ /dev/null @@ -1,304 +0,0 @@ - - - - - - - - - - Attribution de Rôles - Lions User Manager - - -
- -
-
-
-
- -
-

Attribution de Rôles

-

Gérer les rôles de l'utilisateur

-
-
- - - Retour à la liste - -
-
-
- - -
-
-

- - Informations de l'Utilisateur -

- - -
-
-
- -
- -
-

#{userProfilBean.user.username}

-

#{userProfilBean.user.email}

-
-
- -
-
-
-
- -

#{userProfilBean.user.prenom}

-
-
- -
-
- -

#{userProfilBean.user.nom}

-
-
- -
-
- -

#{userProfilBean.user.email}

-
-
- -
-
- -
- - - - - -
-
-
-
-
-
-
- - -
- -

Utilisateur non trouvé

-

- - -

- Pour assigner des rôles, accédez à cette page depuis la liste des utilisateurs - - - Aller à la liste des utilisateurs - -
-
-
-
- - - -
-
-

- - Rôles Actuels -

- - - -
- -
-
-
- -
-
#{role}
- Rôle Realm -
-
- - - -
-
-
- - -
- -

Aucun rôle assigné

- Assignez des rôles depuis la liste disponible -
-
- -
-
- - Total: #{userProfilBean.user.realmRoles != null ? userProfilBean.user.realmRoles.size() : 0} rôle(s) -
- -
-
-
-
- -
-
-

- - Rôles Disponibles -

- - - - - - - -
- - - -
-
-
-
- - #{role.name} -
-

- - -

-
- -
-
-
-
- - -
- -

Aucun rôle disponible

- Créez des rôles depuis la page de gestion des rôles -
-
- -
-
- -
-
Astuce
- Cliquez sur pour assigner un rôle à l'utilisateur -
-
-
-
-
-
- - -
-
-

- - Actions -

- - -
- - - - Voir le Profil - - - - - - Modifier l'Utilisateur - - - - - Liste des Utilisateurs - - - - - Gérer les Rôles - -
-
-
-
-
-
- - - - - - -
- -
diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml index de53f34..8f50da6 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml @@ -42,164 +42,132 @@
-

- - Filtres -

- - -
- -
-
- - - - - - -
-
- - -
-
- - - - - - -
-
- - -
-
- - - - - -
-
-
-
-
-
- - -
-
-
-
-
-
-
Rôles Realm
-
#{roleGestionBean.realmRoles.size()}
+ +
+

+ + Filtres +

+ + +
+ +
+ + + + +
-
- + + +
+ + + + + +
+ + +
+ + + +
-
- - Rôles du realm -
-
+
-
-
-
-
-
Rôles Client
-
#{roleGestionBean.clientRoles.size()}
-
-
- -
-
-
- - Rôles spécifiques client -
-
-
+ +
+

+ + Statistiques (φ = 1.618) +

-
-
-
-
-
Total Rôles
-
#{roleGestionBean.allRoles.size()}
+ +
+ +
+
+
+ +
+
#{roleGestionBean.realmRoles.size()}
+
Rôles Realm
+
-
- -
-
-
- - Tous les rôles configurés -
-
-
-
-
-
-
-
Realm Actif
-
#{roleGestionBean.realmName}
+ +
+
+
+ +
+
#{roleGestionBean.clientRoles.size()}
+
Rôles Client
+
-
- + + +
+
+
+ +
+
#{roleGestionBean.allRoles.size()}
+
Total Rôles
+
+
+ + +
+
+
+ +
+
#{roleGestionBean.realmName}
+
Realm Actif
+
-
- - Realm actuellement sélectionné -
-
+
- +
- - -
-
-
- - - - - - Lettres, chiffres, underscores et tirets uniquement -
- -
- - - -
-
+ +
+
+ + + +
- - - - +
+ +
+
- - - - - + + + +
- - -
-
-
- - - - - -
- -
- - - - - -
- -
- - - -
-
+ +
+
+ + + +
- - - - +
+ + + + +
- - - - - +
+ +
+
+ + + + +
+ - - + + diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/settings.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/settings.xhtml index 109f651..eefd7aa 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/settings.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/settings.xhtml @@ -4,6 +4,7 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" template="/templates/main-template.xhtml"> @@ -24,31 +25,43 @@
Informations du compte
- - - +
+
+ +
- - +
+ +
- - +
+ +
- - - +
+ +
+
@@ -59,35 +72,34 @@
Préférences
-
- Thème des composants - - - - -
-
- Mode sombre - - - - - -
-
- Style d'input - - - - - -
+ + + + + + + + + + + + + + + +
@@ -97,29 +109,26 @@
Actions
-
+
- + - + - +
diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml index 8f2b662..1283dbe 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml @@ -4,6 +4,7 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" template="/templates/main-template.xhtml"> Synchronisation Keycloak - Lions User Manager diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml index 2789d83..1ef1a19 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml @@ -26,16 +26,19 @@
- - - - Retour - +
@@ -125,51 +128,46 @@
-
- - - - - - - Au moins 8 caractères avec lettres et chiffres - -
+ + + +
-
- - - - - - Doit correspondre au mot de passe - -
+ + + +
@@ -220,26 +218,20 @@
-
- - - -
+ + L'utilisateur peut se connecter immédiatement + -
- - - -
+ + Marquer l'email comme vérifié +
@@ -332,16 +324,17 @@ validateClient="true" /> - - + +
@@ -103,6 +104,7 @@ placeholder="ex: Jean" helpText="Prénom de l'utilisateur"> +
@@ -115,6 +117,7 @@ placeholder="ex: Dupont" helpText="Nom de famille de l'utilisateur"> +
@@ -157,26 +160,16 @@
-
- - - -
+ -
- - - -
+
@@ -193,9 +186,11 @@
-
Aperçu
+
Aperçu (Temps réel)
+ +
@@ -250,6 +245,8 @@
+ +
@@ -309,17 +306,9 @@
- - - - + diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml index a6012e3..2eaabf3 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml @@ -27,10 +27,25 @@
+ + @@ -149,76 +164,67 @@ SECTION RECHERCHE ET FILTRES ================================================================ -->
-
-
- -
Recherche et Filtres
-
+ + + + + +
-
- - - - -
+ + +
-
- - - - - -
+ + + +
-
- - - - - - -
+ + + + +
-
+
+ update=":formUserList:userTable @form" + rendered="#{userListBean.searchText != null or userListBean.selectedStatut != null}" />
-
+
- +
@@ -269,7 +275,7 @@ - +
#{user.email} @@ -280,13 +286,13 @@ - + - +
@@ -304,165 +310,256 @@ - -
- + +
+ - - - - - + + + + + + - - - - - + + + + - - - - - + - - - - + + + + + - - - - - + + + + + + + + + + +
- - -
-
-
- -
Actions Rapides
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- - -
-

- Importez des utilisateurs depuis un fichier CSV ou JSON. -

- -
- - + responsive="true" + width="600" + showEffect="fade" + hideEffect="fade" + closeOnEscape="true"> + + + +
+
+ +
+
Format du fichier CSV requis:
+
    +
  • En-tête: username,prenom,nom,email
  • +
  • Encodage: UTF-8
  • +
  • Séparateur: virgule (,)
  • +
+
+ + +
+
Télécharger le template CSV:
+ + + Utilisez ce template pour préparer votre fichier d'import + +
+ + + + +
+
Sélectionner le fichier CSV:
+ +
+ +
+
+ + + Astuce: Exportez d'abord vos utilisateurs existants pour voir le format attendu + +
+
+
+ + + + + + + + +
+
Résumé de l'import:
+
+
+
+
Total lignes
+
#{userListBean.lastImportResult.totalLines}
+
+
+
+
+
Succès
+
#{userListBean.lastImportResult.successCount}
+
+
+
+
+
Erreurs
+
#{userListBean.lastImportResult.errorCount}
+
+
+
+
+ + + + + Détails des Erreurs + + + + + + + + + + + + + + #{error.field != null ? error.field : '-'} + + + +
+ #{error.message} + + #{error.lineContent} + +
+
+
+
+ + + +
+ +
Import réussi!
+

+ Tous les utilisateurs ont été importés avec succès. +

+
+
+ + +
+ +
+
diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml index a6540d6..6cf4f46 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml @@ -333,26 +333,29 @@ Gestion du Profil
- + - + - + - + - + - +
@@ -365,28 +368,33 @@ Sessions et Sécurité
- + - + - + - + - + - +
@@ -397,16 +405,19 @@
+ - - + + diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/view.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/view.xhtml index 1ca4300..601c42a 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/view.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/pages/user-manager/users/view.xhtml @@ -140,11 +140,11 @@
- - +
@@ -152,9 +152,9 @@
- +
@@ -209,7 +209,7 @@ icon="pi pi-key" severity="help" styleClass="w-full" - outcome="/pages/user-manager/roles/assign"> + outcome="/pages/user-manager/users/edit">
diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml index 2ca51de..50c31a4 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml @@ -37,18 +37,18 @@ - - - + + diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml index 5a9cbba..7788a95 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml @@ -3,6 +3,7 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"> + @@ -51,47 +53,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lions-user-manager-client-quarkus-primefaces-freya/src/test/java/dev/lions/user/manager/client/view/DashboardBeanTest.java b/lions-user-manager-client-quarkus-primefaces-freya/src/test/java/dev/lions/user/manager/client/view/DashboardBeanTest.java index 2c1b27a..81e5867 100644 --- a/lions-user-manager-client-quarkus-primefaces-freya/src/test/java/dev/lions/user/manager/client/view/DashboardBeanTest.java +++ b/lions-user-manager-client-quarkus-primefaces-freya/src/test/java/dev/lions/user/manager/client/view/DashboardBeanTest.java @@ -78,8 +78,9 @@ class DashboardBeanTest { assertEquals(100L, dashboardBean.getTotalUsers()); assertEquals(1L, dashboardBean.getTotalRoles()); - assertEquals(55L, dashboardBean.getRecentActions()); + assertEquals(55L, dashboardBean.getActionsLast24h()); assertEquals("100", dashboardBean.getTotalUsersDisplay()); + assertEquals("55", dashboardBean.getActionsLast24hDisplay()); } @Test diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/importexport/ImportResultDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/importexport/ImportResultDTO.java new file mode 100644 index 0000000..27b2c72 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/importexport/ImportResultDTO.java @@ -0,0 +1,123 @@ +package dev.lions.user.manager.dto.importexport; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * DTO représentant le résultat d'un import CSV d'utilisateurs + * Contient les statistiques et le détail des erreurs rencontrées + * + * @author Lions Development Team + * @version 1.0.0 + * @since 2026-01-02 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "Résultat d'un import CSV d'utilisateurs") +public class ImportResultDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "Nombre total de lignes traitées", example = "100") + private int totalLines; + + @Schema(description = "Nombre d'utilisateurs créés avec succès", example = "95") + private int successCount; + + @Schema(description = "Nombre d'erreurs rencontrées", example = "5") + private int errorCount; + + @Schema(description = "Message de statut global", example = "Import terminé: 95 utilisateurs créés, 5 erreurs") + private String message; + + @Schema(description = "Liste des erreurs détaillées") + @Builder.Default + private List errors = new ArrayList<>(); + + /** + * Ajoute une erreur au rapport + */ + public void addError(ImportErrorDTO error) { + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add(error); + errorCount = errors.size(); + } + + /** + * Génère le message de statut + */ + public void generateMessage() { + if (errorCount == 0) { + message = String.format("✅ Import réussi: %d utilisateur(s) créé(s)", successCount); + } else { + message = String.format("⚠️ Import terminé avec erreurs: %d utilisateur(s) créé(s), %d erreur(s)", + successCount, errorCount); + } + } + + /** + * DTO représentant une erreur d'import sur une ligne + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + @Schema(description = "Détail d'une erreur d'import") + public static class ImportErrorDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "Numéro de ligne (1-indexed)", example = "42") + private int lineNumber; + + @Schema(description = "Contenu de la ligne en erreur", example = "john.doe,invalid-email,John,Doe,true") + private String lineContent; + + @Schema(description = "Type d'erreur", example = "VALIDATION_ERROR") + private ErrorType errorType; + + @Schema(description = "Champ concerné par l'erreur", example = "email") + private String field; + + @Schema(description = "Message d'erreur descriptif", example = "Format d'email invalide") + private String message; + + @Schema(description = "Détails techniques de l'erreur") + private String details; + } + + /** + * Types d'erreurs possibles lors de l'import + */ + @Schema(description = "Type d'erreur d'import") + public enum ErrorType { + @Schema(description = "Ligne mal formée ou nombre de colonnes incorrect") + INVALID_FORMAT, + + @Schema(description = "Erreur de validation des données") + VALIDATION_ERROR, + + @Schema(description = "Utilisateur déjà existant") + DUPLICATE_USER, + + @Schema(description = "Erreur lors de la création de l'utilisateur") + CREATION_ERROR, + + @Schema(description = "Erreur interne du système") + SYSTEM_ERROR + } +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/service/UserService.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/service/UserService.java index acbce8b..8e26274 100644 --- a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/service/UserService.java +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/service/UserService.java @@ -176,10 +176,10 @@ public interface UserService { String exportUsersToCSV(@NotNull UserSearchCriteriaDTO criteria); /** - * Importe des utilisateurs depuis un CSV + * Importe des utilisateurs depuis un CSV avec rapport détaillé * @param csvContent contenu CSV * @param realmName nom du realm - * @return nombre d'utilisateurs importés + * @return résultat détaillé de l'import (succès, erreurs) */ - int importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName); + dev.lions.user.manager.dto.importexport.ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName); } diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/resource/UserResource.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/resource/UserResource.java index bf51fa6..5f82741 100644 --- a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/resource/UserResource.java +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/resource/UserResource.java @@ -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.tags.Tag; +import java.time.LocalDateTime; 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 ==================== @Schema(description = "Requête de réinitialisation de mot de passe") diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java index 26a2827..2924283 100644 --- a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java @@ -2,8 +2,12 @@ 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.server.impl.entity.AuditLogEntity; +import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; import dev.lions.user.manager.service.AuditService; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -19,19 +23,49 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** - * Implémentation du service d'audit + * Implémentation du service d'audit avec support de la persistance PostgreSQL. * - * NOTES: - * - Cette implémentation utilise un stockage en mémoire pour le développement - * - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache) - * - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés + *

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 { - // Stockage en mémoire (à remplacer par une DB en production) - private final Map auditLogs = new ConcurrentHashMap<>(); + // ==================== DÉPENDANCES ==================== + + @Inject + AuditLogMapper auditLogMapper; + + // ==================== CONFIGURATION ==================== @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true") boolean auditEnabled; @@ -39,7 +73,24 @@ public class AuditServiceImpl implements AuditService { @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false") 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 public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) { if (!auditEnabled) { log.debug("Audit désactivé, log ignoré"); @@ -56,7 +107,7 @@ public class AuditServiceImpl implements AuditService { 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: {}", auditLog.getTypeAction(), auditLog.getActeurUsername(), @@ -65,15 +116,30 @@ public class AuditServiceImpl implements AuditService { auditLog.getIpAddress(), auditLog.getDescription()); - // Stocker en mémoire - auditLogs.put(auditLog.getId(), auditLog); + // Stocker en base de données si activé + 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 - // Exemple: - // if (logToDatabase) { - // AuditLogEntity entity = AuditLogMapper.toEntity(auditLog); - // entity.persist(); - // } + entity.persist(); + + log.debug("Log d'audit persisté en base de données avec ID: {}", entity.id); + } 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 + } + } + + // 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; } @@ -88,7 +154,7 @@ public class AuditServiceImpl implements AuditService { String description) { AuditLogDTO auditLog = AuditLogDTO.builder() .acteurUserId(acteurUserId) - .acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant + .acteurUsername(acteurUserId) .typeAction(typeAction) .ressourceType(ressourceType) .ressourceId(ressourceId != null ? ressourceId : "") @@ -111,7 +177,7 @@ public class AuditServiceImpl implements AuditService { String errorMessage) { AuditLogDTO auditLog = AuditLogDTO.builder() .acteurUserId(acteurUserId) - .acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant + .acteurUsername(acteurUserId) .typeAction(typeAction) .ressourceType(ressourceType) .ressourceId(ressourceId != null ? ressourceId : "") @@ -123,13 +189,18 @@ public class AuditServiceImpl implements AuditService { logAction(auditLog); } + // ==================== MÉTHODES DE RECHERCHE ==================== + @Override public List findByActeur(@NotBlank String acteurUserId, LocalDateTime dateDebut, LocalDateTime dateFin, int page, 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 @@ -139,7 +210,13 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, 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() .filter(log -> ressourceId.equals(log.getRessourceId())) .collect(Collectors.toList()); @@ -152,7 +229,10 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, 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 @@ -161,8 +241,11 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, int pageSize) { - // Pour l'instant, on retourne tous les logs car on n'a pas de champ realmName dans AuditLogDTO - return searchLogs(null, dateDebut, dateFin, null, null, null, page, pageSize); + if (logToDatabase) { + List entities = AuditLogEntity.findByRealm(realmName); + return auditLogMapper.toDTOList(entities); + } + return searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize); } @Override @@ -171,7 +254,10 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, 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 @@ -180,29 +266,22 @@ public class AuditServiceImpl implements AuditService { LocalDateTime dateFin, int page, int pageSize) { - // Les actions critiques sont USER_DELETE, ROLE_DELETE, etc. - return auditLogs.values().stream() + 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 || + return type == TypeActionAudit.USER_DELETE || type == TypeActionAudit.ROLE_DELETE || 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()); } + // ==================== MÉTHODES STATISTIQUES ==================== + @Override public Map countByActionType(@NotBlank String realmName, LocalDateTime dateDebut, @@ -223,13 +302,43 @@ public class AuditServiceImpl implements AuditService { 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); return result; } + @Override + public Map getAuditStatistics(@NotBlank String realmName, + LocalDateTime dateDebut, + LocalDateTime dateFin) { + Map 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 public String exportToCSV(@NotBlank String realmName, LocalDateTime dateDebut, @@ -239,159 +348,192 @@ public class AuditServiceImpl implements AuditService { } @Override + @Transactional public long purgeOldLogs(@NotNull LocalDateTime dateLimite) { - long beforeCount = auditLogs.size(); - auditLogs.entrySet().removeIf(entry -> + long purgedCount = 0; + + // 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) ); - long afterCount = auditLogs.size(); - return beforeCount - afterCount; + long cacheRemoved = beforeCacheCount - auditLogsCache.size(); + + log.info("Supprimé {} logs du cache mémoire avant {}", cacheRemoved, dateLimite); + + return purgedCount + cacheRemoved; } - @Override - public Map getAuditStatistics(@NotBlank String realmName, - LocalDateTime dateDebut, - LocalDateTime dateFin) { - Map 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ÉTHODES PRIVÉES ==================== - // Méthode privée helper pour la recherche - private List searchLogs(String acteurUsername, LocalDateTime dateDebut, - LocalDateTime dateFin, TypeActionAudit typeAction, - String ressourceType, Boolean succes, - int page, int pageSize) { - log.debug("Recherche de logs d'audit: acteur={}, dateDebut={}, dateFin={}, typeAction={}, succes={}", - acteurUsername, dateDebut, dateFin, typeAction, succes); + /** + * Recherche les logs depuis le cache mémoire. + */ + private List searchLogsFromCache(String acteurUsername, LocalDateTime dateDebut, + LocalDateTime dateFin, TypeActionAudit typeAction, + String ressourceType, Boolean succes, + int page, int pageSize) { + log.debug("Recherche logs depuis cache mémoire"); - return auditLogs.values().stream() - .filter(log -> { - // Filtre par acteur (si spécifié et non "*") - 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 + return auditLogsCache.values().stream() + .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()); } - // Méthodes privées helper - private Map getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { - log.debug("Calcul des statistiques d'actions entre {} et {}", dateDebut, dateFin); + /** + * Recherche les logs depuis la base de données PostgreSQL. + */ + private List 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 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 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 getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { + if (logToDatabase) { + List entities = AuditLogEntity.findByPeriod(dateDebut, dateFin); + return entities.stream() + .collect(Collectors.groupingBy(AuditLogEntity::getAction, Collectors.counting())); + } + + return auditLogsCache.values().stream() .filter(log -> { - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - 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; }) - .collect(Collectors.groupingBy( - AuditLogDTO::getTypeAction, - Collectors.counting() - )); + .collect(Collectors.groupingBy(AuditLogDTO::getTypeAction, Collectors.counting())); } private Map getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { - log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin); + if (logToDatabase) { + List 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 -> { - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - 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; }) - .collect(Collectors.groupingBy( - AuditLogDTO::getActeurUsername, - Collectors.counting() - )); + .collect(Collectors.groupingBy(AuditLogDTO::getActeurUsername, Collectors.counting())); } 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 -> { - if (log.isSuccessful()) { - return false; // On ne compte que les échecs - } - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - 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; }) .count(); } 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 -> { - if (!log.isSuccessful()) { - return false; // On ne compte que les succès - } - if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { - return false; - } - 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; }) .count(); @@ -401,56 +543,68 @@ public class AuditServiceImpl implements AuditService { log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin); List 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"); - // Données - 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; - }) - .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction())) - .forEach(log -> { - String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"", - log.getId(), - log.getDateAction(), - log.getActeurUsername(), - log.getTypeAction(), - log.getRessourceType(), - log.getRessourceId(), - log.isSuccessful(), - log.getIpAddress() != null ? log.getIpAddress() : "", - log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "", - log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : "" - ); - csvLines.add(csvLine); - }); + List logs; + if (logToDatabase) { + List entities = AuditLogEntity.findByPeriod(dateDebut, dateFin); + logs = auditLogMapper.toDTOList(entities); + } else { + logs = 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; + }) + .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction())) + .collect(Collectors.toList()); + } + + logs.forEach(log -> { + String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"", + log.getId(), + log.getDateAction(), + log.getActeurUsername(), + log.getTypeAction(), + log.getRessourceType(), + log.getRessourceId(), + log.isSuccessful(), + log.getIpAddress() != null ? log.getIpAddress() : "", + log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "", + log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : "" + ); + csvLines.add(csvLine); + }); log.info("Export CSV terminé: {} lignes", csvLines.size() - 1); 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() { - 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() { - log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire"); - auditLogs.clear(); + log.warn("ATTENTION: Suppression de tous les logs d'audit"); + + 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"); } } diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java new file mode 100644 index 0000000..a0c4ac3 --- /dev/null +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/CsvValidationHelper.java @@ -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(); + } +} diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java index 1dd03bf..335e37f 100644 --- a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java @@ -608,11 +608,17 @@ public class UserServiceImpl implements UserService { } @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); + 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"); - int count = 0; int startIndex = 0; // Skip header if present @@ -620,48 +626,159 @@ public class UserServiceImpl implements UserService { startIndex = 1; } + result.setTotalLines(lines.length - startIndex); + for (int i = startIndex; i < lines.length; i++) { + int lineNumber = i + 1; String line = lines[i].trim(); - if (line.isEmpty()) - continue; + + if (line.isEmpty()) { + continue; // Ignore empty lines + } try { + // Parse CSV line String[] parts = parseCSVLine(line); 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; } - String username = parts[0]; - String email = parts[1]; - String firstName = parts[2]; - String lastName = parts[3]; - boolean enabled = Boolean.parseBoolean(parts[4]); + String username = CsvValidationHelper.clean(parts[0]); + String email = CsvValidationHelper.clean(parts[1]); + String firstName = CsvValidationHelper.clean(parts[2]); + String lastName = CsvValidationHelper.clean(parts[3]); + String enabledStr = CsvValidationHelper.clean(parts[4]); - if (username == null || username.isBlank()) { - log.warn("Username manquant à la ligne {}", i + 1); + // Validate username + 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; } + // 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 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() - .username(username) - .email(email.isBlank() ? null : email) - .prenom(firstName.isBlank() ? null : firstName) - .nom(lastName.isBlank() ? null : lastName) - .enabled(enabled) - .build(); + .username(username) + .email(email) + .prenom(firstName) + .nom(lastName) + .enabled(enabled) + .build(); - createUser(userDTO, realmName); - count++; + try { + 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) { - log.error("Erreur lors de l'import de la ligne {}: {}", i + 1, e.getMessage()); - // Continue with next line + log.error("Erreur inattendue lors du traitement de la ligne {}: {}", lineNumber, e.getMessage(), 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.SYSTEM_ERROR) + .message("Erreur système") + .details(e.getMessage()) + .build()); } } - log.info("✅ {} utilisateurs importés avec succès", count); - return count; + // Generate summary message + result.generateMessage(); + log.info(result.getMessage()); + + return result; } private String escape(String data) { diff --git a/lions-user-manager-server-impl-quarkus/src/main/resources/db/migration/V1.0.0__Create_audit_logs_table.sql b/lions-user-manager-server-impl-quarkus/src/main/resources/db/migration/V1.0.0__Create_audit_logs_table.sql new file mode 100644 index 0000000..351acae --- /dev/null +++ b/lions-user-manager-server-impl-quarkus/src/main/resources/db/migration/V1.0.0__Create_audit_logs_table.sql @@ -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 +-- ============================================================================= diff --git a/lions-user-manager-server-impl-quarkus/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java b/lions-user-manager-server-impl-quarkus/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java index e315dca..f089927 100644 --- a/lions-user-manager-server-impl-quarkus/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java +++ b/lions-user-manager-server-impl-quarkus/src/test/java/dev/lions/user/manager/service/impl/UserServiceImplTest.java @@ -220,12 +220,15 @@ class UserServiceImplTest { lenient().doNothing().when(userResource) .resetPassword(any(org.keycloak.representations.idm.CredentialRepresentation.class)); - String csvContent = "username,email,firstName,lastName,enabled\n" + - "imported,imp@test.com,Imp,Orter,true"; + String csvContent = "username,prenom,nom,email\n" + + "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"))); } } diff --git a/pom.xml b/pom.xml index 1446be4..f72d289 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 3.15.1 3.15.1 14.0.5 - 1.0.0-SNAPSHOT + 1.0.0 26.0.4 1.18.30 1.5.5.Final