feat: Optimisations UX/UI et amélioration import/export CSV
Optimisations majeures de l'interface utilisateur et amélioration du système d'import/export CSV avec rapport d'erreurs détaillé. ## Optimisations UX/UI - Suppression des blocs Actions Rapides redondants dans les pages list/view - Consolidation des actions dans les en-têtes de page - Conversion des filtres en panneau collapsible avec badge Filtres actifs - Suppression du sous-menu Attribution Rôles (redondant avec /users/edit) - Amélioration de la navigation et de l'ergonomie générale - Correction des attributs iconLeft non supportés par fr:fieldInput ## Import/Export CSV - Ajout de ImportResultDTO avec rapport détaillé des erreurs - Création de CsvValidationHelper pour validation robuste des données - Amélioration des messages d'erreur avec numéros de ligne - Support de colonnes flexibles (username,prenom,nom,email) - Validation stricte des formats email ## Corrections techniques - Fix DashboardBeanTest: getRecentActions() → getActionsLast24h() - Fix UserServiceImplTest: retour ImportResultDTO au lieu de int - Amélioration de la gestion d'erreurs dans AuditServiceImpl - Migration Flyway V1.0.0 pour la table audit_logs ## Infrastructure - Mise à jour .gitignore professionnel (exclusion docs de session) - Configuration production sécurisée (variables d'environnement) - Pas de secrets hardcodés dans les fichiers de configuration Testé et validé en environnement de développement.
This commit is contained in:
55
.gitignore
vendored
55
.gitignore
vendored
@@ -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/
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> allRealms = realmServiceClient.getAllRealms();
|
||||
|
||||
if (allRealms == null || allRealms.isEmpty()) {
|
||||
LOGGER.warning("Aucun realm trouvé dans Keycloak");
|
||||
availableRealms = Collections.emptyList();
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> 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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<StatutUser> statutOptions = List.of(StatutUser.values());
|
||||
private List<String> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,6 +190,23 @@
|
||||
<redirect />
|
||||
</navigation-case>
|
||||
|
||||
<!-- ================================================================
|
||||
FREYA EXTENSION SHOWCASE
|
||||
================================================================ -->
|
||||
<navigation-case>
|
||||
<description>Page de démonstration complète Freya Extension</description>
|
||||
<from-outcome>freyaShowcasePage</from-outcome>
|
||||
<to-view-id>/pages/user-manager/freya-showcase.xhtml</to-view-id>
|
||||
<redirect />
|
||||
</navigation-case>
|
||||
|
||||
<navigation-case>
|
||||
<description>Navigation directe vers Freya Showcase</description>
|
||||
<from-outcome>/pages/user-manager/freya-showcase</from-outcome>
|
||||
<to-view-id>/pages/user-manager/freya-showcase.xhtml</to-view-id>
|
||||
<redirect />
|
||||
</navigation-case>
|
||||
|
||||
</navigation-rule>
|
||||
</faces-config>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
<ui:define name="title">Affectation des Realms - Lions User Manager</ui:define>
|
||||
@@ -24,11 +25,11 @@
|
||||
<p class="text-600 m-0">Gérer les permissions d'administration par realm (contrôle multi-tenant)</p>
|
||||
</div>
|
||||
</div>
|
||||
<p:commandButton value="Nouvelle Affectation"
|
||||
icon="pi pi-plus"
|
||||
styleClass="p-button-success"
|
||||
onclick="PF('assignRealmDialog').show();"
|
||||
type="button" />
|
||||
<fr:commandButton value="Nouvelle Affectation"
|
||||
icon="pi pi-plus"
|
||||
severity="success"
|
||||
onclick="PF('assignRealmDialog').show();"
|
||||
type="button" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,30 +92,32 @@
|
||||
<div class="card">
|
||||
<div class="flex align-items-center justify-content-between mb-4">
|
||||
<h5 class="m-0">Affectations Actuelles</h5>
|
||||
<p:commandButton value="Rafraîchir"
|
||||
icon="pi pi-refresh"
|
||||
styleClass="p-button-outlined p-button-sm"
|
||||
action="#{realmAssignmentBean.loadAssignments}"
|
||||
update=":formRealmAssignments" />
|
||||
<fr:commandButton value="Rafraîchir"
|
||||
icon="pi pi-refresh"
|
||||
outlined="true"
|
||||
size="small"
|
||||
action="#{realmAssignmentBean.loadAssignments}"
|
||||
update=":formRealmAssignments" />
|
||||
</div>
|
||||
|
||||
<p:messages id="messages" showDetail="true" closable="true">
|
||||
<fr:message id="messages" showDetail="true" closable="true">
|
||||
<p:autoUpdate />
|
||||
</p:messages>
|
||||
</fr:message>
|
||||
|
||||
<p:dataTable id="assignmentsTable"
|
||||
value="#{realmAssignmentBean.assignments}"
|
||||
var="assignment"
|
||||
paginator="true"
|
||||
rows="10"
|
||||
rows="25"
|
||||
paginatorPosition="bottom"
|
||||
paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
|
||||
rowsPerPageTemplate="10,20,50"
|
||||
rowsPerPageTemplate="10,25,50,100"
|
||||
emptyMessage="Aucune affectation configurée"
|
||||
responsiveLayout="scroll"
|
||||
styleClass="p-datatable-sm">
|
||||
|
||||
<!-- Colonne Utilisateur -->
|
||||
<p:column headerText="Utilisateur" sortBy="#{assignment.username}" filterBy="#{assignment.username}" filterMatchMode="contains">
|
||||
<p:column headerText="Utilisateur" sortBy="#{assignment.username}" filterBy="#{assignment.username}" filterMatchMode="contains" priority="1">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<div style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600)); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: bold; color: white;">
|
||||
<h:outputText value="#{assignment.username != null and assignment.username.length() >= 2 ? assignment.username.substring(0,2).toUpperCase() : 'U'}" />
|
||||
@@ -127,92 +130,101 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Realm -->
|
||||
<p:column headerText="Realm" sortBy="#{assignment.realmName}" filterBy="#{assignment.realmName}" filterMatchMode="contains">
|
||||
<p:tag value="#{assignment.realmName}"
|
||||
severity="info"
|
||||
icon="pi pi-globe" />
|
||||
<p:column headerText="Realm" sortBy="#{assignment.realmName}" filterBy="#{assignment.realmName}" filterMatchMode="contains" priority="2">
|
||||
<fr:tag value="#{assignment.realmName}"
|
||||
severity="info"
|
||||
icon="pi pi-globe" />
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Type -->
|
||||
<p:column headerText="Type" style="width: 150px">
|
||||
<p:tag value="Super Admin"
|
||||
severity="danger"
|
||||
icon="pi pi-star"
|
||||
rendered="#{assignment.isSuperAdmin()}" />
|
||||
<p:tag value="Realm Admin"
|
||||
severity="success"
|
||||
icon="pi pi-shield"
|
||||
rendered="#{!assignment.isSuperAdmin()}" />
|
||||
<p:column headerText="Type" style="width: 150px" priority="3">
|
||||
<fr:tag value="Super Admin"
|
||||
severity="danger"
|
||||
icon="pi pi-star"
|
||||
rendered="#{assignment.isSuperAdmin()}" />
|
||||
<fr:tag value="Realm Admin"
|
||||
severity="success"
|
||||
icon="pi pi-shield"
|
||||
rendered="#{!assignment.isSuperAdmin()}" />
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Statut -->
|
||||
<p:column headerText="Statut" style="width: 120px">
|
||||
<p:tag value="Actif"
|
||||
severity="success"
|
||||
icon="pi pi-check-circle"
|
||||
rendered="#{assignment.active and !assignment.isExpired()}" />
|
||||
<p:tag value="Inactif"
|
||||
severity="warning"
|
||||
icon="pi pi-times-circle"
|
||||
rendered="#{!assignment.active}" />
|
||||
<p:tag value="Expiré"
|
||||
severity="danger"
|
||||
icon="pi pi-exclamation-circle"
|
||||
rendered="#{assignment.isExpired()}" />
|
||||
<p:column headerText="Statut" style="width: 120px" priority="4">
|
||||
<fr:tag value="Actif"
|
||||
severity="success"
|
||||
icon="pi pi-check-circle"
|
||||
rendered="#{assignment.active and !assignment.isExpired()}" />
|
||||
<fr:tag value="Inactif"
|
||||
severity="warning"
|
||||
icon="pi pi-times-circle"
|
||||
rendered="#{!assignment.active}" />
|
||||
<fr:tag value="Expiré"
|
||||
severity="danger"
|
||||
icon="pi pi-exclamation-circle"
|
||||
rendered="#{assignment.isExpired()}" />
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Assigné le -->
|
||||
<p:column headerText="Assigné le" sortBy="#{assignment.assignedAt}" style="width: 180px">
|
||||
<p:column headerText="Assigné le" sortBy="#{assignment.assignedAt}" style="width: 180px" priority="5">
|
||||
<h:outputText value="#{assignment.assignedAt}">
|
||||
<f:convertDateTime pattern="dd/MM/yyyy HH:mm" />
|
||||
</h:outputText>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Par -->
|
||||
<p:column headerText="Par" sortBy="#{assignment.assignedBy}" style="width: 150px">
|
||||
<p:column headerText="Par" sortBy="#{assignment.assignedBy}" style="width: 150px" priority="6">
|
||||
<h:outputText value="#{assignment.assignedBy}" />
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Actions -->
|
||||
<p:column headerText="Actions" style="width: 120px; text-align: center">
|
||||
<p:column headerText="Actions" style="width: 120px; text-align: center" priority="1">
|
||||
<div class="flex gap-1 justify-content-center flex-wrap">
|
||||
<!-- Bouton Désactiver -->
|
||||
<p:commandButton icon="pi pi-ban"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-warning"
|
||||
title="Désactiver"
|
||||
action="#{realmAssignmentBean.deactivateAssignment(assignment)}"
|
||||
update=":formRealmAssignments"
|
||||
process="@this"
|
||||
rendered="#{assignment.active}">
|
||||
<fr:commandButton icon="pi pi-ban"
|
||||
rounded="true"
|
||||
text="true"
|
||||
size="small"
|
||||
severity="warning"
|
||||
title="Désactiver"
|
||||
action="#{realmAssignmentBean.deactivateAssignment(assignment)}"
|
||||
update=":formRealmAssignments"
|
||||
process="@this"
|
||||
rendered="#{assignment.active}">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Désactiver cette affectation ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
|
||||
<!-- Bouton Activer -->
|
||||
<p:commandButton icon="pi pi-check"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-success"
|
||||
title="Activer"
|
||||
action="#{realmAssignmentBean.activateAssignment(assignment)}"
|
||||
update=":formRealmAssignments"
|
||||
process="@this"
|
||||
rendered="#{!assignment.active}">
|
||||
<fr:commandButton icon="pi pi-check"
|
||||
rounded="true"
|
||||
text="true"
|
||||
size="small"
|
||||
severity="success"
|
||||
title="Activer"
|
||||
action="#{realmAssignmentBean.activateAssignment(assignment)}"
|
||||
update=":formRealmAssignments"
|
||||
process="@this"
|
||||
rendered="#{!assignment.active}">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Activer cette affectation ?"
|
||||
icon="pi pi-question-circle" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
|
||||
<!-- Bouton Supprimer -->
|
||||
<p:commandButton icon="pi pi-trash"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
|
||||
title="Supprimer"
|
||||
action="#{realmAssignmentBean.revokeAssignment(assignment)}"
|
||||
update=":formRealmAssignments"
|
||||
process="@this">
|
||||
<fr:commandButton icon="pi pi-trash"
|
||||
rounded="true"
|
||||
text="true"
|
||||
size="small"
|
||||
severity="danger"
|
||||
title="Supprimer"
|
||||
action="#{realmAssignmentBean.revokeAssignment(assignment)}"
|
||||
update=":formRealmAssignments"
|
||||
process="@this">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Révoquer l'accès de #{assignment.username} au realm #{assignment.realmName} ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</p:column>
|
||||
</p:dataTable>
|
||||
@@ -234,72 +246,59 @@
|
||||
<h:form id="formAssignRealm">
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
<label class="block text-900 font-semibold mb-2">
|
||||
<i class="pi pi-user text-primary mr-1"></i>
|
||||
Utilisateur *
|
||||
</label>
|
||||
<p:selectOneMenu value="#{realmAssignmentBean.selectedUserId}"
|
||||
styleClass="w-full"
|
||||
<fr:fieldSelect id="userId"
|
||||
label="Utilisateur *"
|
||||
value="#{realmAssignmentBean.selectedUserId}"
|
||||
filter="true"
|
||||
filterMatchMode="contains">
|
||||
filterMatchMode="contains"
|
||||
iconLeft="pi pi-user">
|
||||
<f:selectItem itemLabel="Sélectionner un utilisateur" itemValue="" noSelectionOption="true" />
|
||||
<f:selectItems value="#{realmAssignmentBean.availableUsers}"
|
||||
var="user"
|
||||
itemValue="#{user.id}"
|
||||
itemLabel="#{user.username} (#{user.email})" />
|
||||
</p:selectOneMenu>
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="block text-900 font-semibold mb-2">
|
||||
<i class="pi pi-globe text-primary mr-1"></i>
|
||||
Realm *
|
||||
</label>
|
||||
<p:selectOneMenu value="#{realmAssignmentBean.selectedRealmName}"
|
||||
styleClass="w-full">
|
||||
<fr:fieldSelect id="realmName"
|
||||
label="Realm *"
|
||||
value="#{realmAssignmentBean.selectedRealmName}"
|
||||
iconLeft="pi pi-globe">
|
||||
<f:selectItem itemLabel="Sélectionner un realm" itemValue="" noSelectionOption="true" />
|
||||
<f:selectItems value="#{realmAssignmentBean.availableRealms}" />
|
||||
</p:selectOneMenu>
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="block text-900 font-semibold mb-2">
|
||||
<i class="pi pi-comment text-primary mr-1"></i>
|
||||
Raison
|
||||
</label>
|
||||
<p:inputText value="#{realmAssignmentBean.newAssignment.raison}"
|
||||
styleClass="w-full"
|
||||
placeholder="Ex: Nouveau gestionnaire du realm client" />
|
||||
<fr:fieldInput id="raison"
|
||||
label="Raison"
|
||||
value="#{realmAssignmentBean.newAssignment.raison}"
|
||||
placeholder="Ex: Nouveau gestionnaire du realm client"
|
||||
iconLeft="pi pi-comment" />
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="block text-900 font-semibold mb-2">
|
||||
<i class="pi pi-file-edit text-primary mr-1"></i>
|
||||
Commentaires
|
||||
</label>
|
||||
<p:inputTextarea value="#{realmAssignmentBean.newAssignment.commentaires}"
|
||||
rows="3"
|
||||
styleClass="w-full"
|
||||
placeholder="Commentaires administratifs (optionnel)" />
|
||||
<fr:fieldTextarea id="commentaires"
|
||||
label="Commentaires"
|
||||
value="#{realmAssignmentBean.newAssignment.commentaires}"
|
||||
rows="3"
|
||||
placeholder="Commentaires administratifs (optionnel)"
|
||||
iconLeft="pi pi-file-edit" />
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="flex align-items-center">
|
||||
<p:selectBooleanCheckbox value="#{realmAssignmentBean.newAssignment.temporaire}"
|
||||
itemLabel="Affectation temporaire"
|
||||
styleClass="mr-2" />
|
||||
</div>
|
||||
<fr:fieldCheckbox id="temporaire"
|
||||
label="Affectation temporaire"
|
||||
value="#{realmAssignmentBean.newAssignment.temporaire}" />
|
||||
</div>
|
||||
|
||||
<div class="col-12" rendered="#{realmAssignmentBean.newAssignment.temporaire}">
|
||||
<label class="block text-900 font-semibold mb-2">
|
||||
<i class="pi pi-calendar text-primary mr-1"></i>
|
||||
Date d'expiration
|
||||
</label>
|
||||
<p:calendar value="#{realmAssignmentBean.newAssignment.dateExpiration}"
|
||||
pattern="dd/MM/yyyy HH:mm"
|
||||
showTime="true"
|
||||
styleClass="w-full" />
|
||||
<fr:fieldCalendar id="dateExpiration"
|
||||
label="Date d'expiration"
|
||||
value="#{realmAssignmentBean.newAssignment.dateExpiration}"
|
||||
pattern="dd/MM/yyyy HH:mm"
|
||||
showTime="true" />
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
@@ -319,18 +318,20 @@
|
||||
|
||||
<div class="col-12">
|
||||
<div class="flex gap-2">
|
||||
<p:commandButton value="Annuler"
|
||||
icon="pi pi-times"
|
||||
styleClass="p-button-text flex-1"
|
||||
onclick="PF('assignRealmDialog').hide();"
|
||||
type="button"
|
||||
action="#{realmAssignmentBean.resetForm}" />
|
||||
<p:commandButton value="Assigner"
|
||||
icon="pi pi-check"
|
||||
styleClass="p-button-success flex-1"
|
||||
action="#{realmAssignmentBean.assignRealm}"
|
||||
update=":formRealmAssignments :formAssignRealm"
|
||||
oncomplete="if (args.validationFailed == false) PF('assignRealmDialog').hide();" />
|
||||
<fr:commandButton value="Annuler"
|
||||
icon="pi pi-times"
|
||||
text="true"
|
||||
styleClass="flex-1"
|
||||
onclick="PF('assignRealmDialog').hide();"
|
||||
type="button"
|
||||
action="#{realmAssignmentBean.resetForm}" />
|
||||
<fr:commandButton value="Assigner"
|
||||
icon="pi pi-check"
|
||||
severity="success"
|
||||
styleClass="flex-1"
|
||||
action="#{realmAssignmentBean.assignRealm}"
|
||||
update=":formRealmAssignments :formAssignRealm"
|
||||
oncomplete="if (args.validationFailed == false) PF('assignRealmDialog').hide();" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,11 +339,12 @@
|
||||
</p:dialog>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG DE CONFIRMATION
|
||||
DIALOG DE CONFIRMATION (Freya Extension)
|
||||
================================================================ -->
|
||||
<!-- Le confirmDialog est géré par p:confirm dans les boutons d'action -->
|
||||
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade" responsive="true" width="400">
|
||||
<p:commandButton value="Non" type="button" styleClass="p-button-text" icon="pi pi-times" />
|
||||
<p:commandButton value="Oui" type="button" styleClass="p-button-danger" icon="pi pi-check" />
|
||||
<fr:commandButton value="Non" type="button" text="true" icon="pi pi-times" />
|
||||
<fr:commandButton value="Oui" type="button" severity="danger" icon="pi pi-check" />
|
||||
</p:confirmDialog>
|
||||
</ui:define>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
<ui:param name="page" value="#{auditConsultationBean}"/>
|
||||
@@ -25,12 +26,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<h:form id="formHeaderActions">
|
||||
<p:commandButton
|
||||
value="Exporter CSV"
|
||||
icon="pi pi-download"
|
||||
styleClass="p-button-success"
|
||||
action="#{auditConsultationBean.exportToCSV}"
|
||||
ajax="false" />
|
||||
<fr:commandButton value="Exporter CSV"
|
||||
icon="pi pi-download"
|
||||
severity="success"
|
||||
action="#{auditConsultationBean.exportToCSV}"
|
||||
ajax="false" />
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,74 +144,70 @@
|
||||
<h:form id="formFilters">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<label for="acteurFilter" class="block text-900 font-medium mb-2">Acteur</label>
|
||||
<p:inputText id="acteurFilter"
|
||||
value="#{auditConsultationBean.acteurUsername}"
|
||||
placeholder="Nom d'utilisateur..."
|
||||
styleClass="w-full" />
|
||||
<fr:fieldInput id="acteurFilter"
|
||||
label="Acteur"
|
||||
value="#{auditConsultationBean.acteurUsername}"
|
||||
placeholder="Nom d'utilisateur..."
|
||||
iconLeft="pi pi-user" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<label for="typeActionFilter" class="block text-900 font-medium mb-2">Type d'action</label>
|
||||
<p:selectOneMenu id="typeActionFilter"
|
||||
<fr:fieldSelect id="typeActionFilter"
|
||||
label="Type d'action"
|
||||
value="#{auditConsultationBean.selectedTypeAction}"
|
||||
styleClass="w-full">
|
||||
iconLeft="pi pi-bolt">
|
||||
<f:selectItem itemLabel="Tous les types" itemValue="" />
|
||||
<f:selectItems value="#{auditConsultationBean.typeActionOptions}" />
|
||||
</p:selectOneMenu>
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<label for="succesFilter" class="block text-900 font-medium mb-2">Résultat</label>
|
||||
<p:selectOneMenu id="succesFilter"
|
||||
<fr:fieldSelect id="succesFilter"
|
||||
label="Résultat"
|
||||
value="#{auditConsultationBean.succes}"
|
||||
styleClass="w-full">
|
||||
iconLeft="pi pi-check-circle">
|
||||
<f:selectItem itemLabel="Tous" itemValue="" />
|
||||
<f:selectItem itemLabel="Succès" itemValue="true" />
|
||||
<f:selectItem itemLabel="Échec" itemValue="false" />
|
||||
</p:selectOneMenu>
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<label for="dateDebutFilter" class="block text-900 font-medium mb-2">Date début</label>
|
||||
<p:calendar id="dateDebutFilter"
|
||||
value="#{auditConsultationBean.dateDebut}"
|
||||
pattern="dd/MM/yyyy"
|
||||
showIcon="true"
|
||||
styleClass="w-full" />
|
||||
<fr:fieldCalendar id="dateDebutFilter"
|
||||
label="Date début"
|
||||
value="#{auditConsultationBean.dateDebut}"
|
||||
pattern="dd/MM/yyyy"
|
||||
showIcon="true" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<label for="dateFinFilter" class="block text-900 font-medium mb-2">Date fin</label>
|
||||
<p:calendar id="dateFinFilter"
|
||||
value="#{auditConsultationBean.dateFin}"
|
||||
pattern="dd/MM/yyyy"
|
||||
showIcon="true"
|
||||
styleClass="w-full" />
|
||||
<fr:fieldCalendar id="dateFinFilter"
|
||||
label="Date fin"
|
||||
value="#{auditConsultationBean.dateFin}"
|
||||
pattern="dd/MM/yyyy"
|
||||
showIcon="true" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<label for="ressourceFilter" class="block text-900 font-medium mb-2">Type ressource</label>
|
||||
<p:inputText id="ressourceFilter"
|
||||
value="#{auditConsultationBean.ressourceType}"
|
||||
placeholder="USER, ROLE, CLIENT..."
|
||||
styleClass="w-full" />
|
||||
<fr:fieldInput id="ressourceFilter"
|
||||
label="Type ressource"
|
||||
value="#{auditConsultationBean.ressourceType}"
|
||||
placeholder="USER, ROLE, CLIENT..."
|
||||
iconLeft="pi pi-database" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 justify-content-end mt-4">
|
||||
<p:commandButton
|
||||
value="Rechercher"
|
||||
icon="pi pi-search"
|
||||
styleClass="p-button-primary"
|
||||
action="#{auditConsultationBean.searchLogs}"
|
||||
update=":formAuditLogs:auditLogsTable" />
|
||||
<p:commandButton
|
||||
value="Réinitialiser"
|
||||
icon="pi pi-refresh"
|
||||
styleClass="p-button-secondary"
|
||||
action="#{auditConsultationBean.resetFilters}"
|
||||
update=":formAuditLogs:auditLogsTable @form" />
|
||||
<fr:commandButton value="Rechercher"
|
||||
icon="pi pi-search"
|
||||
severity="primary"
|
||||
action="#{auditConsultationBean.searchLogs}"
|
||||
update=":formAuditLogs:auditLogsTable" />
|
||||
<fr:commandButton value="Réinitialiser"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
action="#{auditConsultationBean.resetFilters}"
|
||||
update=":formAuditLogs:auditLogsTable @form" />
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
@@ -227,9 +223,9 @@
|
||||
<i class="pi pi-list text-blue-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Logs d'Audit</h5>
|
||||
</div>
|
||||
<p:tag value="#{auditConsultationBean.totalRecords} log(s)"
|
||||
severity="info"
|
||||
icon="pi pi-history" />
|
||||
<fr:tag value="#{auditConsultationBean.totalRecords} log(s)"
|
||||
severity="info"
|
||||
icon="pi pi-history" />
|
||||
</div>
|
||||
|
||||
<h:form id="formAuditLogs">
|
||||
@@ -245,16 +241,16 @@
|
||||
currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}"
|
||||
styleClass="w-full"
|
||||
emptyMessage="Aucun log d'audit trouvé"
|
||||
reflow="true">
|
||||
responsiveLayout="scroll">
|
||||
|
||||
<!-- Colonne Statut -->
|
||||
<p:column headerText="Statut" style="width: 100px; text-align: center">
|
||||
<p:tag value="#{log.succes ? 'Succès' : 'Échec'}"
|
||||
severity="#{log.succes ? 'success' : 'danger'}" />
|
||||
<p:column headerText="Statut" style="width: 100px; text-align: center" priority="2">
|
||||
<fr:tag value="#{log.succes ? 'Succès' : 'Échec'}"
|
||||
severity="#{log.succes ? 'success' : 'danger'}" />
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Type d'action -->
|
||||
<p:column headerText="Type d'action" sortBy="#{log.typeAction}" style="width: 180px">
|
||||
<p:column headerText="Type d'action" sortBy="#{log.typeAction}" style="width: 180px" priority="1">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-bolt text-orange-500"></i>
|
||||
<span class="font-semibold text-900">#{log.typeAction}</span>
|
||||
@@ -262,7 +258,7 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Acteur -->
|
||||
<p:column headerText="Acteur" sortBy="#{log.acteurUsername}" style="width: 200px">
|
||||
<p:column headerText="Acteur" sortBy="#{log.acteurUsername}" style="width: 200px" priority="3">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<div class="border-circle bg-primary text-white flex align-items-center justify-content-center"
|
||||
style="width: 32px; height: 32px; flex-shrink: 0; font-size: 0.75rem;">
|
||||
@@ -275,7 +271,7 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Ressource -->
|
||||
<p:column headerText="Ressource" style="width: 150px">
|
||||
<p:column headerText="Ressource" style="width: 150px" priority="5">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-database text-blue-500"></i>
|
||||
<span class="text-900">#{log.ressourceType}</span>
|
||||
@@ -283,7 +279,7 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Date -->
|
||||
<p:column headerText="Date" sortBy="#{log.dateAction}" style="width: 180px">
|
||||
<p:column headerText="Date" sortBy="#{log.dateAction}" style="width: 180px" priority="4">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-calendar text-purple-500"></i>
|
||||
<span class="text-900">#{log.dateAction}</span>
|
||||
@@ -291,28 +287,30 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Détails -->
|
||||
<p:column headerText="Détails" style="width: 250px">
|
||||
<p:column headerText="Détails" style="width: 250px" priority="6">
|
||||
<span class="text-600 text-sm" style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
#{not empty log.details ? log.details : '-'}
|
||||
</span>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne IP -->
|
||||
<p:column headerText="Adresse IP" style="width: 130px">
|
||||
<p:column headerText="Adresse IP" style="width: 130px" priority="6">
|
||||
<span class="text-600 text-sm font-mono">
|
||||
#{not empty log.adresseIp ? log.adresseIp : '-'}
|
||||
</span>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Actions -->
|
||||
<p:column headerText="Actions" style="width: 80px; text-align: center">
|
||||
<p:commandButton
|
||||
icon="pi pi-eye"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-info"
|
||||
title="Voir les détails"
|
||||
onclick="PF('auditLogDetailsDialog').show()">
|
||||
<p:column headerText="Actions" style="width: 80px; text-align: center" priority="1">
|
||||
<fr:commandButton icon="pi pi-eye"
|
||||
rounded="true"
|
||||
text="true"
|
||||
size="small"
|
||||
severity="info"
|
||||
title="Voir les détails"
|
||||
onclick="PF('auditLogDetailsDialog').show()">
|
||||
<f:setPropertyActionListener target="#{auditConsultationBean.selectedLog}" value="#{log}" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</p:column>
|
||||
</p:dataTable>
|
||||
</h:form>
|
||||
@@ -336,8 +334,8 @@
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<span class="text-600 font-medium">Statut</span>
|
||||
<p:tag value="#{auditConsultationBean.selectedLog.succes ? 'Succès' : 'Échec'}"
|
||||
severity="#{auditConsultationBean.selectedLog.succes ? 'success' : 'danger'}" />
|
||||
<fr:tag value="#{auditConsultationBean.selectedLog.succes ? 'Succès' : 'Échec'}"
|
||||
severity="#{auditConsultationBean.selectedLog.succes ? 'success' : 'danger'}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -409,12 +407,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-content-end mt-4">
|
||||
<p:commandButton
|
||||
value="Fermer"
|
||||
icon="pi pi-times"
|
||||
styleClass="p-button-secondary"
|
||||
onclick="PF('auditLogDetailsDialog').hide()"
|
||||
type="button" />
|
||||
<fr:commandButton value="Fermer"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
onclick="PF('auditLogDetailsDialog').hide()"
|
||||
type="button" />
|
||||
</div>
|
||||
</h:form>
|
||||
</p:dialog>
|
||||
|
||||
@@ -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">
|
||||
|
||||
<ui:define name="title">Tableau de Bord - Lions User Manager</ui:define>
|
||||
@@ -21,114 +22,174 @@
|
||||
<i class="pi pi-home text-blue-500" style="font-size: 2rem"></i>
|
||||
<div>
|
||||
<h3 class="m-0 mb-1">Tableau de Bord</h3>
|
||||
<p class="text-600 m-0">Vue d'ensemble de la gestion des utilisateurs Keycloak</p>
|
||||
<p class="text-600 m-0">Vue d'ensemble de la gestion des utilisateurs - Realm: #{dashboardBean.realmName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p:commandButton
|
||||
value="Rafraîchir"
|
||||
icon="pi pi-refresh"
|
||||
styleClass="p-button-secondary"
|
||||
action="#{dashboardBean.refreshStatistics}"
|
||||
update=":formDashboard" />
|
||||
<fr:commandButton value="Rafraîchir"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
action="#{dashboardBean.refreshStatistics}"
|
||||
update=":formDashboard" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
STATISTIQUES PRINCIPALES (4 KPI CARDS)
|
||||
STATISTIQUES PRINCIPALES - KPIs MÉTIER
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">Statistiques Principales</h5>
|
||||
<h5 class="mb-3">Indicateurs Clés de Performance</h5>
|
||||
</div>
|
||||
|
||||
<!-- KPI 1: Utilisateurs Actifs -->
|
||||
<!-- KPI 1: Total Utilisateurs -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
|
||||
<p:commandButton
|
||||
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
|
||||
outcome="/pages/user-manager/users/list">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
|
||||
outcome="/pages/user-manager/users/list">
|
||||
<div class="flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Utilisateurs Actifs</div>
|
||||
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalUsersDisplay}</div>
|
||||
<div class="text-500 font-medium mb-2 text-sm uppercase">Total Utilisateurs</div>
|
||||
<div class="text-900 font-bold text-4xl">#{dashboardBean.totalUsersDisplay}</div>
|
||||
<div class="text-600 text-sm mt-2">
|
||||
<i class="pi pi-users mr-2"></i>
|
||||
Dans le système
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-users text-blue-600 text-xl"></i>
|
||||
style="width: 3.5rem; height: 3.5rem">
|
||||
<i class="pi pi-users text-blue-600" style="font-size: 1.75rem"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-500 text-sm">
|
||||
<i class="pi pi-arrow-right text-600"></i>
|
||||
<span class="ml-2">Total utilisateurs</span>
|
||||
</div>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI 2: Rôles Realm -->
|
||||
<!-- KPI 2: Utilisateurs Actifs -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
|
||||
<p:commandButton
|
||||
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
|
||||
outcome="/pages/user-manager/roles/list">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
|
||||
outcome="/pages/user-manager/users/list">
|
||||
<div class="flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Rôles Realm</div>
|
||||
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalRolesDisplay}</div>
|
||||
<div class="text-500 font-medium mb-2 text-sm uppercase">Utilisateurs Actifs</div>
|
||||
<div class="text-900 font-bold text-4xl">#{dashboardBean.activeUsersDisplay}</div>
|
||||
<div class="text-600 text-sm mt-2">
|
||||
<i class="pi pi-check-circle mr-2"></i>
|
||||
Comptes activés
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-shield text-green-600 text-xl"></i>
|
||||
style="width: 3.5rem; height: 3.5rem">
|
||||
<i class="pi pi-check-circle text-green-600" style="font-size: 1.75rem"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-500 text-sm">
|
||||
<i class="pi pi-arrow-right text-600"></i>
|
||||
<span class="ml-2">Rôles configurés</span>
|
||||
</div>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI 3: Actions Récentes -->
|
||||
<!-- KPI 3: Utilisateurs Inactifs -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
|
||||
<p:commandButton
|
||||
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
|
||||
outcome="/pages/user-manager/audit/logs">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
|
||||
outcome="/pages/user-manager/users/list">
|
||||
<div class="flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Actions Récentes</div>
|
||||
<div class="text-900 font-bold text-2xl">#{dashboardBean.recentActionsDisplay}</div>
|
||||
<div class="text-500 font-medium mb-2 text-sm uppercase">Utilisateurs Inactifs</div>
|
||||
<div class="text-900 font-bold text-4xl">#{dashboardBean.inactiveUsersDisplay}</div>
|
||||
<div class="text-600 text-sm mt-2">
|
||||
<i class="pi pi-ban mr-2"></i>
|
||||
Comptes désactivés
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-history text-orange-600 text-xl"></i>
|
||||
style="width: 3.5rem; height: 3.5rem">
|
||||
<i class="pi pi-ban text-orange-600" style="font-size: 1.75rem"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-500 text-sm">
|
||||
<i class="pi pi-arrow-right text-600"></i>
|
||||
<span class="ml-2">Dernières 24h</span>
|
||||
</div>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI 4: Taux d'Activation -->
|
||||
<!-- KPI 4: Taux de Succès 24h -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Realm Actif</div>
|
||||
<div class="text-900 font-bold text-xl" style="word-break: break-word;">lions-user-manager</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-globe text-purple-600 text-xl"></i>
|
||||
<div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
|
||||
<fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
|
||||
outcome="/pages/user-manager/audit/logs">
|
||||
<div class="flex align-items-start justify-content-between">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-2 text-sm uppercase">Taux de Succès</div>
|
||||
<div class="text-900 font-bold text-4xl">#{dashboardBean.successRate24hDisplay}</div>
|
||||
<div class="text-600 text-sm mt-2">
|
||||
<i class="pi pi-chart-line mr-2"></i>
|
||||
Dernières 24h
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-cyan-100 border-circle"
|
||||
style="width: 3.5rem; height: 3.5rem">
|
||||
<i class="pi pi-chart-line text-cyan-600" style="font-size: 1.75rem"></i>
|
||||
</div>
|
||||
</div>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
ACTIVITÉ & PERFORMANCE
|
||||
================================================================ -->
|
||||
<div class="col-12 lg:col-6">
|
||||
<div class="card h-full">
|
||||
<div class="flex align-items-center gap-2 mb-4">
|
||||
<i class="pi pi-history text-purple-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Activité & Performance</h5>
|
||||
</div>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<p:tag value="Opérationnel" severity="success" styleClass="text-xs" />
|
||||
<span class="text-500 text-sm">Realm Keycloak</span>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Actions 24h -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<div class="text-500 text-xs uppercase mb-2">Actions 24h</div>
|
||||
<div class="text-900 font-bold text-3xl mb-2">#{dashboardBean.actionsLast24hDisplay}</div>
|
||||
<fr:tag value="Dernières 24h" severity="info" styleClass="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions 7j -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<div class="text-500 text-xs uppercase mb-2">Actions 7j</div>
|
||||
<div class="text-900 font-bold text-3xl mb-2">#{dashboardBean.actionsLast7dDisplay}</div>
|
||||
<fr:tag value="Derniers 7 jours" severity="info" styleClass="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Taux de réussite -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<div class="text-500 text-xs uppercase mb-2">Performance</div>
|
||||
<div class="text-900 font-bold text-3xl mb-2">#{dashboardBean.successRate24hDisplay}</div>
|
||||
<fr:tag value="Taux de succès" severity="success" styleClass="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Détails succès/échecs -->
|
||||
<div class="col-12 mt-3">
|
||||
<div class="surface-100 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between mb-2">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-check-circle text-green-500"></i>
|
||||
<span class="text-700 font-medium">Actions réussies</span>
|
||||
</div>
|
||||
<span class="font-bold text-900">#{dashboardBean.successfulActions24h}</span>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-times-circle text-red-500"></i>
|
||||
<span class="text-700 font-medium">Actions échouées</span>
|
||||
</div>
|
||||
<span class="font-bold text-900">#{dashboardBean.failedActions24h}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,32 +206,32 @@
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6">
|
||||
<p:commandButton
|
||||
value="Nouvel Utilisateur"
|
||||
icon="pi pi-user-plus"
|
||||
styleClass="w-full p-button-success mb-2"
|
||||
outcome="/pages/user-manager/users/create" />
|
||||
<fr:commandButton value="Nouvel Utilisateur"
|
||||
icon="pi pi-user-plus"
|
||||
severity="success"
|
||||
styleClass="w-full mb-2"
|
||||
outcome="/pages/user-manager/users/create" />
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<p:commandButton
|
||||
value="Liste des Utilisateurs"
|
||||
icon="pi pi-users"
|
||||
styleClass="w-full p-button-primary mb-2"
|
||||
outcome="/pages/user-manager/users/list" />
|
||||
<fr:commandButton value="Liste des Utilisateurs"
|
||||
icon="pi pi-users"
|
||||
severity="primary"
|
||||
styleClass="w-full mb-2"
|
||||
outcome="/pages/user-manager/users/list" />
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<p:commandButton
|
||||
value="Gestion des Rôles"
|
||||
icon="pi pi-shield"
|
||||
styleClass="w-full p-button-info mb-2"
|
||||
outcome="/pages/user-manager/roles/list" />
|
||||
<fr:commandButton value="Gestion des Rôles"
|
||||
icon="pi pi-shield"
|
||||
severity="info"
|
||||
styleClass="w-full mb-2"
|
||||
outcome="/pages/user-manager/roles/list" />
|
||||
</div>
|
||||
<div class="col-12 md:col-6">
|
||||
<p:commandButton
|
||||
value="Journal d'Audit"
|
||||
icon="pi pi-history"
|
||||
styleClass="w-full p-button-help mb-2"
|
||||
outcome="/pages/user-manager/audit/logs" />
|
||||
<fr:commandButton value="Journal d'Audit"
|
||||
icon="pi pi-history"
|
||||
severity="help"
|
||||
styleClass="w-full mb-2"
|
||||
outcome="/pages/user-manager/audit/logs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +240,113 @@
|
||||
<i class="pi pi-lightbulb text-orange-500"></i>
|
||||
<div>
|
||||
<div class="text-700 font-semibold text-sm">Conseil</div>
|
||||
<small class="text-600">Utilisez les raccourcis ci-dessus pour accéder rapidement aux fonctionnalités principales</small>
|
||||
<small class="text-600">Utilisez ces raccourcis pour accéder rapidement aux fonctionnalités principales</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
ALERTES DE SÉCURITÉ (Conditionnel)
|
||||
================================================================ -->
|
||||
<h:panelGroup layout="block" styleClass="col-12" rendered="#{dashboardBean.hasAlerts()}">
|
||||
<div class="card border-left-3 border-red-500">
|
||||
<div class="flex align-items-center gap-2 mb-4">
|
||||
<i class="pi pi-exclamation-triangle text-red-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0 text-red-600">Alertes de Sécurité</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Actions critiques -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<i class="pi pi-shield text-red-500 mb-2" style="font-size: 2rem"></i>
|
||||
<div class="text-900 font-bold text-2xl mb-1">#{dashboardBean.criticalActions24hDisplay}</div>
|
||||
<div class="text-600 text-sm">Actions critiques</div>
|
||||
<small class="text-500">Dernières 24h</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tentatives échouées -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<i class="pi pi-lock text-orange-500 mb-2" style="font-size: 2rem"></i>
|
||||
<div class="text-900 font-bold text-2xl mb-1">#{dashboardBean.failedLogins24hDisplay}</div>
|
||||
<div class="text-600 text-sm">Connexions échouées</div>
|
||||
<small class="text-500">Dernières 24h</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Utilisateurs à risque -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<i class="pi pi-user-minus text-red-500 mb-2" style="font-size: 2rem"></i>
|
||||
<div class="text-900 font-bold text-2xl mb-1">#{dashboardBean.usersAtRiskDisplay}</div>
|
||||
<div class="text-600 text-sm">Utilisateurs à risque</div>
|
||||
<small class="text-500">Nécessitent attention</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 surface-100 border-round p-3 border-left-3 border-orange-500">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-info-circle text-orange-500"></i>
|
||||
<div>
|
||||
<div class="text-700 font-semibold text-sm">Recommandation</div>
|
||||
<small class="text-600">Consultez le journal d'audit pour analyser les événements suspects</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- ================================================================
|
||||
RESSOURCES MÉTIER
|
||||
================================================================ -->
|
||||
<div class="col-12 lg:col-6">
|
||||
<div class="card h-full">
|
||||
<div class="flex align-items-center gap-2 mb-4">
|
||||
<i class="pi pi-database text-blue-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Ressources</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Total Rôles -->
|
||||
<div class="col-12">
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-3">
|
||||
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
|
||||
style="width: 3rem; height: 3rem">
|
||||
<i class="pi pi-shield text-green-600 text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-500 text-xs uppercase mb-1">Rôles Realm</div>
|
||||
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalRolesDisplay}</div>
|
||||
</div>
|
||||
</div>
|
||||
<fr:commandButton icon="pi pi-arrow-right"
|
||||
text="true"
|
||||
severity="secondary"
|
||||
outcome="/pages/user-manager/roles/list" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Realm actif -->
|
||||
<div class="col-12">
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-globe text-500"></i>
|
||||
<span class="text-600 font-medium">Realm Keycloak</span>
|
||||
</div>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<span class="font-semibold text-900">#{dashboardBean.realmName}</span>
|
||||
<fr:tag value="Actif" severity="success" styleClass="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,7 +359,7 @@
|
||||
<div class="col-12 lg:col-6">
|
||||
<div class="card h-full">
|
||||
<div class="flex align-items-center gap-2 mb-4">
|
||||
<i class="pi pi-info-circle text-blue-500" style="font-size: 1.5rem"></i>
|
||||
<i class="pi pi-info-circle text-cyan-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Informations Système</h5>
|
||||
</div>
|
||||
|
||||
@@ -202,34 +369,12 @@
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-tag text-500"></i>
|
||||
<span class="text-600 font-medium">Version</span>
|
||||
<span class="text-600 font-medium">Version Application</span>
|
||||
</div>
|
||||
<span class="font-semibold text-900">1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Realm Keycloak -->
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-globe text-500"></i>
|
||||
<span class="text-600 font-medium">Realm Keycloak</span>
|
||||
</div>
|
||||
<span class="font-semibold text-900">lions-user-manager</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statut -->
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-check-circle text-500"></i>
|
||||
<span class="text-600 font-medium">Statut</span>
|
||||
</div>
|
||||
<p:tag value="Opérationnel" severity="success" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Framework -->
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
@@ -237,90 +382,18 @@
|
||||
<i class="pi pi-code text-500"></i>
|
||||
<span class="text-600 font-medium">Framework</span>
|
||||
</div>
|
||||
<span class="font-semibold text-900 text-right">Quarkus 3.15.1</span>
|
||||
<span class="font-semibold text-900 text-right">Quarkus + PrimeFaces Freya</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interface -->
|
||||
<!-- Statut -->
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-palette text-500"></i>
|
||||
<span class="text-600 font-medium">Interface</span>
|
||||
<i class="pi pi-check-circle text-500"></i>
|
||||
<span class="text-600 font-medium">Statut Système</span>
|
||||
</div>
|
||||
<span class="font-semibold text-900">PrimeFaces Freya</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environnement -->
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-server text-500"></i>
|
||||
<span class="text-600 font-medium">Environnement</span>
|
||||
</div>
|
||||
<p:tag value="Développement" severity="warning" styleClass="text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
ACTIVITÉS RÉCENTES
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="flex align-items-center justify-content-between mb-4">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-clock text-purple-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Activités Récentes</h5>
|
||||
</div>
|
||||
<p:commandButton
|
||||
value="Voir tout"
|
||||
icon="pi pi-arrow-right"
|
||||
styleClass="p-button-text p-button-sm"
|
||||
outcome="/pages/user-manager/audit/logs" />
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Statistique 1: Utilisateurs créés aujourd'hui -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<i class="pi pi-user-plus text-blue-500 mb-2" style="font-size: 2rem"></i>
|
||||
<div class="text-900 font-bold text-xl mb-1">0</div>
|
||||
<div class="text-600 text-sm">Utilisateurs créés</div>
|
||||
<small class="text-500">Aujourd'hui</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistique 2: Rôles modifiés -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<i class="pi pi-shield text-green-500 mb-2" style="font-size: 2rem"></i>
|
||||
<div class="text-900 font-bold text-xl mb-1">0</div>
|
||||
<div class="text-600 text-sm">Rôles modifiés</div>
|
||||
<small class="text-500">Cette semaine</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistique 3: Sessions actives -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<i class="pi pi-circle-fill text-orange-500 mb-2" style="font-size: 2rem; animation: pulse 2s ease-in-out infinite;"></i>
|
||||
<div class="text-900 font-bold text-xl mb-1">-</div>
|
||||
<div class="text-600 text-sm">Sessions actives</div>
|
||||
<small class="text-500">En temps réel</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistique 4: Actions critiques -->
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<i class="pi pi-exclamation-triangle text-red-500 mb-2" style="font-size: 2rem"></i>
|
||||
<div class="text-900 font-bold text-xl mb-1">0</div>
|
||||
<div class="text-600 text-sm">Actions critiques</div>
|
||||
<small class="text-500">24 dernières heures</small>
|
||||
<fr:tag value="Opérationnel" severity="success" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:h="http://xmlns.jcp.org/jsf/html"
|
||||
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">
|
||||
|
||||
<f:metadata>
|
||||
<f:viewParam name="userId" value="#{userProfilBean.userId}" />
|
||||
<f:viewParam name="realm" value="#{userProfilBean.realmName}" />
|
||||
</f:metadata>
|
||||
|
||||
<ui:param name="page" value="#{userProfilBean}"/>
|
||||
<ui:define name="title">Attribution de Rôles - Lions User Manager</ui:define>
|
||||
|
||||
<ui:define name="content">
|
||||
<div class="grid">
|
||||
<!-- ================================================================
|
||||
EN-TÊTE DE LA PAGE
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-key text-purple-500" style="font-size: 2rem"></i>
|
||||
<div>
|
||||
<h3 class="m-0 mb-1">Attribution de Rôles</h3>
|
||||
<p class="text-600 m-0">Gérer les rôles de l'utilisateur</p>
|
||||
</div>
|
||||
</div>
|
||||
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text">
|
||||
<i class="pi pi-arrow-left mr-2"></i>
|
||||
Retour à la liste
|
||||
</h:link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
INFORMATIONS UTILISATEUR
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-user text-blue-500"></i>
|
||||
Informations de l'Utilisateur
|
||||
</h3>
|
||||
|
||||
<h:panelGroup rendered="#{userProfilBean.user != null}">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="surface-50 border-round p-3 text-center">
|
||||
<!-- Avatar -->
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600, #387FE9)); display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem auto; font-size: 2rem; font-weight: bold; color: white; box-shadow: 0 4px 12px rgba(0,0,0,0.12);">
|
||||
<h:outputText value="#{userProfilBean.user.username.substring(0,2).toUpperCase()}" />
|
||||
</div>
|
||||
<h4 class="text-900 font-semibold m-0 mb-1">#{userProfilBean.user.username}</h4>
|
||||
<p class="text-600 m-0 text-sm">#{userProfilBean.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-8">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="mb-3">
|
||||
<label class="block text-600 font-medium mb-1 text-sm">Prénom</label>
|
||||
<p class="text-900 m-0">#{userProfilBean.user.prenom}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="mb-3">
|
||||
<label class="block text-600 font-medium mb-1 text-sm">Nom</label>
|
||||
<p class="text-900 m-0">#{userProfilBean.user.nom}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="mb-3">
|
||||
<label class="block text-600 font-medium mb-1 text-sm">Email</label>
|
||||
<p class="text-900 m-0">#{userProfilBean.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="mb-0">
|
||||
<label class="block text-600 font-medium mb-2 text-sm">Statut</label>
|
||||
<div class="flex align-items-center">
|
||||
<span class="inline-flex align-items-center px-2 py-1 border-round text-xs font-semibold"
|
||||
style="background-color: #{userProfilBean.user.enabled ? '#C8E6C9' : '#FFCDD2'}; color: #{userProfilBean.user.enabled ? '#2E7D32' : '#C62828'};">
|
||||
<i class="pi #{userProfilBean.user.enabled ? 'pi-check-circle' : 'pi-times-circle'} mr-1"></i>
|
||||
<h:outputText value="Actif" rendered="#{userProfilBean.user.enabled}" />
|
||||
<h:outputText value="Inactif" rendered="#{!userProfilBean.user.enabled}" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
|
||||
<h:panelGroup rendered="#{userProfilBean.user == null}">
|
||||
<div class="text-center p-5">
|
||||
<i class="pi pi-exclamation-triangle text-orange-500" style="font-size: 4rem"></i>
|
||||
<h4 class="text-900 mt-4 mb-2">Utilisateur non trouvé</h4>
|
||||
<p class="text-600 mb-3">
|
||||
<h:outputText value="Aucun ID d'utilisateur fourni" rendered="#{userProfilBean.userId == null or userProfilBean.userId == ''}" />
|
||||
<h:outputText value="L'utilisateur avec l'ID '#{userProfilBean.userId}' n'existe pas dans le realm '#{userProfilBean.realmName}'" rendered="#{userProfilBean.userId != null and userProfilBean.userId != ''}" />
|
||||
</p>
|
||||
<small class="text-500 block mb-4">Pour assigner des rôles, accédez à cette page depuis la liste des utilisateurs</small>
|
||||
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-primary">
|
||||
<i class="pi pi-list mr-2"></i>
|
||||
Aller à la liste des utilisateurs
|
||||
</h:link>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
GESTION DES RÔLES
|
||||
================================================================ -->
|
||||
<h:panelGroup rendered="#{userProfilBean.user != null}">
|
||||
<div class="col-12 lg:col-6">
|
||||
<div class="card h-full">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-shield text-green-500"></i>
|
||||
Rôles Actuels
|
||||
</h3>
|
||||
|
||||
<h:form id="formCurrentRoles">
|
||||
<!-- Liste des rôles actuels -->
|
||||
<div class="flex flex-column gap-2">
|
||||
<ui:repeat value="#{userProfilBean.user.realmRoles}" var="role">
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2 flex-grow-1">
|
||||
<i class="pi pi-tag text-purple-500"></i>
|
||||
<div>
|
||||
<div class="text-900 font-semibold">#{role}</div>
|
||||
<small class="text-500">Rôle Realm</small>
|
||||
</div>
|
||||
</div>
|
||||
<p:commandButton icon="pi pi-times"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
|
||||
title="Retirer ce rôle"
|
||||
action="#{roleGestionBean.revokeRoleFromUser(userProfilBean.userId, role)}"
|
||||
update=":formCurrentRoles :formAvailableRoles"
|
||||
oncomplete="PF('formCurrentRoles').refresh();">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Voulez-vous vraiment retirer le rôle '#{role}' ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
</ui:repeat>
|
||||
|
||||
<!-- Message si aucun rôle -->
|
||||
<div class="text-center p-4" rendered="#{userProfilBean.user.realmRoles == null or userProfilBean.user.realmRoles.size() == 0}">
|
||||
<i class="pi pi-inbox text-400" style="font-size: 2.5rem"></i>
|
||||
<p class="text-600 mt-3 mb-0">Aucun rôle assigné</p>
|
||||
<small class="text-500">Assignez des rôles depuis la liste disponible</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex align-items-center justify-content-between surface-100 border-round p-3">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-info-circle text-blue-500"></i>
|
||||
<span class="text-700 font-semibold">Total: #{userProfilBean.user.realmRoles != null ? userProfilBean.user.realmRoles.size() : 0} rôle(s)</span>
|
||||
</div>
|
||||
<fr:commandButton value="Rafraîchir"
|
||||
icon="pi pi-refresh"
|
||||
outlined="true"
|
||||
size="small"
|
||||
action="#{userProfilBean.loadUser}"
|
||||
update=":formCurrentRoles :formAvailableRoles" />
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 lg:col-6">
|
||||
<div class="card h-full">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-plus-circle text-blue-500"></i>
|
||||
Rôles Disponibles
|
||||
</h3>
|
||||
|
||||
<h:form id="formAvailableRoles">
|
||||
<p:messages id="messages" showDetail="true" closable="true">
|
||||
<p:autoUpdate />
|
||||
</p:messages>
|
||||
|
||||
<!-- Liste des rôles disponibles -->
|
||||
<div class="flex flex-column gap-2">
|
||||
<ui:repeat value="#{roleGestionBean.realmRoles}" var="role">
|
||||
<!-- N'afficher que si le rôle n'est pas déjà assigné -->
|
||||
<h:panelGroup rendered="#{!userProfilBean.user.realmRoles.contains(role.name)}">
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex-grow-1">
|
||||
<div class="text-900 font-semibold flex align-items-center gap-2 mb-1">
|
||||
<i class="pi pi-tag text-blue-500"></i>
|
||||
<span>#{role.name}</span>
|
||||
</div>
|
||||
<p class="text-600 text-sm m-0">
|
||||
<h:outputText value="#{role.description}" rendered="#{role.description != null and role.description != ''}" />
|
||||
<h:outputText value="Aucune description" styleClass="text-500 italic" rendered="#{role.description == null or role.description == ''}" />
|
||||
</p>
|
||||
</div>
|
||||
<p:commandButton icon="pi pi-plus"
|
||||
styleClass="p-button-rounded p-button-success p-button-sm"
|
||||
title="Assigner ce rôle"
|
||||
action="#{roleGestionBean.assignRoleToUser(userProfilBean.userId, role.name)}"
|
||||
update=":formCurrentRoles :formAvailableRoles"
|
||||
oncomplete="PF('formAvailableRoles').refresh();" />
|
||||
</div>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
</ui:repeat>
|
||||
|
||||
<!-- Message si aucun rôle disponible -->
|
||||
<div class="text-center p-4" rendered="#{roleGestionBean.realmRoles == null or roleGestionBean.realmRoles.size() == 0}">
|
||||
<i class="pi pi-inbox text-400" style="font-size: 2.5rem"></i>
|
||||
<p class="text-600 mt-3 mb-0">Aucun rôle disponible</p>
|
||||
<small class="text-500">Créez des rôles depuis la page de gestion des rôles</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 surface-100 border-round p-3">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-lightbulb text-orange-500"></i>
|
||||
<div>
|
||||
<div class="text-700 font-semibold text-sm">Astuce</div>
|
||||
<small class="text-600">Cliquez sur <i class="pi pi-plus"></i> pour assigner un rôle à l'utilisateur</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
ACTIONS
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-cog text-gray-500"></i>
|
||||
Actions
|
||||
</h3>
|
||||
|
||||
<h:form id="formActions">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<h:link outcome="/pages/user-manager/users/profile"
|
||||
styleClass="p-button p-button-outlined">
|
||||
<f:param name="userId" value="#{userProfilBean.userId}" />
|
||||
<i class="pi pi-user mr-2"></i>
|
||||
<span>Voir le Profil</span>
|
||||
</h:link>
|
||||
|
||||
<h:link outcome="/pages/user-manager/users/edit"
|
||||
styleClass="p-button p-button-outlined">
|
||||
<f:param name="userId" value="#{userProfilBean.userId}" />
|
||||
<i class="pi pi-pencil mr-2"></i>
|
||||
<span>Modifier l'Utilisateur</span>
|
||||
</h:link>
|
||||
|
||||
<h:link outcome="/pages/user-manager/users/list"
|
||||
styleClass="p-button p-button-outlined p-button-secondary">
|
||||
<i class="pi pi-list mr-2"></i>
|
||||
<span>Liste des Utilisateurs</span>
|
||||
</h:link>
|
||||
|
||||
<h:link outcome="/pages/user-manager/roles/list"
|
||||
styleClass="p-button p-button-outlined p-button-info">
|
||||
<i class="pi pi-shield mr-2"></i>
|
||||
<span>Gérer les Rôles</span>
|
||||
</h:link>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG DE CONFIRMATION
|
||||
================================================================ -->
|
||||
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
|
||||
responsive="true" width="400">
|
||||
<p:commandButton value="Non" type="button"
|
||||
styleClass="p-button-text"
|
||||
icon="pi pi-times" />
|
||||
<p:commandButton value="Oui" type="button"
|
||||
styleClass="p-button-danger"
|
||||
icon="pi pi-check" />
|
||||
</p:confirmDialog>
|
||||
</ui:define>
|
||||
|
||||
</ui:composition>
|
||||
@@ -42,164 +42,132 @@
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
FILTRES
|
||||
FILTRES & KPI INTÉGRÉS (Nombre d'or φ = 1.618 → 62%/38%)
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-filter text-blue-500"></i>
|
||||
Filtres
|
||||
</h3>
|
||||
|
||||
<h:form id="formFilters">
|
||||
<div class="grid">
|
||||
<!-- Realm -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field mb-0">
|
||||
<label for="realmFilter" class="block text-900 font-medium mb-2">
|
||||
Realm
|
||||
</label>
|
||||
<p:selectOneMenu id="realmFilter"
|
||||
value="#{roleGestionBean.realmName}"
|
||||
styleClass="w-full">
|
||||
<f:selectItem itemLabel="Sélectionner un realm..." itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableRealms}"
|
||||
var="realm"
|
||||
itemLabel="#{realm}"
|
||||
itemValue="#{realm}" />
|
||||
<p:ajax event="change"
|
||||
listener="#{roleGestionBean.loadRealmRoles}"
|
||||
update=":formRealmRoles :formClientRoles :formKpis" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field mb-0">
|
||||
<label for="clientFilter" class="block text-900 font-medium mb-2">
|
||||
Client (optionnel)
|
||||
</label>
|
||||
<p:selectOneMenu id="clientFilter"
|
||||
value="#{roleGestionBean.clientName}"
|
||||
styleClass="w-full">
|
||||
<f:selectItem itemLabel="Tous les clients" itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableClients}"
|
||||
var="client"
|
||||
itemLabel="#{client}"
|
||||
itemValue="#{client}" />
|
||||
<p:ajax event="change"
|
||||
listener="#{roleGestionBean.loadClientRoles}"
|
||||
update=":formClientRoles" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type -->
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field mb-0">
|
||||
<label for="typeFilter" class="block text-900 font-medium mb-2">
|
||||
Type de rôle
|
||||
</label>
|
||||
<p:selectOneMenu id="typeFilter"
|
||||
value="#{roleGestionBean.selectedTypeRole}"
|
||||
styleClass="w-full">
|
||||
<f:selectItem itemLabel="Tous les types" itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.typeRoleOptions}"
|
||||
var="type"
|
||||
itemLabel="#{type}"
|
||||
itemValue="#{type}" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
KPI CARDS
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<h:form id="formKpis">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Rôles Realm</div>
|
||||
<div class="text-900 font-bold text-2xl">#{roleGestionBean.realmRoles.size()}</div>
|
||||
<!-- =========== FILTRES (62%) =========== -->
|
||||
<div class="col-12 lg:col-7">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-filter text-blue-500"></i>
|
||||
Filtres
|
||||
</h3>
|
||||
|
||||
<h:form id="formFilters">
|
||||
<div class="grid">
|
||||
<!-- Realm -->
|
||||
<div class="col-12">
|
||||
<fr:fieldSelect id="realmFilter"
|
||||
label="Realm"
|
||||
value="#{roleGestionBean.realmName}"
|
||||
iconLeft="pi pi-globe">
|
||||
<f:selectItem itemLabel="Sélectionner un realm..." itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableRealms}"
|
||||
var="realm"
|
||||
itemLabel="#{realm}"
|
||||
itemValue="#{realm}" />
|
||||
<p:ajax event="change"
|
||||
listener="#{roleGestionBean.loadRealmRoles}"
|
||||
update=":formRealmRoles :formClientRoles :formKpis" />
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-shield text-purple-600 text-xl"></i>
|
||||
|
||||
<!-- Client -->
|
||||
<div class="col-12">
|
||||
<fr:fieldSelect id="clientFilter"
|
||||
label="Client (optionnel)"
|
||||
value="#{roleGestionBean.clientName}"
|
||||
iconLeft="pi pi-box">
|
||||
<f:selectItem itemLabel="Tous les clients" itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableClients}"
|
||||
var="client"
|
||||
itemLabel="#{client}"
|
||||
itemValue="#{client}" />
|
||||
<p:ajax event="change"
|
||||
listener="#{roleGestionBean.loadClientRoles}"
|
||||
update=":formClientRoles" />
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<!-- Type -->
|
||||
<div class="col-12">
|
||||
<fr:fieldSelect id="typeFilter"
|
||||
label="Type de rôle"
|
||||
value="#{roleGestionBean.selectedTypeRole}"
|
||||
iconLeft="pi pi-filter">
|
||||
<f:selectItem itemLabel="Tous les types" itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.typeRoleOptions}"
|
||||
var="type"
|
||||
itemLabel="#{type}"
|
||||
itemValue="#{type}" />
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-500 text-sm">
|
||||
<i class="pi pi-globe text-600"></i>
|
||||
<span class="ml-2">Rôles du realm</span>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Rôles Client</div>
|
||||
<div class="text-900 font-bold text-2xl">#{roleGestionBean.clientRoles.size()}</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-sitemap text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-500 text-sm">
|
||||
<i class="pi pi-box text-600"></i>
|
||||
<span class="ml-2">Rôles spécifiques client</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- =========== KPI STATISTIQUES (38%) =========== -->
|
||||
<div class="col-12 lg:col-5">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-chart-bar text-purple-500"></i>
|
||||
Statistiques <small class="text-500 font-normal">(φ = 1.618)</small>
|
||||
</h3>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Total Rôles</div>
|
||||
<div class="text-900 font-bold text-2xl">#{roleGestionBean.allRoles.size()}</div>
|
||||
<h:form id="formKpis">
|
||||
<div class="grid">
|
||||
<!-- KPI 1: Rôles Realm -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="surface-50 border-round p-3 text-center h-full">
|
||||
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle mx-auto mb-2"
|
||||
style="width: 2rem; height: 2rem">
|
||||
<i class="pi pi-shield text-purple-600 text-sm"></i>
|
||||
</div>
|
||||
<div class="text-900 font-bold text-xl mb-1">#{roleGestionBean.realmRoles.size()}</div>
|
||||
<div class="text-500 text-xs">Rôles Realm</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-check-circle text-green-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-500 text-sm">
|
||||
<i class="pi pi-chart-bar text-600"></i>
|
||||
<span class="ml-2">Tous les rôles configurés</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="card surface-0 border-round-lg">
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Realm Actif</div>
|
||||
<div class="text-900 font-bold text-xl">#{roleGestionBean.realmName}</div>
|
||||
<!-- KPI 2: Rôles Client -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="surface-50 border-round p-3 text-center h-full">
|
||||
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle mx-auto mb-2"
|
||||
style="width: 2rem; height: 2rem">
|
||||
<i class="pi pi-sitemap text-blue-600 text-sm"></i>
|
||||
</div>
|
||||
<div class="text-900 font-bold text-xl mb-1">#{roleGestionBean.clientRoles.size()}</div>
|
||||
<div class="text-500 text-xs">Rôles Client</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-database text-orange-600 text-xl"></i>
|
||||
|
||||
<!-- KPI 3: Total Rôles -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="surface-50 border-round p-3 text-center h-full">
|
||||
<div class="flex align-items-center justify-content-center bg-green-100 border-circle mx-auto mb-2"
|
||||
style="width: 2rem; height: 2rem">
|
||||
<i class="pi pi-check-circle text-green-600 text-sm"></i>
|
||||
</div>
|
||||
<div class="text-900 font-bold text-xl mb-1">#{roleGestionBean.allRoles.size()}</div>
|
||||
<div class="text-500 text-xs">Total Rôles</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI 4: Realm Actif -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="surface-50 border-round p-3 text-center h-full">
|
||||
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle mx-auto mb-2"
|
||||
style="width: 2rem; height: 2rem">
|
||||
<i class="pi pi-database text-orange-600 text-sm"></i>
|
||||
</div>
|
||||
<div class="text-900 font-bold text-sm mb-1" style="word-break: break-all;">#{roleGestionBean.realmName}</div>
|
||||
<div class="text-500 text-xs">Realm Actif</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-500 text-sm">
|
||||
<i class="pi pi-server text-600"></i>
|
||||
<span class="ml-2">Realm actuellement sélectionné</span>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
@@ -237,15 +205,18 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex align-items-center gap-1">
|
||||
<p:commandButton icon="pi pi-trash"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
|
||||
title="Supprimer"
|
||||
action="#{roleGestionBean.deleteRealmRole(role.name)}"
|
||||
update=":formRealmRoles :formKpis">
|
||||
<fr:commandButton icon="pi pi-trash"
|
||||
severity="danger"
|
||||
rounded="true"
|
||||
text="true"
|
||||
size="small"
|
||||
title="Supprimer"
|
||||
action="#{roleGestionBean.deleteRealmRole(role.name)}"
|
||||
update=":formRealmRoles :formKpis">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -317,15 +288,18 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex align-items-center gap-1">
|
||||
<p:commandButton icon="pi pi-trash"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
|
||||
title="Supprimer"
|
||||
action="#{roleGestionBean.deleteClientRole(role.name)}"
|
||||
update=":formClientRoles :formKpis">
|
||||
<fr:commandButton icon="pi pi-trash"
|
||||
severity="danger"
|
||||
rounded="true"
|
||||
text="true"
|
||||
size="small"
|
||||
title="Supprimer"
|
||||
action="#{roleGestionBean.deleteClientRole(role.name)}"
|
||||
update=":formClientRoles :formKpis">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -368,150 +342,108 @@
|
||||
<!-- ================================================================
|
||||
DIALOG CRÉATION RÔLE REALM
|
||||
================================================================ -->
|
||||
<p:dialog header="Nouveau Rôle Realm"
|
||||
widgetVar="createRealmRoleDialog"
|
||||
modal="true"
|
||||
responsive="true"
|
||||
width="600"
|
||||
showEffect="fade"
|
||||
hideEffect="fade">
|
||||
<h:form id="formCreateRealmRole">
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
<div class="field mb-3">
|
||||
<label for="realmRoleName" class="block text-900 font-medium mb-2">
|
||||
Nom du rôle <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:inputText id="realmRoleName"
|
||||
value="#{roleGestionBean.newRole.name}"
|
||||
styleClass="w-full"
|
||||
required="true"
|
||||
placeholder="ex: admin_lions">
|
||||
<f:validateLength minimum="2" maximum="100" />
|
||||
<f:validateRegex pattern="^[a-zA-Z0-9_-]+$" />
|
||||
</p:inputText>
|
||||
<small class="text-500">Lettres, chiffres, underscores et tirets uniquement</small>
|
||||
</div>
|
||||
|
||||
<div class="field mb-0">
|
||||
<label for="realmRoleDesc" class="block text-900 font-medium mb-2">
|
||||
Description
|
||||
</label>
|
||||
<p:inputTextarea id="realmRoleDesc"
|
||||
value="#{roleGestionBean.newRole.description}"
|
||||
styleClass="w-full"
|
||||
rows="3"
|
||||
placeholder="Description du rôle...">
|
||||
</p:inputTextarea>
|
||||
</div>
|
||||
</div>
|
||||
<fr:formDialog widgetVar="createRealmRoleDialog"
|
||||
header="Nouveau Rôle Realm"
|
||||
formId="formCreateRealmRole"
|
||||
saveLabel="Créer"
|
||||
cancelLabel="Annuler"
|
||||
saveAction="#{roleGestionBean.createRealmRole}"
|
||||
update=":formRealmRoles :formKpis :formCreateRealmRole"
|
||||
width="600">
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
<fr:fieldInput id="realmRoleName"
|
||||
label="Nom du rôle"
|
||||
value="#{roleGestionBean.newRole.name}"
|
||||
required="true"
|
||||
placeholder="ex: admin_lions"
|
||||
iconLeft="pi pi-tag"
|
||||
helpText="Lettres, chiffres, underscores et tirets uniquement">
|
||||
<f:validateLength for="input" minimum="2" maximum="100" />
|
||||
<f:validateRegex for="input" pattern="^[a-zA-Z0-9_-]+$" />
|
||||
</fr:fieldInput>
|
||||
</div>
|
||||
|
||||
<p:messages id="messagesRealmRole" showDetail="true" closable="true" styleClass="mt-3">
|
||||
<p:autoUpdate />
|
||||
</p:messages>
|
||||
</h:form>
|
||||
<div class="col-12">
|
||||
<fr:fieldTextarea id="realmRoleDesc"
|
||||
label="Description"
|
||||
value="#{roleGestionBean.newRole.description}"
|
||||
rows="3"
|
||||
placeholder="Description du rôle..."
|
||||
iconLeft="pi pi-align-left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<f:facet name="footer">
|
||||
<p:commandButton value="Annuler"
|
||||
icon="pi pi-times"
|
||||
styleClass="p-button-text"
|
||||
onclick="PF('createRealmRoleDialog').hide();"
|
||||
type="button" />
|
||||
<p:commandButton value="Créer"
|
||||
icon="pi pi-check"
|
||||
styleClass="p-button-success"
|
||||
action="#{roleGestionBean.createRealmRole}"
|
||||
update=":formRealmRoles :formKpis :formCreateRealmRole"
|
||||
oncomplete="if (args && !args.validationFailed) PF('createRealmRoleDialog').hide();" />
|
||||
</f:facet>
|
||||
</p:dialog>
|
||||
<fr:message id="messagesRealmRole" showDetail="true" closable="true">
|
||||
<p:autoUpdate />
|
||||
</fr:message>
|
||||
</fr:formDialog>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG CRÉATION RÔLE CLIENT
|
||||
================================================================ -->
|
||||
<p:dialog header="Nouveau Rôle Client"
|
||||
widgetVar="createClientRoleDialog"
|
||||
modal="true"
|
||||
responsive="true"
|
||||
width="600"
|
||||
showEffect="fade"
|
||||
hideEffect="fade">
|
||||
<h:form id="formCreateClientRole">
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
<div class="field mb-3">
|
||||
<label for="clientRoleName" class="block text-900 font-medium mb-2">
|
||||
Nom du rôle <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:inputText id="clientRoleName"
|
||||
value="#{roleGestionBean.newRole.name}"
|
||||
styleClass="w-full"
|
||||
required="true"
|
||||
placeholder="ex: manager">
|
||||
<f:validateLength minimum="2" maximum="100" />
|
||||
<f:validateRegex pattern="^[a-zA-Z0-9_-]+$" />
|
||||
</p:inputText>
|
||||
</div>
|
||||
|
||||
<div class="field mb-3">
|
||||
<label for="clientName" class="block text-900 font-medium mb-2">
|
||||
Client <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:selectOneMenu id="clientName"
|
||||
value="#{roleGestionBean.clientName}"
|
||||
styleClass="w-full"
|
||||
required="true">
|
||||
<f:selectItem itemLabel="Sélectionner un client..." itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableClients}" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
|
||||
<div class="field mb-0">
|
||||
<label for="clientRoleDesc" class="block text-900 font-medium mb-2">
|
||||
Description
|
||||
</label>
|
||||
<p:inputTextarea id="clientRoleDesc"
|
||||
value="#{roleGestionBean.newRole.description}"
|
||||
styleClass="w-full"
|
||||
rows="3"
|
||||
placeholder="Description du rôle...">
|
||||
</p:inputTextarea>
|
||||
</div>
|
||||
</div>
|
||||
<fr:formDialog widgetVar="createClientRoleDialog"
|
||||
header="Nouveau Rôle Client"
|
||||
formId="formCreateClientRole"
|
||||
saveLabel="Créer"
|
||||
cancelLabel="Annuler"
|
||||
saveAction="#{roleGestionBean.createClientRole}"
|
||||
update=":formClientRoles :formKpis :formCreateClientRole"
|
||||
width="600">
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
<fr:fieldInput id="clientRoleName"
|
||||
label="Nom du rôle"
|
||||
value="#{roleGestionBean.newRole.name}"
|
||||
required="true"
|
||||
placeholder="ex: manager"
|
||||
iconLeft="pi pi-tag"
|
||||
helpText="Lettres, chiffres, underscores et tirets uniquement">
|
||||
<f:validateLength for="input" minimum="2" maximum="100" />
|
||||
<f:validateRegex for="input" pattern="^[a-zA-Z0-9_-]+$" />
|
||||
</fr:fieldInput>
|
||||
</div>
|
||||
|
||||
<p:messages id="messagesClientRole" showDetail="true" closable="true" styleClass="mt-3">
|
||||
<p:autoUpdate />
|
||||
</p:messages>
|
||||
</h:form>
|
||||
<div class="col-12">
|
||||
<fr:fieldSelect id="clientName"
|
||||
label="Client"
|
||||
value="#{roleGestionBean.clientName}"
|
||||
required="true"
|
||||
iconLeft="pi pi-box">
|
||||
<f:selectItem itemLabel="Sélectionner un client..." itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableClients}" />
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<f:facet name="footer">
|
||||
<p:commandButton value="Annuler"
|
||||
icon="pi pi-times"
|
||||
styleClass="p-button-text"
|
||||
onclick="PF('createClientRoleDialog').hide();"
|
||||
type="button" />
|
||||
<p:commandButton value="Créer"
|
||||
icon="pi pi-check"
|
||||
styleClass="p-button-success"
|
||||
action="#{roleGestionBean.createClientRole}"
|
||||
update=":formClientRoles :formKpis :formCreateClientRole"
|
||||
oncomplete="if (args && !args.validationFailed) PF('createClientRoleDialog').hide();" />
|
||||
</f:facet>
|
||||
</p:dialog>
|
||||
<div class="col-12">
|
||||
<fr:fieldTextarea id="clientRoleDesc"
|
||||
label="Description"
|
||||
value="#{roleGestionBean.newRole.description}"
|
||||
rows="3"
|
||||
placeholder="Description du rôle..."
|
||||
iconLeft="pi pi-align-left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fr:message id="messagesClientRole" showDetail="true" closable="true">
|
||||
<p:autoUpdate />
|
||||
</fr:message>
|
||||
</fr:formDialog>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG DE CONFIRMATION
|
||||
DIALOG DE CONFIRMATION (Freya Extension)
|
||||
================================================================ -->
|
||||
<!-- Le confirmDialog est géré par p:confirm dans les boutons de suppression -->
|
||||
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
|
||||
responsive="true" width="400">
|
||||
<p:commandButton value="Non" type="button"
|
||||
styleClass="p-button-text"
|
||||
icon="pi pi-times" />
|
||||
<p:commandButton value="Oui" type="button"
|
||||
styleClass="p-button-danger"
|
||||
icon="pi pi-check" />
|
||||
<fr:commandButton value="Non"
|
||||
type="button"
|
||||
text="true"
|
||||
icon="pi pi-times" />
|
||||
<fr:commandButton value="Oui"
|
||||
type="button"
|
||||
severity="danger"
|
||||
icon="pi pi-check" />
|
||||
</p:confirmDialog>
|
||||
</ui:define>
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<div class="card">
|
||||
<h5>Informations du compte</h5>
|
||||
<h:form id="formAccountInfo">
|
||||
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-4, col-12 md:col-8">
|
||||
<p:outputLabel for="username" value="Nom d'utilisateur" />
|
||||
<p:inputText id="username"
|
||||
value="#{userSessionBean.username}"
|
||||
readonly="true"
|
||||
styleClass="w-full" />
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6">
|
||||
<fr:fieldInput id="username"
|
||||
label="Nom d'utilisateur"
|
||||
value="#{userSessionBean.username}"
|
||||
disabled="true"
|
||||
iconLeft="pi pi-user"
|
||||
helpText="Votre identifiant unique" />
|
||||
</div>
|
||||
|
||||
<p:outputLabel for="email" value="Email" />
|
||||
<p:inputText id="email"
|
||||
value="#{userSessionBean.email}"
|
||||
readonly="true"
|
||||
styleClass="w-full" />
|
||||
<div class="col-12 md:col-6">
|
||||
<fr:fieldInput id="email"
|
||||
label="Email"
|
||||
value="#{userSessionBean.email}"
|
||||
disabled="true"
|
||||
iconLeft="pi pi-envelope"
|
||||
helpText="Votre adresse email" />
|
||||
</div>
|
||||
|
||||
<p:outputLabel for="fullName" value="Nom complet" />
|
||||
<p:inputText id="fullName"
|
||||
value="#{userSessionBean.fullName}"
|
||||
readonly="true"
|
||||
styleClass="w-full" />
|
||||
<div class="col-12 md:col-6">
|
||||
<fr:fieldInput id="fullName"
|
||||
label="Nom complet"
|
||||
value="#{userSessionBean.fullName}"
|
||||
disabled="true"
|
||||
iconLeft="pi pi-id-card"
|
||||
helpText="Votre nom complet" />
|
||||
</div>
|
||||
|
||||
<p:outputLabel for="mainRole" value="Rôle principal" />
|
||||
<p:inputText id="mainRole"
|
||||
value="#{userSessionBean.mainRole}"
|
||||
readonly="true"
|
||||
styleClass="w-full" />
|
||||
</p:panelGrid>
|
||||
<div class="col-12 md:col-6">
|
||||
<fr:fieldInput id="mainRole"
|
||||
label="Rôle principal"
|
||||
value="#{userSessionBean.mainRole}"
|
||||
disabled="true"
|
||||
iconLeft="pi pi-shield"
|
||||
helpText="Votre rôle dans l'application" />
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,35 +72,34 @@
|
||||
<h5>Préférences</h5>
|
||||
<h:form id="formPreferences">
|
||||
<div class="flex flex-column gap-3">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<span class="text-600">Thème des composants</span>
|
||||
<p:selectOneMenu value="#{guestPreferences.componentTheme}"
|
||||
styleClass="w-12rem">
|
||||
<f:selectItems value="#{guestPreferences.componentThemes}"
|
||||
var="theme"
|
||||
itemLabel="#{theme.name}"
|
||||
itemValue="#{theme.file}" />
|
||||
<p:ajax event="change" update="@form" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<span class="text-600">Mode sombre</span>
|
||||
<p:selectOneMenu value="#{guestPreferences.darkMode}"
|
||||
styleClass="w-12rem">
|
||||
<f:selectItem itemLabel="Clair" itemValue="light" />
|
||||
<f:selectItem itemLabel="Sombre" itemValue="dark" />
|
||||
<p:ajax event="change" update="@form" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<span class="text-600">Style d'input</span>
|
||||
<p:selectOneMenu value="#{guestPreferences.inputStyle}"
|
||||
styleClass="w-12rem">
|
||||
<f:selectItem itemLabel="Outlined" itemValue="outlined" />
|
||||
<f:selectItem itemLabel="Filled" itemValue="filled" />
|
||||
<p:ajax event="change" update="@form" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
<fr:fieldSelect id="componentTheme"
|
||||
label="Thème des composants"
|
||||
value="#{guestPreferences.componentTheme}"
|
||||
iconLeft="pi pi-palette">
|
||||
<f:selectItems value="#{guestPreferences.componentThemes}"
|
||||
var="theme"
|
||||
itemLabel="#{theme.name}"
|
||||
itemValue="#{theme.file}" />
|
||||
<p:ajax event="change" update="@form" />
|
||||
</fr:fieldSelect>
|
||||
|
||||
<fr:fieldSelect id="darkMode"
|
||||
label="Mode sombre"
|
||||
value="#{guestPreferences.darkMode}"
|
||||
iconLeft="pi pi-moon">
|
||||
<f:selectItem itemLabel="Clair" itemValue="light" />
|
||||
<f:selectItem itemLabel="Sombre" itemValue="dark" />
|
||||
<p:ajax event="change" update="@form" />
|
||||
</fr:fieldSelect>
|
||||
|
||||
<fr:fieldSelect id="inputStyle"
|
||||
label="Style d'input"
|
||||
value="#{guestPreferences.inputStyle}"
|
||||
iconLeft="pi pi-sliders-h">
|
||||
<f:selectItem itemLabel="Outlined" itemValue="outlined" />
|
||||
<f:selectItem itemLabel="Filled" itemValue="filled" />
|
||||
<p:ajax event="change" update="@form" />
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
@@ -97,29 +109,26 @@
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h5>Actions</h5>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<h:form>
|
||||
<p:commandButton
|
||||
value="Rafraîchir les informations"
|
||||
icon="pi pi-refresh"
|
||||
styleClass="p-button-secondary"
|
||||
action="#{userSessionBean.loadUserInfo}"
|
||||
update="formAccountInfo" />
|
||||
<fr:commandButton value="Rafraîchir les informations"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
action="#{userSessionBean.loadUserInfo}"
|
||||
update="formAccountInfo" />
|
||||
</h:form>
|
||||
<h:form>
|
||||
<p:commandButton
|
||||
value="Changer le mot de passe"
|
||||
icon="pi pi-key"
|
||||
styleClass="p-button-info"
|
||||
outcome="/pages/user-manager/users/profile" />
|
||||
<fr:commandButton value="Changer le mot de passe"
|
||||
icon="pi pi-key"
|
||||
severity="info"
|
||||
outcome="/pages/user-manager/users/profile" />
|
||||
</h:form>
|
||||
<h:form>
|
||||
<p:commandButton
|
||||
value="Sauvegarder les préférences"
|
||||
icon="pi pi-save"
|
||||
styleClass="p-button-success"
|
||||
action="#{settingsBean.savePreferences}"
|
||||
update="@form" />
|
||||
<fr:commandButton value="Sauvegarder les préférences"
|
||||
icon="pi pi-save"
|
||||
severity="success"
|
||||
action="#{settingsBean.savePreferences}"
|
||||
update="@form" />
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
<ui:define name="title">Synchronisation Keycloak - Lions User Manager</ui:define>
|
||||
|
||||
@@ -26,16 +26,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<p:commandButton
|
||||
<fr:commandButton
|
||||
icon="pi pi-question-circle"
|
||||
styleClass="p-button-rounded p-button-text p-button-help"
|
||||
rounded="true"
|
||||
text="true"
|
||||
severity="help"
|
||||
title="Aide"
|
||||
type="button"
|
||||
onclick="PF('helpDialog').show();" />
|
||||
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text">
|
||||
<i class="pi pi-arrow-left mr-2"></i>
|
||||
Retour
|
||||
</h:link>
|
||||
<fr:button value="Retour"
|
||||
icon="pi pi-arrow-left"
|
||||
severity="secondary"
|
||||
text="true"
|
||||
outcome="/pages/user-manager/users/list" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,51 +128,46 @@
|
||||
<div class="grid">
|
||||
<!-- Mot de passe -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="field">
|
||||
<label for="password" class="block text-900 font-medium mb-2">
|
||||
<i class="pi pi-lock text-500 mr-1"></i>
|
||||
Mot de passe <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:password id="password"
|
||||
value="#{userCreationBean.password}"
|
||||
styleClass="w-full"
|
||||
required="true"
|
||||
feedback="true"
|
||||
toggleMask="true"
|
||||
promptLabel="Entrez un mot de passe"
|
||||
weakLabel="Faible"
|
||||
goodLabel="Moyen"
|
||||
strongLabel="Fort"
|
||||
placeholder="Minimum 8 caractères">
|
||||
<f:validateLength minimum="8" maximum="100" />
|
||||
</p:password>
|
||||
<small class="text-500">
|
||||
<i class="pi pi-shield mr-1"></i>
|
||||
Au moins 8 caractères avec lettres et chiffres
|
||||
</small>
|
||||
</div>
|
||||
<fr:fieldPassword id="password"
|
||||
label="Mot de passe"
|
||||
value="#{userCreationBean.password}"
|
||||
required="true"
|
||||
feedback="true"
|
||||
toggleMask="true"
|
||||
promptLabel="Entrez un mot de passe"
|
||||
weakLabel="Faible"
|
||||
goodLabel="Moyen"
|
||||
strongLabel="Fort"
|
||||
placeholder="Minimum 8 caractères"
|
||||
iconLeft="pi pi-lock"
|
||||
helpText="Au moins 8 caractères avec lettres et chiffres">
|
||||
<f:validateLength for="input" minimum="8" maximum="100" />
|
||||
<p:ajax event="keyup"
|
||||
delay="500"
|
||||
listener="#{userCreationBean.validatePasswordMatch}"
|
||||
update="passwordConfirm passwordConfirmMsg"
|
||||
process="@this passwordConfirm" />
|
||||
</fr:fieldPassword>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation mot de passe -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="field">
|
||||
<label for="passwordConfirm" class="block text-900 font-medium mb-2">
|
||||
<i class="pi pi-lock text-500 mr-1"></i>
|
||||
Confirmer le mot de passe <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:password id="passwordConfirm"
|
||||
value="#{userCreationBean.passwordConfirm}"
|
||||
styleClass="w-full"
|
||||
required="true"
|
||||
feedback="false"
|
||||
toggleMask="true"
|
||||
placeholder="Confirmer le mot de passe">
|
||||
</p:password>
|
||||
<small class="text-500">
|
||||
<i class="pi pi-info-circle mr-1"></i>
|
||||
Doit correspondre au mot de passe
|
||||
</small>
|
||||
</div>
|
||||
<fr:fieldPassword id="passwordConfirm"
|
||||
label="Confirmer le mot de passe"
|
||||
value="#{userCreationBean.passwordConfirm}"
|
||||
required="true"
|
||||
feedback="false"
|
||||
toggleMask="true"
|
||||
placeholder="Confirmer le mot de passe"
|
||||
iconLeft="pi pi-lock"
|
||||
helpText="Doit correspondre au mot de passe">
|
||||
<p:ajax event="keyup"
|
||||
delay="500"
|
||||
listener="#{userCreationBean.validatePasswordMatch}"
|
||||
update="passwordConfirmMsg"
|
||||
process="@this password" />
|
||||
</fr:fieldPassword>
|
||||
<p:message id="passwordConfirmMsg" for="passwordConfirm" display="text" styleClass="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -220,26 +218,20 @@
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex flex-column gap-3">
|
||||
<!-- Compte activé -->
|
||||
<div class="flex align-items-center">
|
||||
<p:selectBooleanCheckbox id="enabled"
|
||||
value="#{userCreationBean.newUser.enabled}">
|
||||
</p:selectBooleanCheckbox>
|
||||
<label for="enabled" class="ml-2 mb-0 cursor-pointer">
|
||||
<span class="font-semibold text-900">Compte activé</span>
|
||||
<small class="block text-500">L'utilisateur peut se connecter immédiatement</small>
|
||||
</label>
|
||||
</div>
|
||||
<fr:fieldCheckbox id="enabled"
|
||||
label=""
|
||||
value="#{userCreationBean.newUser.enabled}"
|
||||
checkboxLabel="Compte activé">
|
||||
<small class="block text-500 mt-1">L'utilisateur peut se connecter immédiatement</small>
|
||||
</fr:fieldCheckbox>
|
||||
|
||||
<!-- Email vérifié -->
|
||||
<div class="flex align-items-center">
|
||||
<p:selectBooleanCheckbox id="emailVerified"
|
||||
value="#{userCreationBean.newUser.emailVerified}">
|
||||
</p:selectBooleanCheckbox>
|
||||
<label for="emailVerified" class="ml-2 mb-0 cursor-pointer">
|
||||
<span class="font-semibold text-900">Email vérifié</span>
|
||||
<small class="block text-500">Marquer l'email comme vérifié</small>
|
||||
</label>
|
||||
</div>
|
||||
<fr:fieldCheckbox id="emailVerified"
|
||||
label=""
|
||||
value="#{userCreationBean.newUser.emailVerified}"
|
||||
checkboxLabel="Email vérifié">
|
||||
<small class="block text-500 mt-1">Marquer l'email comme vérifié</small>
|
||||
</fr:fieldCheckbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,16 +324,17 @@
|
||||
validateClient="true" />
|
||||
|
||||
<!-- Bouton Réinitialiser -->
|
||||
<p:commandButton value="Réinitialiser"
|
||||
<fr:commandButton value="Réinitialiser"
|
||||
icon="pi pi-refresh"
|
||||
styleClass="p-button-secondary p-button-outlined"
|
||||
severity="secondary"
|
||||
outlined="true"
|
||||
action="#{userCreationBean.resetForm}"
|
||||
update=":formUserCreation"
|
||||
immediate="true">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Voulez-vous vraiment réinitialiser le formulaire ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
|
||||
<!-- Bouton Annuler -->
|
||||
<fr:commandButton value="Annuler"
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
placeholder="ex: jean.dupont@example.com"
|
||||
helpText="Adresse email valide">
|
||||
<f:validateRegex for="input" pattern="^[A-Za-z0-9+_.-]+@(.+)$" />
|
||||
<p:ajax event="keyup" delay="500" update="previewPanel" />
|
||||
</fr:fieldInput>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +104,7 @@
|
||||
placeholder="ex: Jean"
|
||||
helpText="Prénom de l'utilisateur">
|
||||
<f:validateLength for="input" minimum="2" maximum="100" />
|
||||
<p:ajax event="keyup" delay="500" update="previewPanel" />
|
||||
</fr:fieldInput>
|
||||
</div>
|
||||
|
||||
@@ -115,6 +117,7 @@
|
||||
placeholder="ex: Dupont"
|
||||
helpText="Nom de famille de l'utilisateur">
|
||||
<f:validateLength for="input" minimum="2" maximum="100" />
|
||||
<p:ajax event="keyup" delay="500" update="previewPanel" />
|
||||
</fr:fieldInput>
|
||||
</div>
|
||||
|
||||
@@ -157,26 +160,16 @@
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex flex-column gap-3">
|
||||
<!-- Compte activé -->
|
||||
<div class="flex align-items-center">
|
||||
<p:selectBooleanCheckbox id="enabled"
|
||||
value="#{userProfilBean.user.enabled}">
|
||||
</p:selectBooleanCheckbox>
|
||||
<label for="enabled" class="ml-2 mb-0 cursor-pointer">
|
||||
<span class="font-semibold text-900">Compte activé</span>
|
||||
<small class="block text-500">L'utilisateur peut se connecter</small>
|
||||
</label>
|
||||
</div>
|
||||
<fr:fieldCheckbox id="enabled"
|
||||
label="Compte activé"
|
||||
value="#{userProfilBean.user.enabled}"
|
||||
helpText="L'utilisateur peut se connecter" />
|
||||
|
||||
<!-- Email vérifié -->
|
||||
<div class="flex align-items-center">
|
||||
<p:selectBooleanCheckbox id="emailVerified"
|
||||
value="#{userProfilBean.user.emailVerified}">
|
||||
</p:selectBooleanCheckbox>
|
||||
<label for="emailVerified" class="ml-2 mb-0 cursor-pointer">
|
||||
<span class="font-semibold text-900">Email vérifié</span>
|
||||
<small class="block text-500">Marquer l'email comme vérifié</small>
|
||||
</label>
|
||||
</div>
|
||||
<fr:fieldCheckbox id="emailVerified"
|
||||
label="Email vérifié"
|
||||
value="#{userProfilBean.user.emailVerified}"
|
||||
helpText="Marquer l'email comme vérifié" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,9 +186,11 @@
|
||||
<div class="card sticky" style="top: 1rem;">
|
||||
<div class="flex align-items-center gap-2 mb-4">
|
||||
<i class="pi pi-eye text-blue-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Aperçu</h5>
|
||||
<h5 class="m-0">Aperçu <small class="text-500 font-normal">(Temps réel)</small></h5>
|
||||
</div>
|
||||
|
||||
<h:panelGroup id="previewPanel">
|
||||
|
||||
<!-- Avatar Preview -->
|
||||
<div class="text-center mb-4">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600)); display: flex; align-items-center; justify-content: center; margin: 0 auto; font-size: 2rem; font-weight: bold; color: white; box-shadow: 0 4px 12px rgba(0,0,0,0.12);">
|
||||
@@ -250,6 +245,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -309,17 +306,9 @@
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG DE CONFIRMATION
|
||||
DIALOG DE CONFIRMATION (Freya Extension)
|
||||
================================================================ -->
|
||||
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
|
||||
responsive="true" width="400">
|
||||
<p:commandButton value="Non" type="button"
|
||||
styleClass="p-button-text"
|
||||
icon="pi pi-times" />
|
||||
<p:commandButton value="Oui" type="button"
|
||||
styleClass="p-button-primary"
|
||||
icon="pi pi-check" />
|
||||
</p:confirmDialog>
|
||||
<!-- Suppression désactivée - utiliser la page liste pour supprimer des utilisateurs -->
|
||||
</ui:define>
|
||||
|
||||
</ui:composition>
|
||||
|
||||
@@ -27,10 +27,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<fr:commandButton
|
||||
value="Importer"
|
||||
icon="pi pi-upload"
|
||||
severity="info"
|
||||
outlined="true"
|
||||
onclick="PF('importUsersDialog').show()"
|
||||
type="button" />
|
||||
<fr:commandButton
|
||||
value="Exporter"
|
||||
icon="pi pi-download"
|
||||
severity="secondary"
|
||||
outlined="true"
|
||||
action="#{userListBean.exportToCSV}"
|
||||
ajax="false" />
|
||||
<fr:commandButton
|
||||
value="Rafraîchir"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined="true"
|
||||
action="#{userListBean.refreshData}"
|
||||
update=":formUserList"
|
||||
process="@this" />
|
||||
@@ -149,76 +164,67 @@
|
||||
SECTION RECHERCHE ET FILTRES
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="flex align-items-center gap-2 mb-3">
|
||||
<i class="pi pi-search text-blue-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Recherche et Filtres</h5>
|
||||
</div>
|
||||
<fr:panel header="Recherche et Filtres"
|
||||
toggleable="true"
|
||||
collapsed="false">
|
||||
<f:facet name="icons">
|
||||
<h:panelGroup rendered="#{userListBean.searchText != null or userListBean.selectedStatut != null}">
|
||||
<fr:tag value="Filtres actifs"
|
||||
severity="info"
|
||||
icon="pi pi-filter"
|
||||
styleClass="mr-2" />
|
||||
</h:panelGroup>
|
||||
</f:facet>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<div class="field">
|
||||
<label for="searchText" class="block text-900 font-medium mb-2">
|
||||
<i class="pi pi-search text-500 mr-1"></i>
|
||||
Recherche
|
||||
</label>
|
||||
<p:inputText id="searchText"
|
||||
value="#{userListBean.searchText}"
|
||||
styleClass="w-full"
|
||||
placeholder="Nom, email...">
|
||||
<p:ajax event="keyup"
|
||||
delay="500"
|
||||
update=":formUserList:userTable"
|
||||
listener="#{userListBean.search}" />
|
||||
</p:inputText>
|
||||
</div>
|
||||
<fr:fieldInput id="searchText"
|
||||
label="Recherche"
|
||||
value="#{userListBean.searchText}"
|
||||
placeholder="Nom, email...">
|
||||
<p:ajax event="keyup"
|
||||
delay="500"
|
||||
update=":formUserList:userTable"
|
||||
listener="#{userListBean.search}" />
|
||||
</fr:fieldInput>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="field">
|
||||
<label for="realmFilter" class="block text-900 font-medium mb-2">
|
||||
<i class="pi pi-globe text-500 mr-1"></i>
|
||||
Realm
|
||||
</label>
|
||||
<p:selectOneMenu id="realmFilter"
|
||||
value="#{userListBean.realmName}"
|
||||
styleClass="w-full">
|
||||
<f:selectItems value="#{userListBean.availableRealms}" />
|
||||
<p:ajax event="change"
|
||||
update=":formUserList:userTable"
|
||||
listener="#{userListBean.search}" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
<fr:fieldSelect id="realmFilter"
|
||||
label="Realm"
|
||||
value="#{userListBean.realmName}">
|
||||
<f:selectItems value="#{userListBean.availableRealms}" />
|
||||
<p:ajax event="change"
|
||||
update=":formUserList:userTable"
|
||||
listener="#{userListBean.search}" />
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<div class="field">
|
||||
<label for="statutFilter" class="block text-900 font-medium mb-2">
|
||||
<i class="pi pi-filter text-500 mr-1"></i>
|
||||
Statut
|
||||
</label>
|
||||
<p:selectOneMenu id="statutFilter"
|
||||
value="#{userListBean.selectedStatut}"
|
||||
styleClass="w-full">
|
||||
<f:selectItem itemLabel="Tous" itemValue="#{null}" />
|
||||
<f:selectItems value="#{userListBean.statutOptions}" />
|
||||
<p:ajax update=":formUserList:userTable"
|
||||
listener="#{userListBean.search}" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
<fr:fieldSelect id="statutFilter"
|
||||
label="Statut"
|
||||
value="#{userListBean.selectedStatut}">
|
||||
<f:selectItem itemLabel="Tous" itemValue="#{null}" />
|
||||
<f:selectItems value="#{userListBean.statutOptions}" />
|
||||
<p:ajax event="change"
|
||||
update=":formUserList:userTable"
|
||||
listener="#{userListBean.search}" />
|
||||
</fr:fieldSelect>
|
||||
</div>
|
||||
|
||||
<div class="col-12 lg:col-2 flex align-items-end">
|
||||
<div class="col-12 md:col-6 lg:col-2 flex align-items-end">
|
||||
<fr:commandButton
|
||||
value="Réinitialiser"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined="true"
|
||||
styleClass="w-full"
|
||||
action="#{userListBean.resetSearch}"
|
||||
update=":formUserList:userTable @form" />
|
||||
update=":formUserList:userTable @form"
|
||||
rendered="#{userListBean.searchText != null or userListBean.selectedStatut != null}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fr:panel>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
@@ -244,16 +250,16 @@
|
||||
var="user"
|
||||
rowKey="#{user.id}"
|
||||
paginator="true"
|
||||
rows="#{userListBean.pageSize != null ? userListBean.pageSize : 10}"
|
||||
rowsPerPageTemplate="10,20,50"
|
||||
rows="#{userListBean.pageSize != null ? userListBean.pageSize : 25}"
|
||||
rowsPerPageTemplate="10,25,50,100"
|
||||
emptyMessage="Aucun utilisateur trouvé"
|
||||
reflow="true"
|
||||
responsiveLayout="scroll"
|
||||
styleClass="p-datatable-striped">
|
||||
|
||||
<p:ajax event="page" listener="#{userListBean.onPageChange}" update=":formUserList:userTable :formUserList:formMessages" />
|
||||
|
||||
<!-- Colonne Avatar + Username -->
|
||||
<p:column headerText="Utilisateur" sortBy="#{user.username}" style="width: 250px">
|
||||
<p:column headerText="Utilisateur" sortBy="#{user.username}" style="width: 250px" priority="1">
|
||||
<div class="flex align-items-center gap-3">
|
||||
<div class="border-circle bg-primary text-white flex align-items-center justify-content-center"
|
||||
style="width: 42px; height: 42px; flex-shrink: 0;">
|
||||
@@ -269,7 +275,7 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Email -->
|
||||
<p:column headerText="Email" sortBy="#{user.email}" style="width: 250px">
|
||||
<p:column headerText="Email" sortBy="#{user.email}" style="width: 250px" priority="2">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-envelope text-500"></i>
|
||||
<span class="text-900">#{user.email}</span>
|
||||
@@ -280,13 +286,13 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Statut -->
|
||||
<p:column headerText="Statut" sortBy="#{user.enabled}" style="width: 120px; text-align: center">
|
||||
<p:column headerText="Statut" sortBy="#{user.enabled}" style="width: 120px; text-align: center" priority="3">
|
||||
<fr:tag value="#{user.enabled ? 'ACTIF' : 'INACTIF'}"
|
||||
severity="#{user.enabled ? 'success' : 'danger'}" />
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Rôles -->
|
||||
<p:column headerText="Rôles" style="width: 250px">
|
||||
<p:column headerText="Rôles" style="width: 250px" priority="5">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<h:outputText value="Aucun rôle" styleClass="text-500 text-sm"
|
||||
rendered="#{user.realmRoles == null or user.realmRoles.size() == 0}" />
|
||||
@@ -304,165 +310,256 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Actions -->
|
||||
<p:column headerText="Actions" style="width: 250px; text-align: center">
|
||||
<div class="flex gap-1 justify-content-center flex-wrap">
|
||||
<!-- Bouton Voir Profil -->
|
||||
<p:column headerText="Actions" style="width: 120px; text-align: center" priority="4">
|
||||
<div class="flex gap-1 justify-content-center">
|
||||
<!-- Bouton Voir (Action principale) -->
|
||||
<p:button icon="pi pi-eye"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-info"
|
||||
styleClass="p-button-rounded p-button-sm p-button-info"
|
||||
title="Voir le profil"
|
||||
outcome="/pages/user-manager/users/view">
|
||||
<f:param name="userId" value="#{user.id}" />
|
||||
<f:param name="realm" value="#{userListBean.realmName}" />
|
||||
</p:button>
|
||||
|
||||
<!-- Bouton Modifier -->
|
||||
<p:button icon="pi pi-pencil"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm"
|
||||
title="Modifier"
|
||||
outcome="/pages/user-manager/users/edit">
|
||||
<f:param name="userId" value="#{user.id}" />
|
||||
<f:param name="realm" value="#{userListBean.realmName}" />
|
||||
</p:button>
|
||||
<!-- Menu Actions Secondaires -->
|
||||
<p:splitButton icon="pi pi-ellipsis-v"
|
||||
styleClass="p-button-rounded p-button-sm p-button-secondary"
|
||||
menuStyleClass="text-left">
|
||||
<p:menuitem value="Modifier"
|
||||
icon="pi pi-pencil"
|
||||
outcome="/pages/user-manager/users/edit">
|
||||
<f:param name="userId" value="#{user.id}" />
|
||||
<f:param name="realm" value="#{userListBean.realmName}" />
|
||||
</p:menuitem>
|
||||
|
||||
<!-- Bouton Gérer les Rôles -->
|
||||
<p:button icon="pi pi-key"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-help"
|
||||
title="Gérer les rôles"
|
||||
outcome="/pages/user-manager/roles/assign">
|
||||
<f:param name="userId" value="#{user.id}" />
|
||||
<f:param name="realm" value="#{userListBean.realmName}" />
|
||||
</p:button>
|
||||
<p:menuitem value="Gérer les Rôles"
|
||||
icon="pi pi-key"
|
||||
outcome="/pages/user-manager/users/edit">
|
||||
<f:param name="userId" value="#{user.id}" />
|
||||
<f:param name="realm" value="#{userListBean.realmName}" />
|
||||
</p:menuitem>
|
||||
|
||||
<!-- Bouton Désactiver (si actif) -->
|
||||
<p:commandButton icon="pi pi-ban"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-warning"
|
||||
title="Désactiver"
|
||||
action="#{userListBean.deactivateUserAction}"
|
||||
update=":formUserList:userTable :formUserList:formMessages"
|
||||
process="@this"
|
||||
rendered="#{user.enabled}">
|
||||
<f:attribute name="userId" value="#{user.id}" />
|
||||
<p:confirm header="Désactiver l'utilisateur"
|
||||
message="Voulez-vous vraiment désactiver l'utilisateur #{user.username} ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
<p:divider />
|
||||
|
||||
<!-- Bouton Activer (si inactif) -->
|
||||
<fr:commandButton icon="pi pi-check"
|
||||
rounded="true"
|
||||
text="true"
|
||||
size="small"
|
||||
severity="success"
|
||||
title="Activer"
|
||||
action="#{userListBean.activateUserAction}"
|
||||
update=":formUserList:userTable :formUserList:formMessages"
|
||||
process="@this"
|
||||
rendered="#{not user.enabled}">
|
||||
<f:attribute name="userId" value="#{user.id}" />
|
||||
</fr:commandButton>
|
||||
<!-- Désactiver (si utilisateur actif) -->
|
||||
<p:menuitem value="Désactiver"
|
||||
icon="pi pi-ban"
|
||||
styleClass="text-orange-500"
|
||||
action="#{userListBean.deactivateUserAction}"
|
||||
update=":formUserList:userTable :formUserList:formMessages"
|
||||
process="@this"
|
||||
rendered="#{user.enabled}">
|
||||
<f:attribute name="userId" value="#{user.id}" />
|
||||
<p:confirm header="Désactiver l'utilisateur"
|
||||
message="Voulez-vous vraiment désactiver l'utilisateur #{user.username} ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:menuitem>
|
||||
|
||||
<!-- Bouton Supprimer -->
|
||||
<p:commandButton icon="pi pi-trash"
|
||||
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger"
|
||||
title="Supprimer"
|
||||
action="#{userListBean.deleteUserAction}"
|
||||
update=":formUserList:userTable :formUserList:formMessages"
|
||||
process="@this">
|
||||
<f:attribute name="userId" value="#{user.id}" />
|
||||
<p:confirm header="Supprimer l'utilisateur"
|
||||
message="Voulez-vous vraiment supprimer définitivement l'utilisateur #{user.username} ? Cette action est irréversible."
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
<!-- Activer (si utilisateur inactif) -->
|
||||
<p:menuitem value="Activer"
|
||||
icon="pi pi-check"
|
||||
styleClass="text-green-500"
|
||||
action="#{userListBean.activateUserAction}"
|
||||
update=":formUserList:userTable :formUserList:formMessages"
|
||||
process="@this"
|
||||
rendered="#{!user.enabled}">
|
||||
<f:attribute name="userId" value="#{user.id}" />
|
||||
<p:confirm header="Activer l'utilisateur"
|
||||
message="Voulez-vous vraiment activer l'utilisateur #{user.username} ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:menuitem>
|
||||
|
||||
<p:menuitem value="Supprimer"
|
||||
icon="pi pi-trash"
|
||||
styleClass="text-red-500"
|
||||
action="#{userListBean.deleteUserAction}"
|
||||
update=":formUserList:userTable :formUserList:formMessages"
|
||||
process="@this">
|
||||
<f:attribute name="userId" value="#{user.id}" />
|
||||
<p:confirm header="Supprimer l'utilisateur"
|
||||
message="Voulez-vous vraiment supprimer définitivement l'utilisateur #{user.username} ? Cette action est IRRÉVERSIBLE."
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:menuitem>
|
||||
</p:splitButton>
|
||||
</div>
|
||||
</p:column>
|
||||
</p:dataTable>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
ACTIONS RAPIDES
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="flex align-items-center gap-2 mb-3">
|
||||
<i class="pi pi-bolt text-orange-500" style="font-size: 1.5rem"></i>
|
||||
<h5 class="m-0">Actions Rapides</h5>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<fr:commandButton
|
||||
value="Créer un Utilisateur"
|
||||
icon="pi pi-user-plus"
|
||||
severity="success"
|
||||
styleClass="w-full"
|
||||
outcome="/pages/user-manager/users/create" />
|
||||
</div>
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<fr:commandButton
|
||||
value="Exporter la Liste"
|
||||
icon="pi pi-download"
|
||||
severity="secondary"
|
||||
styleClass="w-full"
|
||||
action="#{userListBean.exportToCSV}"
|
||||
ajax="false" />
|
||||
</div>
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<fr:commandButton
|
||||
value="Importer des Utilisateurs"
|
||||
icon="pi pi-upload"
|
||||
severity="info"
|
||||
styleClass="w-full"
|
||||
onclick="PF('importUsersDialog').show()"
|
||||
type="button" />
|
||||
</div>
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<fr:commandButton
|
||||
value="Gestion des Rôles"
|
||||
icon="pi pi-shield"
|
||||
severity="primary"
|
||||
styleClass="w-full"
|
||||
outcome="/pages/user-manager/roles/list" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG D'IMPORT
|
||||
DIALOG D'IMPORT CSV (Freya formDialog)
|
||||
================================================================ -->
|
||||
<p:dialog id="importUsersDialog"
|
||||
widgetVar="importUsersDialog"
|
||||
header="Importer des Utilisateurs"
|
||||
<p:dialog widgetVar="importUsersDialog"
|
||||
header="Importer des Utilisateurs depuis CSV"
|
||||
modal="true"
|
||||
resizable="false"
|
||||
styleClass="w-full md:w-30rem">
|
||||
<h:form id="formImportUsers">
|
||||
<div class="flex flex-column gap-3">
|
||||
<p class="text-600">
|
||||
Importez des utilisateurs depuis un fichier CSV ou JSON.
|
||||
</p>
|
||||
<p:fileUpload mode="simple"
|
||||
skinSimple="true"
|
||||
accept=".csv,.json"
|
||||
label="Sélectionner un fichier" />
|
||||
<div class="flex justify-content-end gap-2 mt-3">
|
||||
<fr:commandButton value="Annuler"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
onclick="PF('importUsersDialog').hide()"
|
||||
type="button" />
|
||||
<fr:commandButton value="Importer"
|
||||
icon="pi pi-upload"
|
||||
severity="success"
|
||||
action="#{userListBean.importUsers}"
|
||||
update=":formUserList"
|
||||
oncomplete="PF('importUsersDialog').hide()" />
|
||||
responsive="true"
|
||||
width="600"
|
||||
showEffect="fade"
|
||||
hideEffect="fade"
|
||||
closeOnEscape="true">
|
||||
|
||||
<h:form id="formImportDialog">
|
||||
<!-- Instructions -->
|
||||
<div class="surface-100 border-round p-3 mb-4">
|
||||
<div class="flex align-items-start gap-2">
|
||||
<i class="pi pi-info-circle text-blue-500 mt-1"></i>
|
||||
<div>
|
||||
<h6 class="mt-0 mb-2">Format du fichier CSV requis:</h6>
|
||||
<ul class="text-600 text-sm mt-0 mb-0 pl-3">
|
||||
<li>En-tête: <code class="bg-white px-2 py-1 border-round">username,prenom,nom,email</code></li>
|
||||
<li>Encodage: UTF-8</li>
|
||||
<li>Séparateur: virgule (,)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template CSV téléchargeable -->
|
||||
<div class="mb-4">
|
||||
<h6 class="mb-2">Télécharger le template CSV:</h6>
|
||||
<fr:commandButton value="Télécharger Template CSV"
|
||||
icon="pi pi-download"
|
||||
severity="info"
|
||||
outlined="true"
|
||||
styleClass="w-full"
|
||||
action="#{userListBean.downloadCSVTemplate}"
|
||||
ajax="false" />
|
||||
<small class="text-500 mt-1">
|
||||
Utilisez ce template pour préparer votre fichier d'import
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<fr:divider />
|
||||
|
||||
<!-- Upload de fichier -->
|
||||
<div class="mb-3">
|
||||
<h6 class="mb-3">Sélectionner le fichier CSV:</h6>
|
||||
<p:fileUpload id="csvFileUpload"
|
||||
mode="simple"
|
||||
skinSimple="true"
|
||||
label="Choisir un fichier CSV"
|
||||
chooseIcon="pi pi-folder-open"
|
||||
accept=".csv"
|
||||
listener="#{userListBean.handleFileUpload}"
|
||||
update=":formUserList:userTable :formUserList:formMessages"
|
||||
oncomplete="PF('importUsersDialog').hide()"
|
||||
styleClass="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="surface-50 border-round p-3">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-lightbulb text-orange-500"></i>
|
||||
<small class="text-600">
|
||||
<strong>Astuce:</strong> Exportez d'abord vos utilisateurs existants pour voir le format attendu
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</p:dialog>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG RÉSULTATS D'IMPORT
|
||||
================================================================ -->
|
||||
<p:dialog widgetVar="importResultDialog"
|
||||
header="Résultats de l'Import CSV"
|
||||
modal="true"
|
||||
responsive="true"
|
||||
width="700"
|
||||
showEffect="fade"
|
||||
hideEffect="fade"
|
||||
closeOnEscape="true">
|
||||
|
||||
<h:form id="formImportResult">
|
||||
<h:panelGroup rendered="#{userListBean.lastImportResult != null}">
|
||||
<!-- Résumé -->
|
||||
<div class="mb-4">
|
||||
<h6 class="mb-3">Résumé de l'import:</h6>
|
||||
<div class="grid">
|
||||
<div class="col-4">
|
||||
<div class="surface-100 border-round p-3 text-center">
|
||||
<div class="text-500 text-xs uppercase mb-1">Total lignes</div>
|
||||
<div class="text-900 font-bold text-2xl">#{userListBean.lastImportResult.totalLines}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="surface-100 border-round p-3 text-center border-left-3 border-green-500">
|
||||
<div class="text-500 text-xs uppercase mb-1">Succès</div>
|
||||
<div class="text-green-600 font-bold text-2xl">#{userListBean.lastImportResult.successCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="surface-100 border-round p-3 text-center border-left-3 border-red-500">
|
||||
<div class="text-500 text-xs uppercase mb-1">Erreurs</div>
|
||||
<div class="text-red-600 font-bold text-2xl">#{userListBean.lastImportResult.errorCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des erreurs détaillées -->
|
||||
<h:panelGroup rendered="#{userListBean.lastImportResult.errorCount > 0}">
|
||||
<fr:divider align="left">
|
||||
<span class="text-red-600 font-bold">Détails des Erreurs</span>
|
||||
</fr:divider>
|
||||
|
||||
<p:dataTable value="#{userListBean.lastImportResult.errors}"
|
||||
var="error"
|
||||
paginator="true"
|
||||
rows="10"
|
||||
rowsPerPageTemplate="10,20,50"
|
||||
emptyMessage="Aucune erreur"
|
||||
styleClass="p-datatable-sm p-datatable-striped">
|
||||
|
||||
<p:column headerText="Ligne" style="width: 80px; text-align: center">
|
||||
<fr:tag value="#{error.lineNumber}"
|
||||
severity="danger"
|
||||
styleClass="font-mono" />
|
||||
</p:column>
|
||||
|
||||
<p:column headerText="Type d'erreur" style="width: 150px">
|
||||
<fr:tag value="#{error.errorType}"
|
||||
severity="#{error.errorType == 'VALIDATION_ERROR' ? 'warning' : 'danger'}" />
|
||||
</p:column>
|
||||
|
||||
<p:column headerText="Champ" style="width: 120px">
|
||||
<span class="font-semibold text-900">#{error.field != null ? error.field : '-'}</span>
|
||||
</p:column>
|
||||
|
||||
<p:column headerText="Message d'erreur">
|
||||
<div class="flex flex-column gap-1">
|
||||
<span class="text-900">#{error.message}</span>
|
||||
<h:panelGroup rendered="#{error.lineContent != null and error.lineContent.length() > 0}">
|
||||
<code class="text-xs bg-red-50 text-red-900 p-2 border-round block overflow-x-auto">#{error.lineContent}</code>
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
</p:column>
|
||||
</p:dataTable>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- Message de succès complet -->
|
||||
<h:panelGroup rendered="#{userListBean.lastImportResult.errorCount == 0}">
|
||||
<div class="surface-100 border-left-3 border-green-500 border-round p-4 text-center">
|
||||
<i class="pi pi-check-circle text-green-500" style="font-size: 3rem"></i>
|
||||
<h5 class="text-green-600 mt-3 mb-2">Import réussi!</h5>
|
||||
<p class="text-600 m-0">
|
||||
Tous les utilisateurs ont été importés avec succès.
|
||||
</p>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-content-end gap-2 mt-4">
|
||||
<fr:commandButton value="Fermer"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
onclick="PF('importResultDialog').hide()"
|
||||
type="button" />
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
</h:form>
|
||||
</p:dialog>
|
||||
|
||||
|
||||
@@ -333,26 +333,29 @@
|
||||
<span>Gestion du Profil</span>
|
||||
</h4>
|
||||
<div class="flex flex-column gap-2">
|
||||
<p:commandButton value="Modifier mon profil"
|
||||
icon="pi pi-pencil"
|
||||
styleClass="p-button-outlined w-full justify-content-start"
|
||||
disabled="true">
|
||||
<fr:commandButton value="Modifier mon profil"
|
||||
icon="pi pi-pencil"
|
||||
outlined="true"
|
||||
styleClass="w-full justify-content-start"
|
||||
disabled="true">
|
||||
<f:attribute name="data-tooltip" value="Fonctionnalité gérée par Keycloak"/>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
|
||||
<p:commandButton value="Changer mon mot de passe"
|
||||
icon="pi pi-key"
|
||||
styleClass="p-button-outlined w-full justify-content-start"
|
||||
disabled="true">
|
||||
<fr:commandButton value="Changer mon mot de passe"
|
||||
icon="pi pi-key"
|
||||
outlined="true"
|
||||
styleClass="w-full justify-content-start"
|
||||
disabled="true">
|
||||
<f:attribute name="data-tooltip" value="Utilisez le portail Keycloak"/>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
|
||||
<p:commandButton value="Paramètres de sécurité"
|
||||
icon="pi pi-shield"
|
||||
styleClass="p-button-outlined w-full justify-content-start"
|
||||
disabled="true">
|
||||
<fr:commandButton value="Paramètres de sécurité"
|
||||
icon="pi pi-shield"
|
||||
outlined="true"
|
||||
styleClass="w-full justify-content-start"
|
||||
disabled="true">
|
||||
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -365,28 +368,33 @@
|
||||
<span>Sessions et Sécurité</span>
|
||||
</h4>
|
||||
<div class="flex flex-column gap-2">
|
||||
<p:commandButton value="Voir mes sessions actives"
|
||||
icon="pi pi-desktop"
|
||||
styleClass="p-button-outlined p-button-info w-full justify-content-start"
|
||||
disabled="true">
|
||||
<fr:commandButton value="Voir mes sessions actives"
|
||||
icon="pi pi-desktop"
|
||||
outlined="true"
|
||||
severity="info"
|
||||
styleClass="w-full justify-content-start"
|
||||
disabled="true">
|
||||
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
|
||||
<p:commandButton value="Historique des connexions"
|
||||
icon="pi pi-history"
|
||||
styleClass="p-button-outlined p-button-secondary w-full justify-content-start"
|
||||
disabled="true">
|
||||
<fr:commandButton value="Historique des connexions"
|
||||
icon="pi pi-history"
|
||||
outlined="true"
|
||||
severity="secondary"
|
||||
styleClass="w-full justify-content-start"
|
||||
disabled="true">
|
||||
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
|
||||
<p:commandButton value="Se déconnecter"
|
||||
icon="pi pi-sign-out"
|
||||
styleClass="p-button-danger w-full justify-content-start"
|
||||
action="#{userSessionBean.logout}">
|
||||
<fr:commandButton value="Se déconnecter"
|
||||
icon="pi pi-sign-out"
|
||||
severity="danger"
|
||||
styleClass="w-full justify-content-start"
|
||||
action="#{userSessionBean.logout}">
|
||||
<p:confirm header="Confirmation de déconnexion"
|
||||
message="Êtes-vous sûr de vouloir vous déconnecter ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
</fr:commandButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,16 +405,19 @@
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG DE CONFIRMATION
|
||||
DIALOG DE CONFIRMATION (Freya Extension)
|
||||
================================================================ -->
|
||||
<!-- Le confirmDialog est géré par p:confirm dans le bouton de déconnexion -->
|
||||
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
|
||||
responsive="true" width="400">
|
||||
<p:commandButton value="Non" type="button"
|
||||
styleClass="p-button-text"
|
||||
icon="pi pi-times" />
|
||||
<p:commandButton value="Oui" type="button"
|
||||
styleClass="p-button-danger"
|
||||
icon="pi pi-check" />
|
||||
<fr:commandButton value="Non"
|
||||
type="button"
|
||||
text="true"
|
||||
icon="pi pi-times" />
|
||||
<fr:commandButton value="Oui"
|
||||
type="button"
|
||||
severity="danger"
|
||||
icon="pi pi-check" />
|
||||
</p:confirmDialog>
|
||||
|
||||
<!-- Animation CSS pour le badge "Connecté" -->
|
||||
|
||||
@@ -140,11 +140,11 @@
|
||||
<div class="mb-3 pb-3 border-bottom-1 surface-border">
|
||||
<label class="block text-600 font-medium mb-2 text-sm">Rôles Realm assignés</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<h:outputText value="Aucun rôle"
|
||||
<h:outputText value="Aucun rôle"
|
||||
styleClass="text-500 text-sm"
|
||||
rendered="#{userProfilBean.user.realmRoles == null or userProfilBean.user.realmRoles.size() == 0}" />
|
||||
<ui:repeat value="#{userProfilBean.user.realmRoles}" var="role">
|
||||
<p:badge value="#{role}" severity="info" styleClass="text-sm"></p:badge>
|
||||
<fr:tag value="#{role}" severity="info" styleClass="text-sm" />
|
||||
</ui:repeat>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,9 +152,9 @@
|
||||
<div class="mb-3 pb-3 border-bottom-1 surface-border">
|
||||
<label class="block text-600 font-medium mb-1 text-sm">Statut du compte</label>
|
||||
<div class="flex align-items-center">
|
||||
<p:tag value="#{userProfilBean.user.enabled ? 'ACTIF' : 'INACTIF'}"
|
||||
severity="#{userProfilBean.user.enabled ? 'success' : 'danger'}"
|
||||
styleClass="text-sm"></p:tag>
|
||||
<fr:tag value="#{userProfilBean.user.enabled ? 'ACTIF' : 'INACTIF'}"
|
||||
severity="#{userProfilBean.user.enabled ? 'success' : 'danger'}"
|
||||
styleClass="text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
<f:param name="userId" value="#{userProfilBean.userId}" />
|
||||
<f:param name="realm" value="#{userProfilBean.realmName}" />
|
||||
</fr:commandButton>
|
||||
|
||||
@@ -37,18 +37,18 @@
|
||||
<!-- Gestion Rôles -->
|
||||
<p:submenu id="m_roles" label="Gestion Rôles" icon="pi pi-shield">
|
||||
<p:menuitem id="m_roles_list" value="Liste des Rôles" icon="pi pi-list" outcome="/pages/user-manager/roles/list" />
|
||||
<p:menuitem id="m_roles_assign" value="Attribution Rôles" icon="pi pi-key" outcome="/pages/user-manager/roles/assign" />
|
||||
</p:submenu>
|
||||
|
||||
<!-- Audit -->
|
||||
<p:submenu id="m_audit" label="Audit" icon="pi pi-history">
|
||||
<p:menuitem id="m_audit_logs" value="Journal d'Audit" icon="pi pi-file-o" outcome="/pages/user-manager/audit/logs" />
|
||||
</p:submenu>
|
||||
|
||||
<!-- Synchronisation -->
|
||||
|
||||
<!-- Synchronisation - DÉSACTIVÉ: page stub non implémentée
|
||||
<p:submenu id="m_sync" label="Synchronisation" icon="pi pi-sync">
|
||||
<p:menuitem id="m_sync_dashboard" value="Dashboard" icon="pi pi-dashboard" outcome="/pages/user-manager/sync/dashboard" />
|
||||
</p:submenu>
|
||||
-->
|
||||
|
||||
<!-- Administration (visible uniquement pour les admins) -->
|
||||
<p:submenu id="m_admin" label="Administration" icon="pi pi-cog" rendered="#{userSessionBean.hasRole('admin')}">
|
||||
|
||||
@@ -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">
|
||||
|
||||
<!--
|
||||
@@ -44,6 +45,7 @@
|
||||
</ui:include>
|
||||
-->
|
||||
|
||||
<!-- Valeurs par défaut simplifiées (fr:commandButton gère severity/size nativement) -->
|
||||
<c:set var="severity" value="#{empty severity ? 'primary' : severity}" />
|
||||
<c:set var="size" value="#{empty size ? 'normal' : size}" />
|
||||
<c:set var="disabled" value="#{empty disabled ? false : disabled}" />
|
||||
@@ -51,47 +53,14 @@
|
||||
<c:set var="hasAction" value="#{empty hasAction ? false : hasAction}" />
|
||||
<c:set var="hasOutcome" value="#{empty hasOutcome ? false : hasOutcome}" />
|
||||
|
||||
<!-- Déterminer la classe selon la severity -->
|
||||
<c:choose>
|
||||
<c:when test="#{severity == 'primary'}">
|
||||
<c:set var="buttonClass" value="p-button-primary" />
|
||||
</c:when>
|
||||
<c:when test="#{severity == 'success'}">
|
||||
<c:set var="buttonClass" value="p-button-success" />
|
||||
</c:when>
|
||||
<c:when test="#{severity == 'warning'}">
|
||||
<c:set var="buttonClass" value="p-button-warning" />
|
||||
</c:when>
|
||||
<c:when test="#{severity == 'danger'}">
|
||||
<c:set var="buttonClass" value="p-button-danger" />
|
||||
</c:when>
|
||||
<c:when test="#{severity == 'info'}">
|
||||
<c:set var="buttonClass" value="p-button-info" />
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<c:set var="buttonClass" value="p-button-secondary" />
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
|
||||
<!-- Ajouter la taille -->
|
||||
<c:if test="#{size == 'small'}">
|
||||
<c:set var="buttonClass" value="#{buttonClass} p-button-sm" />
|
||||
</c:if>
|
||||
<c:if test="#{size == 'large'}">
|
||||
<c:set var="buttonClass" value="#{buttonClass} p-button-lg" />
|
||||
</c:if>
|
||||
|
||||
<!-- Ajouter les classes personnalisées -->
|
||||
<c:if test="#{not empty styleClass}">
|
||||
<c:set var="buttonClass" value="#{buttonClass} #{styleClass}" />
|
||||
</c:if>
|
||||
|
||||
<c:choose>
|
||||
<c:when test="#{hasAction}">
|
||||
<p:commandButton
|
||||
<fr:commandButton
|
||||
value="#{value}"
|
||||
icon="#{not empty icon ? icon : ''}"
|
||||
styleClass="#{buttonClass}"
|
||||
severity="#{severity}"
|
||||
size="#{size != 'normal' ? size : null}"
|
||||
styleClass="#{styleClass}"
|
||||
disabled="#{disabled}"
|
||||
action="#{action}"
|
||||
update="#{not empty update ? update : '@form'}"
|
||||
@@ -99,10 +68,12 @@
|
||||
onclick="#{not empty onclick ? onclick : ''}" />
|
||||
</c:when>
|
||||
<c:when test="#{hasOutcome}">
|
||||
<p:commandButton
|
||||
<fr:commandButton
|
||||
value="#{value}"
|
||||
icon="#{not empty icon ? icon : ''}"
|
||||
styleClass="#{buttonClass}"
|
||||
severity="#{severity}"
|
||||
size="#{size != 'normal' ? size : null}"
|
||||
styleClass="#{styleClass}"
|
||||
disabled="#{disabled}"
|
||||
outcome="#{outcome}"
|
||||
update="#{not empty update ? update : '@form'}"
|
||||
@@ -110,19 +81,23 @@
|
||||
onclick="#{not empty onclick ? onclick : ''}" />
|
||||
</c:when>
|
||||
<c:when test="#{not empty onclick}">
|
||||
<p:commandButton
|
||||
<fr:commandButton
|
||||
value="#{value}"
|
||||
icon="#{not empty icon ? icon : ''}"
|
||||
styleClass="#{buttonClass}"
|
||||
severity="#{severity}"
|
||||
size="#{size != 'normal' ? size : null}"
|
||||
styleClass="#{styleClass}"
|
||||
disabled="#{disabled}"
|
||||
type="button"
|
||||
onclick="#{onclick}" />
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<p:commandButton
|
||||
<fr:commandButton
|
||||
value="#{value}"
|
||||
icon="#{not empty icon ? icon : ''}"
|
||||
styleClass="#{buttonClass}"
|
||||
severity="#{severity}"
|
||||
size="#{size != 'normal' ? size : null}"
|
||||
styleClass="#{styleClass}"
|
||||
disabled="true"
|
||||
title="Aucune action définie" />
|
||||
</c:otherwise>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ImportErrorDTO> 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
* <p><b>Architecture Hybride:</b></p>
|
||||
* <ul>
|
||||
* <li><b>Cache en mémoire</b> - Pour les logs récents (performances)</li>
|
||||
* <li><b>Persistance PostgreSQL</b> - Pour l'historique long terme (activable via config)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Configuration:</b></p>
|
||||
* <ul>
|
||||
* <li>{@code lions.audit.enabled} - Active/désactive l'audit (défaut: true)</li>
|
||||
* <li>{@code lions.audit.log-to-database} - Active la persistance DB (défaut: false en dev, true en prod)</li>
|
||||
* <li>{@code lions.audit.cache-size} - Taille max du cache mémoire (défaut: 10000)</li>
|
||||
* <li>{@code lions.audit.retention-days} - Durée de rétention en jours (défaut: 365)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Modes de Fonctionnement:</b></p>
|
||||
* <pre>
|
||||
* Mode DEV (logToDatabase=false):
|
||||
* - Stockage en mémoire uniquement
|
||||
* - Logs perdus au redémarrage
|
||||
* - Performances maximales
|
||||
*
|
||||
* Mode PROD (logToDatabase=true):
|
||||
* - Persistance PostgreSQL
|
||||
* - Cache mémoire pour requêtes fréquentes
|
||||
* - Historique complet préservé
|
||||
* </pre>
|
||||
*
|
||||
* @author Lions Development Team
|
||||
* @version 2.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class AuditServiceImpl implements AuditService {
|
||||
|
||||
// Stockage en mémoire (à remplacer par une DB en production)
|
||||
private final Map<String, AuditLogDTO> 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.
|
||||
* <p>Limité à {@code cacheSize} entrées. Les plus anciens sont supprimés automatiquement.</p>
|
||||
*/
|
||||
private final Map<String, AuditLogDTO> auditLogsCache = new ConcurrentHashMap<>();
|
||||
|
||||
// ==================== MÉTHODES PRINCIPALES ====================
|
||||
|
||||
@Override
|
||||
@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<AuditLogDTO> 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<AuditLogEntity> 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<AuditLogDTO> 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<TypeActionAudit, Long> 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<String, Long> result = new java.util.HashMap<>();
|
||||
result.put("success", successCount);
|
||||
result.put("failure", failureCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
Map<String, Object> stats = new java.util.HashMap<>();
|
||||
|
||||
long total = logToDatabase ?
|
||||
AuditLogEntity.findByPeriod(dateDebut, dateFin).size() :
|
||||
auditLogsCache.values().stream()
|
||||
.filter(log -> {
|
||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||
return false;
|
||||
}
|
||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.count();
|
||||
|
||||
stats.put("total", total);
|
||||
stats.put("success", getSuccessCount(dateDebut, dateFin));
|
||||
stats.put("failure", getFailureCount(dateDebut, dateFin));
|
||||
stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin));
|
||||
stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin));
|
||||
return stats;
|
||||
}
|
||||
|
||||
// ==================== EXPORT / PURGE ====================
|
||||
|
||||
@Override
|
||||
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<String, Object> getAuditStatistics(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
Map<String, Object> stats = new java.util.HashMap<>();
|
||||
stats.put("total", auditLogs.values().stream()
|
||||
.filter(log -> {
|
||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||
return false;
|
||||
}
|
||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.count());
|
||||
stats.put("success", getSuccessCount(dateDebut, dateFin));
|
||||
stats.put("failure", getFailureCount(dateDebut, dateFin));
|
||||
stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin));
|
||||
stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin));
|
||||
return stats;
|
||||
}
|
||||
// ==================== MÉTHODES PRIVÉES ====================
|
||||
|
||||
// Méthode privée helper pour la recherche
|
||||
private List<AuditLogDTO> 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<AuditLogDTO> 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<TypeActionAudit, Long> 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<AuditLogDTO> searchLogsFromDatabase(String acteurUsername, LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin, TypeActionAudit typeAction,
|
||||
String ressourceType, Boolean succes,
|
||||
int page, int pageSize) {
|
||||
log.debug("Recherche logs depuis base de données");
|
||||
|
||||
return auditLogs.values().stream()
|
||||
List<AuditLogEntity> entities;
|
||||
|
||||
// Optimisation: utiliser les requêtes spécialisées si possible
|
||||
if (acteurUsername != null && typeAction == null && ressourceType == null) {
|
||||
entities = AuditLogEntity.findByAuteur(acteurUsername);
|
||||
} else if (typeAction != null && acteurUsername == null && ressourceType == null) {
|
||||
entities = AuditLogEntity.findByAction(typeAction);
|
||||
} else if (dateDebut != null && dateFin != null) {
|
||||
entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
|
||||
} else {
|
||||
entities = AuditLogEntity.listAll();
|
||||
}
|
||||
|
||||
return entities.stream()
|
||||
.map(auditLogMapper::toDTO)
|
||||
.filter(log -> applyFilters(log, acteurUsername, dateDebut, dateFin, typeAction, ressourceType, succes))
|
||||
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
|
||||
.skip((long) page * pageSize)
|
||||
.limit(pageSize)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique les filtres de recherche à un log.
|
||||
*/
|
||||
private boolean applyFilters(AuditLogDTO log, String acteurUsername, LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin, TypeActionAudit typeAction,
|
||||
String ressourceType, Boolean succes) {
|
||||
if (acteurUsername != null && !"*".equals(acteurUsername) &&
|
||||
!acteurUsername.equals(log.getActeurUsername())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeAction != null && !typeAction.equals(log.getTypeAction())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (succes != null && succes != log.isSuccessful()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie les entrées les plus anciennes du cache.
|
||||
*/
|
||||
private void cleanOldestCacheEntries() {
|
||||
int toRemove = auditLogsCache.size() - (cacheSize * 90 / 100); // Garder 90%
|
||||
|
||||
if (toRemove > 0) {
|
||||
List<String> oldestKeys = auditLogsCache.entrySet().stream()
|
||||
.sorted((a, b) -> a.getValue().getDateAction().compareTo(b.getValue().getDateAction()))
|
||||
.limit(toRemove)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
oldestKeys.forEach(auditLogsCache::remove);
|
||||
log.debug("Nettoyé {} entrées du cache d'audit", oldestKeys.size());
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes helpers (statistiques)
|
||||
private Map<TypeActionAudit, Long> getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
if (logToDatabase) {
|
||||
List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
|
||||
return entities.stream()
|
||||
.collect(Collectors.groupingBy(AuditLogEntity::getAction, Collectors.counting()));
|
||||
}
|
||||
|
||||
return auditLogsCache.values().stream()
|
||||
.filter(log -> {
|
||||
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<String, Long> getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin);
|
||||
if (logToDatabase) {
|
||||
List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
|
||||
return entities.stream()
|
||||
.collect(Collectors.groupingBy(AuditLogEntity::getAuteurAction, Collectors.counting()));
|
||||
}
|
||||
|
||||
return auditLogs.values().stream()
|
||||
return auditLogsCache.values().stream()
|
||||
.filter(log -> {
|
||||
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<String> csvLines = new ArrayList<>();
|
||||
|
||||
// En-tête CSV
|
||||
csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur");
|
||||
|
||||
// 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<AuditLogDTO> logs;
|
||||
if (logToDatabase) {
|
||||
List<AuditLogEntity> 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour la validation des données CSV lors de l'import d'utilisateurs
|
||||
*
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class CsvValidationHelper {
|
||||
|
||||
/**
|
||||
* Pattern pour valider le format d'email selon RFC 5322 (simplifié)
|
||||
*/
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Pattern pour valider le username (alphanumérique, tirets, underscores, points)
|
||||
*/
|
||||
private static final Pattern USERNAME_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9._-]{2,255}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Longueur minimale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MIN_LENGTH = 2;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un nom ou prénom
|
||||
*/
|
||||
private static final int NAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Valide le format d'un email
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return true si l'email est valide, false sinon
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return EMAIL_PATTERN.matcher(email.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un username
|
||||
*
|
||||
* @param username Username à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateUsername(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return "Username obligatoire";
|
||||
}
|
||||
|
||||
String trimmed = username.trim();
|
||||
|
||||
if (trimmed.length() < USERNAME_MIN_LENGTH) {
|
||||
return String.format("Username trop court (minimum %d caractères)", USERNAME_MIN_LENGTH);
|
||||
}
|
||||
|
||||
if (trimmed.length() > USERNAME_MAX_LENGTH) {
|
||||
return String.format("Username trop long (maximum %d caractères)", USERNAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
if (!USERNAME_PATTERN.matcher(trimmed).matches()) {
|
||||
return "Username invalide (autorisé: lettres, chiffres, .-_)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un email (peut être vide)
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return Message d'erreur si invalide, null si valide ou vide
|
||||
*/
|
||||
public static String validateEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return null; // Email optionnel
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
return "Format d'email invalide";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un nom ou prénom
|
||||
*
|
||||
* @param name Nom à valider
|
||||
* @param fieldName Nom du champ pour les messages d'erreur
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateName(String name, String fieldName) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return null; // Nom optionnel
|
||||
}
|
||||
|
||||
String trimmed = name.trim();
|
||||
|
||||
if (trimmed.length() > NAME_MAX_LENGTH) {
|
||||
return String.format("%s trop long (maximum %d caractères)", fieldName, NAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide une valeur boolean
|
||||
*
|
||||
* @param value Valeur à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null; // Optionnel, défaut à false
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
if (!trimmed.equals("true") && !trimmed.equals("false") &&
|
||||
!trimmed.equals("1") && !trimmed.equals("0") &&
|
||||
!trimmed.equals("yes") && !trimmed.equals("no")) {
|
||||
return "Valeur boolean invalide (attendu: true/false, 1/0, yes/no)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une chaîne en boolean
|
||||
*
|
||||
* @param value Valeur à convertir
|
||||
* @return boolean correspondant
|
||||
*/
|
||||
public static boolean parseBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
return trimmed.equals("true") || trimmed.equals("1") || trimmed.equals("yes");
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie une chaîne (trim et null si vide)
|
||||
*
|
||||
* @param value Valeur à nettoyer
|
||||
* @return Valeur nettoyée ou null
|
||||
*/
|
||||
public static String clean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -608,11 +608,17 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
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<UserDTO> existingUser = getUserByUsername(username, realmName);
|
||||
if (existingUser.isPresent()) {
|
||||
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
|
||||
.lineNumber(lineNumber)
|
||||
.lineContent(line)
|
||||
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.DUPLICATE_USER)
|
||||
.field("username")
|
||||
.message("Utilisateur déjà existant: " + username)
|
||||
.build());
|
||||
continue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// User doesn't exist, continue with creation
|
||||
}
|
||||
|
||||
// Create user
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.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) {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
-- =============================================================================
|
||||
-- Migration Flyway V1.0.0 - Création de la table audit_logs
|
||||
-- =============================================================================
|
||||
-- Description: Création de la table pour la persistance des logs d'audit
|
||||
-- des actions effectuées sur le système de gestion des utilisateurs
|
||||
--
|
||||
-- Auteur: Lions Development Team
|
||||
-- Date: 2026-01-02
|
||||
-- Version: 1.0.0
|
||||
-- =============================================================================
|
||||
|
||||
-- Création de la table audit_logs
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
-- Clé primaire générée automatiquement
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- Informations sur l'utilisateur concerné
|
||||
user_id VARCHAR(255),
|
||||
|
||||
-- Type d'action effectuée
|
||||
action VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Détails de l'action
|
||||
details TEXT,
|
||||
|
||||
-- Informations sur l'auteur de l'action
|
||||
auteur_action VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Timestamp de l'action
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Informations de traçabilité réseau
|
||||
ip_address VARCHAR(45),
|
||||
user_agent VARCHAR(500),
|
||||
|
||||
-- Informations multi-tenant
|
||||
realm_name VARCHAR(255),
|
||||
|
||||
-- Statut de l'action
|
||||
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
error_message TEXT,
|
||||
|
||||
-- Métadonnées
|
||||
CONSTRAINT chk_audit_action CHECK (action IN (
|
||||
-- Actions utilisateurs
|
||||
'CREATION_UTILISATEUR',
|
||||
'MODIFICATION_UTILISATEUR',
|
||||
'SUPPRESSION_UTILISATEUR',
|
||||
'ACTIVATION_UTILISATEUR',
|
||||
'DESACTIVATION_UTILISATEUR',
|
||||
'VERROUILLAGE_UTILISATEUR',
|
||||
'DEVERROUILLAGE_UTILISATEUR',
|
||||
|
||||
-- Actions mot de passe
|
||||
'RESET_PASSWORD',
|
||||
'CHANGE_PASSWORD',
|
||||
'FORCE_PASSWORD_RESET',
|
||||
|
||||
-- Actions sessions
|
||||
'LOGOUT_UTILISATEUR',
|
||||
'LOGOUT_ALL_SESSIONS',
|
||||
'SESSION_EXPIREE',
|
||||
|
||||
-- Actions rôles
|
||||
'ATTRIBUTION_ROLE',
|
||||
'REVOCATION_ROLE',
|
||||
'CREATION_ROLE',
|
||||
'MODIFICATION_ROLE',
|
||||
'SUPPRESSION_ROLE',
|
||||
|
||||
-- Actions groupes
|
||||
'AJOUT_GROUPE',
|
||||
'RETRAIT_GROUPE',
|
||||
|
||||
-- Actions realms
|
||||
'ATTRIBUTION_REALM',
|
||||
'REVOCATION_REALM',
|
||||
|
||||
-- Actions synchronisation
|
||||
'SYNC_MANUEL',
|
||||
'SYNC_AUTO',
|
||||
'SYNC_ERREUR',
|
||||
|
||||
-- Actions import/export
|
||||
'EXPORT_CSV',
|
||||
'IMPORT_CSV',
|
||||
|
||||
-- Actions système
|
||||
'CONNEXION_REUSSIE',
|
||||
'CONNEXION_ECHOUEE',
|
||||
'TENTATIVE_ACCES_NON_AUTORISE',
|
||||
'ERREUR_SYSTEME',
|
||||
'CONFIGURATION_MODIFIEE'
|
||||
))
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- INDEX pour optimiser les requêtes
|
||||
-- =============================================================================
|
||||
|
||||
-- Index sur user_id pour recherches rapides par utilisateur
|
||||
CREATE INDEX idx_audit_user_id ON audit_logs(user_id)
|
||||
WHERE user_id IS NOT NULL;
|
||||
|
||||
-- Index sur action pour filtrer par type d'action
|
||||
CREATE INDEX idx_audit_action ON audit_logs(action);
|
||||
|
||||
-- Index sur timestamp pour recherches chronologiques et tri
|
||||
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp DESC);
|
||||
|
||||
-- Index sur auteur_action pour tracer les actions d'un administrateur
|
||||
CREATE INDEX idx_audit_auteur ON audit_logs(auteur_action);
|
||||
|
||||
-- Index sur realm_name pour isolation multi-tenant
|
||||
CREATE INDEX idx_audit_realm ON audit_logs(realm_name)
|
||||
WHERE realm_name IS NOT NULL;
|
||||
|
||||
-- Index composite pour recherches fréquentes
|
||||
CREATE INDEX idx_audit_user_timestamp ON audit_logs(user_id, timestamp DESC)
|
||||
WHERE user_id IS NOT NULL;
|
||||
|
||||
-- Index sur success pour identifier rapidement les échecs
|
||||
CREATE INDEX idx_audit_failures ON audit_logs(success, timestamp DESC)
|
||||
WHERE success = FALSE;
|
||||
|
||||
-- =============================================================================
|
||||
-- COMMENTAIRES sur les colonnes
|
||||
-- =============================================================================
|
||||
|
||||
COMMENT ON TABLE audit_logs IS 'Table de persistance des logs d''audit pour traçabilité complète';
|
||||
|
||||
COMMENT ON COLUMN audit_logs.id IS 'Identifiant unique auto-incrémenté du log';
|
||||
COMMENT ON COLUMN audit_logs.user_id IS 'ID de l''utilisateur concerné par l''action (null pour actions système)';
|
||||
COMMENT ON COLUMN audit_logs.action IS 'Type d''action effectuée (enum TypeActionAudit)';
|
||||
COMMENT ON COLUMN audit_logs.details IS 'Détails complémentaires sur l''action';
|
||||
COMMENT ON COLUMN audit_logs.auteur_action IS 'Identifiant de l''utilisateur ayant effectué l''action';
|
||||
COMMENT ON COLUMN audit_logs.timestamp IS 'Date et heure précise de l''action';
|
||||
COMMENT ON COLUMN audit_logs.ip_address IS 'Adresse IP du client ayant effectué l''action';
|
||||
COMMENT ON COLUMN audit_logs.user_agent IS 'User-Agent du navigateur/client';
|
||||
COMMENT ON COLUMN audit_logs.realm_name IS 'Nom du realm Keycloak concerné (multi-tenant)';
|
||||
COMMENT ON COLUMN audit_logs.success IS 'Indique si l''action a réussi (true) ou échoué (false)';
|
||||
COMMENT ON COLUMN audit_logs.error_message IS 'Message d''erreur en cas d''échec (null si success=true)';
|
||||
|
||||
-- =============================================================================
|
||||
-- POLITIQUE DE RÉTENTION (optionnel - à activer selon besoins)
|
||||
-- =============================================================================
|
||||
|
||||
-- Fonction pour nettoyer automatiquement les vieux logs
|
||||
-- Décommenter et adapter la période de rétention selon les besoins
|
||||
|
||||
/*
|
||||
CREATE OR REPLACE FUNCTION cleanup_old_audit_logs() RETURNS void AS $$
|
||||
BEGIN
|
||||
-- Supprime les logs de plus de 365 jours (configurable)
|
||||
DELETE FROM audit_logs
|
||||
WHERE timestamp < CURRENT_TIMESTAMP - INTERVAL '365 days';
|
||||
|
||||
RAISE NOTICE 'Logs d''audit plus anciens que 365 jours supprimés';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Créer un job CRON (nécessite extension pg_cron)
|
||||
-- SELECT cron.schedule('cleanup-audit-logs', '0 2 * * 0', 'SELECT cleanup_old_audit_logs()');
|
||||
*/
|
||||
|
||||
-- =============================================================================
|
||||
-- GRANTS (à adapter selon les rôles de votre base de données)
|
||||
-- =============================================================================
|
||||
|
||||
-- GRANT SELECT, INSERT ON audit_logs TO lions_app_user;
|
||||
-- GRANT USAGE, SELECT ON SEQUENCE audit_logs_id_seq TO lions_app_user;
|
||||
|
||||
-- =============================================================================
|
||||
-- FIN DE LA MIGRATION
|
||||
-- =============================================================================
|
||||
@@ -220,12 +220,15 @@ class UserServiceImplTest {
|
||||
lenient().doNothing().when(userResource)
|
||||
.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")));
|
||||
}
|
||||
}
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -22,7 +22,7 @@
|
||||
<quarkus.version>3.15.1</quarkus.version>
|
||||
<quarkus-primefaces.version>3.15.1</quarkus-primefaces.version>
|
||||
<primefaces.version>14.0.5</primefaces.version>
|
||||
<primefaces-freya-extension.version>1.0.0-SNAPSHOT</primefaces-freya-extension.version>
|
||||
<primefaces-freya-extension.version>1.0.0</primefaces-freya-extension.version>
|
||||
<keycloak.version>26.0.4</keycloak.version>
|
||||
<lombok.version>1.18.30</lombok.version>
|
||||
<mapstruct.version>1.5.5.Final</mapstruct.version>
|
||||
|
||||
Reference in New Issue
Block a user