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:
lionsdev
2026-01-03 13:53:35 +00:00
parent d798db1e4d
commit dc426b754e
32 changed files with 2805 additions and 1760 deletions

55
.gitignore vendored
View File

@@ -141,3 +141,58 @@ token.txt
*-secrets.ps1 *-secrets.ps1
*-password.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/

View File

@@ -150,5 +150,25 @@ public interface UserServiceClient {
@PathParam("userId") String userId, @PathParam("userId") String userId,
@QueryParam("realm") String realmName @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
);
} }

View File

@@ -6,6 +6,7 @@ import dev.lions.user.manager.dto.audit.AuditLogDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit; import dev.lions.user.manager.enums.audit.TypeActionAudit;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage; import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@@ -170,10 +171,29 @@ public class AuditConsultationBean implements Serializable {
String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null; String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null;
String csv = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr); 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) { } 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()); addErrorMessage("Erreur lors de l'export: " + e.getMessage());
} }
} }

View File

@@ -1,6 +1,7 @@
package dev.lions.user.manager.client.view; package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.service.AuditServiceClient; 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.RoleServiceClient;
import dev.lions.user.manager.client.service.UserServiceClient; import dev.lions.user.manager.client.service.UserServiceClient;
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
@@ -18,6 +19,8 @@ import jakarta.faces.context.FacesContext;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -47,104 +50,278 @@ public class DashboardBean implements Serializable {
@RestClient @RestClient
private AuditServiceClient auditServiceClient; private AuditServiceClient auditServiceClient;
// Statistiques @Inject
@RestClient
private RealmServiceClient realmServiceClient;
@Inject
private UserSessionBean userSessionBean;
// ==================== STATISTIQUES MÉTIER ====================
// Utilisateurs
private Long totalUsers = 0L; 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 totalRoles = 0L;
private Long recentActions = 0L;
private Long activeSessions = 0L; // Audit & Activité
private Long onlineUsers = 0L; 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 // Indicateur de chargement
private boolean loading = false; private boolean loading = false;
// Méthodes pour obtenir les valeurs formatées pour l'affichage // ==================== MÉTHODES D'AFFICHAGE ====================
public String getTotalUsersDisplay() { public String getTotalUsersDisplay() {
if (loading) return "..."; return loading ? "..." : String.valueOf(totalUsers);
return totalUsers != null ? String.valueOf(totalUsers) : "0"; }
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() { public String getTotalRolesDisplay() {
if (loading) return "..."; return loading ? "..." : String.valueOf(totalRoles);
return totalRoles != null ? String.valueOf(totalRoles) : "0";
} }
public String getRecentActionsDisplay() { public String getActionsLast24hDisplay() {
if (loading) return "..."; return loading ? "..." : String.valueOf(actionsLast24h);
return recentActions != null ? String.valueOf(recentActions) : "0"; }
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() { public boolean isLoading() {
return loading; return loading;
} }
// Realm par défaut public boolean hasAlerts() {
private String realmName = "master"; 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"); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
@PostConstruct @PostConstruct
public void init() { public void init() {
LOGGER.info("=== Initialisation du DashboardBean ==="); LOGGER.info("=== Initialisation du DashboardBean ===");
LOGGER.info("Realm par défaut: " + realmName);
LOGGER.info("UserServiceClient injecté: " + (userServiceClient != null ? "OUI" : "NON")); LOGGER.info("UserServiceClient injecté: " + (userServiceClient != null ? "OUI" : "NON"));
LOGGER.info("RoleServiceClient injecté: " + (roleServiceClient != null ? "OUI" : "NON")); LOGGER.info("RoleServiceClient injecté: " + (roleServiceClient != null ? "OUI" : "NON"));
LOGGER.info("AuditServiceClient injecté: " + (auditServiceClient != 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(); loadStatistics();
} }
/** /**
* Charger toutes les statistiques * Charger toutes les statistiques métier
*/ */
public void loadStatistics() { public void loadStatistics() {
loading = true; loading = true;
try { try {
loadTotalUsers(); // Statistiques utilisateurs
loadUserStatistics();
// Statistiques rôles
loadTotalRoles(); loadTotalRoles();
loadRecentActions();
// Les sessions actives nécessitent une API spécifique qui n'existe pas encore // Statistiques activité & audit
// activeSessions = 0L; loadActivityStatistics();
// onlineUsers = 0L;
// Statistiques sécurité
loadSecurityStatistics();
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage()); LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage());
e.printStackTrace();
} finally { } finally {
loading = false; loading = false;
} }
} }
/** /**
* Charger le nombre total d'utilisateurs * Charger les statistiques utilisateurs
*/ */
private void loadTotalUsers() { private void loadUserStatistics() {
try { try {
LOGGER.info("Début chargement total utilisateurs pour realm: " + realmName); // Total utilisateurs
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() UserSearchCriteriaDTO criteriaAll = UserSearchCriteriaDTO.builder()
.realmName(realmName) .realmName(realmName)
.page(0) .page(0)
.pageSize(1) // On n'a besoin que du count .pageSize(1)
.build(); .build();
UserSearchResultDTO resultAll = userServiceClient.searchUsers(criteriaAll);
totalUsers = resultAll != null && resultAll.getTotalCount() != null ? resultAll.getTotalCount() : 0L;
LOGGER.info("Appel userServiceClient.searchUsers()..."); // Utilisateurs actifs (enabled=true)
UserSearchResultDTO result = userServiceClient.searchUsers(criteria); UserSearchCriteriaDTO criteriaActive = UserSearchCriteriaDTO.builder()
LOGGER.info("Résultat reçu: " + (result != null ? "NON NULL" : "NULL")); .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);
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;
}
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("❌ ERREUR lors du chargement du nombre d'utilisateurs: " + e.getMessage()); LOGGER.severe("❌ Erreur chargement statistiques utilisateurs: " + e.getMessage());
LOGGER.severe(" Type d'erreur: " + e.getClass().getName());
e.printStackTrace();
totalUsers = 0L; 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;
} }
} }
@@ -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 * Rafraîchir les statistiques
*/ */
@@ -238,6 +360,52 @@ public class DashboardBean implements Serializable {
addSuccessMessage("Statistiques rafraîchies avec succès"); 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 // Méthodes utilitaires pour les messages
private void addSuccessMessage(String message) { private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,

View File

@@ -28,6 +28,7 @@ import java.util.Map;
@Named @Named
@ViewScoped @ViewScoped
@Slf4j @Slf4j
@SuppressWarnings("deprecation") // ChartData API dépréciée - migration vers JSON prévue
public class DashboardView implements Serializable { public class DashboardView implements Serializable {
@Inject @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() { public void createBarModel() {
barModel = new BarChartModel(); barModel = new BarChartModel();
ChartData data = new ChartData(); ChartData data = new ChartData();

View File

@@ -120,6 +120,32 @@ public class UserCreationBean implements Serializable {
return "userListPage"; 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 * Charger les realms disponibles depuis Keycloak
*/ */

View File

@@ -8,6 +8,7 @@ import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import dev.lions.user.manager.enums.user.StatutUser; import dev.lions.user.manager.enums.user.StatutUser;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage; import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.faces.event.ActionEvent; import jakarta.faces.event.ActionEvent;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
@@ -18,6 +19,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.primefaces.event.data.PageEvent; import org.primefaces.event.data.PageEvent;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -75,6 +77,9 @@ public class UserListBean implements Serializable {
private List<StatutUser> statutOptions = List.of(StatutUser.values()); private List<StatutUser> statutOptions = List.of(StatutUser.values());
private List<String> availableRealms = new ArrayList<>(); private List<String> availableRealms = new ArrayList<>();
// Résultats de l'import CSV
private dev.lions.user.manager.dto.importexport.ImportResultDTO lastImportResult;
@PostConstruct @PostConstruct
public void init() { public void init() {
LOGGER.info("Initialisation de UserListBean"); 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() { 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() { 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, FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); 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));
}
} }

View File

@@ -190,6 +190,23 @@
<redirect /> <redirect />
</navigation-case> </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> </navigation-rule>
</faces-config> </faces-config>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml"> template="/templates/main-template.xhtml">
<ui:define name="title">Affectation des Realms - Lions User Manager</ui:define> <ui:define name="title">Affectation des Realms - Lions User Manager</ui:define>
@@ -24,9 +25,9 @@
<p class="text-600 m-0">Gérer les permissions d'administration par realm (contrôle multi-tenant)</p> <p class="text-600 m-0">Gérer les permissions d'administration par realm (contrôle multi-tenant)</p>
</div> </div>
</div> </div>
<p:commandButton value="Nouvelle Affectation" <fr:commandButton value="Nouvelle Affectation"
icon="pi pi-plus" icon="pi pi-plus"
styleClass="p-button-success" severity="success"
onclick="PF('assignRealmDialog').show();" onclick="PF('assignRealmDialog').show();"
type="button" /> type="button" />
</div> </div>
@@ -91,30 +92,32 @@
<div class="card"> <div class="card">
<div class="flex align-items-center justify-content-between mb-4"> <div class="flex align-items-center justify-content-between mb-4">
<h5 class="m-0">Affectations Actuelles</h5> <h5 class="m-0">Affectations Actuelles</h5>
<p:commandButton value="Rafraîchir" <fr:commandButton value="Rafraîchir"
icon="pi pi-refresh" icon="pi pi-refresh"
styleClass="p-button-outlined p-button-sm" outlined="true"
size="small"
action="#{realmAssignmentBean.loadAssignments}" action="#{realmAssignmentBean.loadAssignments}"
update=":formRealmAssignments" /> update=":formRealmAssignments" />
</div> </div>
<p:messages id="messages" showDetail="true" closable="true"> <fr:message id="messages" showDetail="true" closable="true">
<p:autoUpdate /> <p:autoUpdate />
</p:messages> </fr:message>
<p:dataTable id="assignmentsTable" <p:dataTable id="assignmentsTable"
value="#{realmAssignmentBean.assignments}" value="#{realmAssignmentBean.assignments}"
var="assignment" var="assignment"
paginator="true" paginator="true"
rows="10" rows="25"
paginatorPosition="bottom" paginatorPosition="bottom"
paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}" paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
rowsPerPageTemplate="10,20,50" rowsPerPageTemplate="10,25,50,100"
emptyMessage="Aucune affectation configurée" emptyMessage="Aucune affectation configurée"
responsiveLayout="scroll"
styleClass="p-datatable-sm"> styleClass="p-datatable-sm">
<!-- Colonne Utilisateur --> <!-- 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 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;"> <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'}" /> <h:outputText value="#{assignment.username != null and assignment.username.length() >= 2 ? assignment.username.substring(0,2).toUpperCase() : 'U'}" />
@@ -127,58 +130,61 @@
</p:column> </p:column>
<!-- Colonne Realm --> <!-- Colonne Realm -->
<p:column headerText="Realm" sortBy="#{assignment.realmName}" filterBy="#{assignment.realmName}" filterMatchMode="contains"> <p:column headerText="Realm" sortBy="#{assignment.realmName}" filterBy="#{assignment.realmName}" filterMatchMode="contains" priority="2">
<p:tag value="#{assignment.realmName}" <fr:tag value="#{assignment.realmName}"
severity="info" severity="info"
icon="pi pi-globe" /> icon="pi pi-globe" />
</p:column> </p:column>
<!-- Colonne Type --> <!-- Colonne Type -->
<p:column headerText="Type" style="width: 150px"> <p:column headerText="Type" style="width: 150px" priority="3">
<p:tag value="Super Admin" <fr:tag value="Super Admin"
severity="danger" severity="danger"
icon="pi pi-star" icon="pi pi-star"
rendered="#{assignment.isSuperAdmin()}" /> rendered="#{assignment.isSuperAdmin()}" />
<p:tag value="Realm Admin" <fr:tag value="Realm Admin"
severity="success" severity="success"
icon="pi pi-shield" icon="pi pi-shield"
rendered="#{!assignment.isSuperAdmin()}" /> rendered="#{!assignment.isSuperAdmin()}" />
</p:column> </p:column>
<!-- Colonne Statut --> <!-- Colonne Statut -->
<p:column headerText="Statut" style="width: 120px"> <p:column headerText="Statut" style="width: 120px" priority="4">
<p:tag value="Actif" <fr:tag value="Actif"
severity="success" severity="success"
icon="pi pi-check-circle" icon="pi pi-check-circle"
rendered="#{assignment.active and !assignment.isExpired()}" /> rendered="#{assignment.active and !assignment.isExpired()}" />
<p:tag value="Inactif" <fr:tag value="Inactif"
severity="warning" severity="warning"
icon="pi pi-times-circle" icon="pi pi-times-circle"
rendered="#{!assignment.active}" /> rendered="#{!assignment.active}" />
<p:tag value="Expiré" <fr:tag value="Expiré"
severity="danger" severity="danger"
icon="pi pi-exclamation-circle" icon="pi pi-exclamation-circle"
rendered="#{assignment.isExpired()}" /> rendered="#{assignment.isExpired()}" />
</p:column> </p:column>
<!-- Colonne Assigné le --> <!-- 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}"> <h:outputText value="#{assignment.assignedAt}">
<f:convertDateTime pattern="dd/MM/yyyy HH:mm" /> <f:convertDateTime pattern="dd/MM/yyyy HH:mm" />
</h:outputText> </h:outputText>
</p:column> </p:column>
<!-- Colonne Par --> <!-- 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}" /> <h:outputText value="#{assignment.assignedBy}" />
</p:column> </p:column>
<!-- Colonne Actions --> <!-- 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"> <div class="flex gap-1 justify-content-center flex-wrap">
<!-- Bouton Désactiver --> <!-- Bouton Désactiver -->
<p:commandButton icon="pi pi-ban" <fr:commandButton icon="pi pi-ban"
styleClass="p-button-rounded p-button-text p-button-sm p-button-warning" rounded="true"
text="true"
size="small"
severity="warning"
title="Désactiver" title="Désactiver"
action="#{realmAssignmentBean.deactivateAssignment(assignment)}" action="#{realmAssignmentBean.deactivateAssignment(assignment)}"
update=":formRealmAssignments" update=":formRealmAssignments"
@@ -187,11 +193,14 @@
<p:confirm header="Confirmation" <p:confirm header="Confirmation"
message="Désactiver cette affectation ?" message="Désactiver cette affectation ?"
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </fr:commandButton>
<!-- Bouton Activer --> <!-- Bouton Activer -->
<p:commandButton icon="pi pi-check" <fr:commandButton icon="pi pi-check"
styleClass="p-button-rounded p-button-text p-button-sm p-button-success" rounded="true"
text="true"
size="small"
severity="success"
title="Activer" title="Activer"
action="#{realmAssignmentBean.activateAssignment(assignment)}" action="#{realmAssignmentBean.activateAssignment(assignment)}"
update=":formRealmAssignments" update=":formRealmAssignments"
@@ -200,11 +209,14 @@
<p:confirm header="Confirmation" <p:confirm header="Confirmation"
message="Activer cette affectation ?" message="Activer cette affectation ?"
icon="pi pi-question-circle" /> icon="pi pi-question-circle" />
</p:commandButton> </fr:commandButton>
<!-- Bouton Supprimer --> <!-- Bouton Supprimer -->
<p:commandButton icon="pi pi-trash" <fr:commandButton icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger" rounded="true"
text="true"
size="small"
severity="danger"
title="Supprimer" title="Supprimer"
action="#{realmAssignmentBean.revokeAssignment(assignment)}" action="#{realmAssignmentBean.revokeAssignment(assignment)}"
update=":formRealmAssignments" update=":formRealmAssignments"
@@ -212,7 +224,7 @@
<p:confirm header="Confirmation" <p:confirm header="Confirmation"
message="Révoquer l'accès de #{assignment.username} au realm #{assignment.realmName} ?" message="Révoquer l'accès de #{assignment.username} au realm #{assignment.realmName} ?"
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </fr:commandButton>
</div> </div>
</p:column> </p:column>
</p:dataTable> </p:dataTable>
@@ -234,72 +246,59 @@
<h:form id="formAssignRealm"> <h:form id="formAssignRealm">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<label class="block text-900 font-semibold mb-2"> <fr:fieldSelect id="userId"
<i class="pi pi-user text-primary mr-1"></i> label="Utilisateur *"
Utilisateur * value="#{realmAssignmentBean.selectedUserId}"
</label>
<p:selectOneMenu value="#{realmAssignmentBean.selectedUserId}"
styleClass="w-full"
filter="true" filter="true"
filterMatchMode="contains"> filterMatchMode="contains"
iconLeft="pi pi-user">
<f:selectItem itemLabel="Sélectionner un utilisateur" itemValue="" noSelectionOption="true" /> <f:selectItem itemLabel="Sélectionner un utilisateur" itemValue="" noSelectionOption="true" />
<f:selectItems value="#{realmAssignmentBean.availableUsers}" <f:selectItems value="#{realmAssignmentBean.availableUsers}"
var="user" var="user"
itemValue="#{user.id}" itemValue="#{user.id}"
itemLabel="#{user.username} (#{user.email})" /> itemLabel="#{user.username} (#{user.email})" />
</p:selectOneMenu> </fr:fieldSelect>
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="block text-900 font-semibold mb-2"> <fr:fieldSelect id="realmName"
<i class="pi pi-globe text-primary mr-1"></i> label="Realm *"
Realm * value="#{realmAssignmentBean.selectedRealmName}"
</label> iconLeft="pi pi-globe">
<p:selectOneMenu value="#{realmAssignmentBean.selectedRealmName}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner un realm" itemValue="" noSelectionOption="true" /> <f:selectItem itemLabel="Sélectionner un realm" itemValue="" noSelectionOption="true" />
<f:selectItems value="#{realmAssignmentBean.availableRealms}" /> <f:selectItems value="#{realmAssignmentBean.availableRealms}" />
</p:selectOneMenu> </fr:fieldSelect>
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="block text-900 font-semibold mb-2"> <fr:fieldInput id="raison"
<i class="pi pi-comment text-primary mr-1"></i> label="Raison"
Raison value="#{realmAssignmentBean.newAssignment.raison}"
</label> placeholder="Ex: Nouveau gestionnaire du realm client"
<p:inputText value="#{realmAssignmentBean.newAssignment.raison}" iconLeft="pi pi-comment" />
styleClass="w-full"
placeholder="Ex: Nouveau gestionnaire du realm client" />
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="block text-900 font-semibold mb-2"> <fr:fieldTextarea id="commentaires"
<i class="pi pi-file-edit text-primary mr-1"></i> label="Commentaires"
Commentaires value="#{realmAssignmentBean.newAssignment.commentaires}"
</label>
<p:inputTextarea value="#{realmAssignmentBean.newAssignment.commentaires}"
rows="3" rows="3"
styleClass="w-full" placeholder="Commentaires administratifs (optionnel)"
placeholder="Commentaires administratifs (optionnel)" /> iconLeft="pi pi-file-edit" />
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="flex align-items-center"> <fr:fieldCheckbox id="temporaire"
<p:selectBooleanCheckbox value="#{realmAssignmentBean.newAssignment.temporaire}" label="Affectation temporaire"
itemLabel="Affectation temporaire" value="#{realmAssignmentBean.newAssignment.temporaire}" />
styleClass="mr-2" />
</div>
</div> </div>
<div class="col-12" rendered="#{realmAssignmentBean.newAssignment.temporaire}"> <div class="col-12" rendered="#{realmAssignmentBean.newAssignment.temporaire}">
<label class="block text-900 font-semibold mb-2"> <fr:fieldCalendar id="dateExpiration"
<i class="pi pi-calendar text-primary mr-1"></i> label="Date d'expiration"
Date d'expiration value="#{realmAssignmentBean.newAssignment.dateExpiration}"
</label>
<p:calendar value="#{realmAssignmentBean.newAssignment.dateExpiration}"
pattern="dd/MM/yyyy HH:mm" pattern="dd/MM/yyyy HH:mm"
showTime="true" showTime="true" />
styleClass="w-full" />
</div> </div>
<div class="col-12"> <div class="col-12">
@@ -319,15 +318,17 @@
<div class="col-12"> <div class="col-12">
<div class="flex gap-2"> <div class="flex gap-2">
<p:commandButton value="Annuler" <fr:commandButton value="Annuler"
icon="pi pi-times" icon="pi pi-times"
styleClass="p-button-text flex-1" text="true"
styleClass="flex-1"
onclick="PF('assignRealmDialog').hide();" onclick="PF('assignRealmDialog').hide();"
type="button" type="button"
action="#{realmAssignmentBean.resetForm}" /> action="#{realmAssignmentBean.resetForm}" />
<p:commandButton value="Assigner" <fr:commandButton value="Assigner"
icon="pi pi-check" icon="pi pi-check"
styleClass="p-button-success flex-1" severity="success"
styleClass="flex-1"
action="#{realmAssignmentBean.assignRealm}" action="#{realmAssignmentBean.assignRealm}"
update=":formRealmAssignments :formAssignRealm" update=":formRealmAssignments :formAssignRealm"
oncomplete="if (args.validationFailed == false) PF('assignRealmDialog').hide();" /> oncomplete="if (args.validationFailed == false) PF('assignRealmDialog').hide();" />
@@ -338,11 +339,12 @@
</p:dialog> </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: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" /> <fr:commandButton value="Non" type="button" text="true" icon="pi pi-times" />
<p:commandButton value="Oui" type="button" styleClass="p-button-danger" icon="pi pi-check" /> <fr:commandButton value="Oui" type="button" severity="danger" icon="pi pi-check" />
</p:confirmDialog> </p:confirmDialog>
</ui:define> </ui:define>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml"> template="/templates/main-template.xhtml">
<ui:param name="page" value="#{auditConsultationBean}"/> <ui:param name="page" value="#{auditConsultationBean}"/>
@@ -25,10 +26,9 @@
</div> </div>
</div> </div>
<h:form id="formHeaderActions"> <h:form id="formHeaderActions">
<p:commandButton <fr:commandButton value="Exporter CSV"
value="Exporter CSV"
icon="pi pi-download" icon="pi pi-download"
styleClass="p-button-success" severity="success"
action="#{auditConsultationBean.exportToCSV}" action="#{auditConsultationBean.exportToCSV}"
ajax="false" /> ajax="false" />
</h:form> </h:form>
@@ -144,72 +144,68 @@
<h:form id="formFilters"> <h:form id="formFilters">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-6 lg:col-4"> <div class="col-12 md:col-6 lg:col-4">
<label for="acteurFilter" class="block text-900 font-medium mb-2">Acteur</label> <fr:fieldInput id="acteurFilter"
<p:inputText id="acteurFilter" label="Acteur"
value="#{auditConsultationBean.acteurUsername}" value="#{auditConsultationBean.acteurUsername}"
placeholder="Nom d'utilisateur..." placeholder="Nom d'utilisateur..."
styleClass="w-full" /> iconLeft="pi pi-user" />
</div> </div>
<div class="col-12 md:col-6 lg:col-4"> <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> <fr:fieldSelect id="typeActionFilter"
<p:selectOneMenu id="typeActionFilter" label="Type d'action"
value="#{auditConsultationBean.selectedTypeAction}" value="#{auditConsultationBean.selectedTypeAction}"
styleClass="w-full"> iconLeft="pi pi-bolt">
<f:selectItem itemLabel="Tous les types" itemValue="" /> <f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItems value="#{auditConsultationBean.typeActionOptions}" /> <f:selectItems value="#{auditConsultationBean.typeActionOptions}" />
</p:selectOneMenu> </fr:fieldSelect>
</div> </div>
<div class="col-12 md:col-6 lg:col-4"> <div class="col-12 md:col-6 lg:col-4">
<label for="succesFilter" class="block text-900 font-medium mb-2">Résultat</label> <fr:fieldSelect id="succesFilter"
<p:selectOneMenu id="succesFilter" label="Résultat"
value="#{auditConsultationBean.succes}" value="#{auditConsultationBean.succes}"
styleClass="w-full"> iconLeft="pi pi-check-circle">
<f:selectItem itemLabel="Tous" itemValue="" /> <f:selectItem itemLabel="Tous" itemValue="" />
<f:selectItem itemLabel="Succès" itemValue="true" /> <f:selectItem itemLabel="Succès" itemValue="true" />
<f:selectItem itemLabel="Échec" itemValue="false" /> <f:selectItem itemLabel="Échec" itemValue="false" />
</p:selectOneMenu> </fr:fieldSelect>
</div> </div>
<div class="col-12 md:col-6 lg:col-4"> <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> <fr:fieldCalendar id="dateDebutFilter"
<p:calendar id="dateDebutFilter" label="Date début"
value="#{auditConsultationBean.dateDebut}" value="#{auditConsultationBean.dateDebut}"
pattern="dd/MM/yyyy" pattern="dd/MM/yyyy"
showIcon="true" showIcon="true" />
styleClass="w-full" />
</div> </div>
<div class="col-12 md:col-6 lg:col-4"> <div class="col-12 md:col-6 lg:col-4">
<label for="dateFinFilter" class="block text-900 font-medium mb-2">Date fin</label> <fr:fieldCalendar id="dateFinFilter"
<p:calendar id="dateFinFilter" label="Date fin"
value="#{auditConsultationBean.dateFin}" value="#{auditConsultationBean.dateFin}"
pattern="dd/MM/yyyy" pattern="dd/MM/yyyy"
showIcon="true" showIcon="true" />
styleClass="w-full" />
</div> </div>
<div class="col-12 md:col-6 lg:col-4"> <div class="col-12 md:col-6 lg:col-4">
<label for="ressourceFilter" class="block text-900 font-medium mb-2">Type ressource</label> <fr:fieldInput id="ressourceFilter"
<p:inputText id="ressourceFilter" label="Type ressource"
value="#{auditConsultationBean.ressourceType}" value="#{auditConsultationBean.ressourceType}"
placeholder="USER, ROLE, CLIENT..." placeholder="USER, ROLE, CLIENT..."
styleClass="w-full" /> iconLeft="pi pi-database" />
</div> </div>
</div> </div>
<div class="flex gap-2 justify-content-end mt-4"> <div class="flex gap-2 justify-content-end mt-4">
<p:commandButton <fr:commandButton value="Rechercher"
value="Rechercher"
icon="pi pi-search" icon="pi pi-search"
styleClass="p-button-primary" severity="primary"
action="#{auditConsultationBean.searchLogs}" action="#{auditConsultationBean.searchLogs}"
update=":formAuditLogs:auditLogsTable" /> update=":formAuditLogs:auditLogsTable" />
<p:commandButton <fr:commandButton value="Réinitialiser"
value="Réinitialiser"
icon="pi pi-refresh" icon="pi pi-refresh"
styleClass="p-button-secondary" severity="secondary"
action="#{auditConsultationBean.resetFilters}" action="#{auditConsultationBean.resetFilters}"
update=":formAuditLogs:auditLogsTable @form" /> update=":formAuditLogs:auditLogsTable @form" />
</div> </div>
@@ -227,7 +223,7 @@
<i class="pi pi-list text-blue-500" style="font-size: 1.5rem"></i> <i class="pi pi-list text-blue-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Logs d'Audit</h5> <h5 class="m-0">Logs d'Audit</h5>
</div> </div>
<p:tag value="#{auditConsultationBean.totalRecords} log(s)" <fr:tag value="#{auditConsultationBean.totalRecords} log(s)"
severity="info" severity="info"
icon="pi pi-history" /> icon="pi pi-history" />
</div> </div>
@@ -245,16 +241,16 @@
currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}" currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}"
styleClass="w-full" styleClass="w-full"
emptyMessage="Aucun log d'audit trouvé" emptyMessage="Aucun log d'audit trouvé"
reflow="true"> responsiveLayout="scroll">
<!-- Colonne Statut --> <!-- Colonne Statut -->
<p:column headerText="Statut" style="width: 100px; text-align: center"> <p:column headerText="Statut" style="width: 100px; text-align: center" priority="2">
<p:tag value="#{log.succes ? 'Succès' : 'Échec'}" <fr:tag value="#{log.succes ? 'Succès' : 'Échec'}"
severity="#{log.succes ? 'success' : 'danger'}" /> severity="#{log.succes ? 'success' : 'danger'}" />
</p:column> </p:column>
<!-- Colonne Type d'action --> <!-- 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"> <div class="flex align-items-center gap-2">
<i class="pi pi-bolt text-orange-500"></i> <i class="pi pi-bolt text-orange-500"></i>
<span class="font-semibold text-900">#{log.typeAction}</span> <span class="font-semibold text-900">#{log.typeAction}</span>
@@ -262,7 +258,7 @@
</p:column> </p:column>
<!-- Colonne Acteur --> <!-- 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="flex align-items-center gap-2">
<div class="border-circle bg-primary text-white flex align-items-center justify-content-center" <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;"> style="width: 32px; height: 32px; flex-shrink: 0; font-size: 0.75rem;">
@@ -275,7 +271,7 @@
</p:column> </p:column>
<!-- Colonne Ressource --> <!-- 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"> <div class="flex align-items-center gap-2">
<i class="pi pi-database text-blue-500"></i> <i class="pi pi-database text-blue-500"></i>
<span class="text-900">#{log.ressourceType}</span> <span class="text-900">#{log.ressourceType}</span>
@@ -283,7 +279,7 @@
</p:column> </p:column>
<!-- Colonne Date --> <!-- 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"> <div class="flex align-items-center gap-2">
<i class="pi pi-calendar text-purple-500"></i> <i class="pi pi-calendar text-purple-500"></i>
<span class="text-900">#{log.dateAction}</span> <span class="text-900">#{log.dateAction}</span>
@@ -291,28 +287,30 @@
</p:column> </p:column>
<!-- Colonne Détails --> <!-- 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;"> <span class="text-600 text-sm" style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
#{not empty log.details ? log.details : '-'} #{not empty log.details ? log.details : '-'}
</span> </span>
</p:column> </p:column>
<!-- Colonne IP --> <!-- 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"> <span class="text-600 text-sm font-mono">
#{not empty log.adresseIp ? log.adresseIp : '-'} #{not empty log.adresseIp ? log.adresseIp : '-'}
</span> </span>
</p:column> </p:column>
<!-- Colonne Actions --> <!-- Colonne Actions -->
<p:column headerText="Actions" style="width: 80px; text-align: center"> <p:column headerText="Actions" style="width: 80px; text-align: center" priority="1">
<p:commandButton <fr:commandButton icon="pi pi-eye"
icon="pi pi-eye" rounded="true"
styleClass="p-button-rounded p-button-text p-button-sm p-button-info" text="true"
size="small"
severity="info"
title="Voir les détails" title="Voir les détails"
onclick="PF('auditLogDetailsDialog').show()"> onclick="PF('auditLogDetailsDialog').show()">
<f:setPropertyActionListener target="#{auditConsultationBean.selectedLog}" value="#{log}" /> <f:setPropertyActionListener target="#{auditConsultationBean.selectedLog}" value="#{log}" />
</p:commandButton> </fr:commandButton>
</p:column> </p:column>
</p:dataTable> </p:dataTable>
</h:form> </h:form>
@@ -336,7 +334,7 @@
<div class="surface-50 border-round p-3"> <div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between"> <div class="flex align-items-center justify-content-between">
<span class="text-600 font-medium">Statut</span> <span class="text-600 font-medium">Statut</span>
<p:tag value="#{auditConsultationBean.selectedLog.succes ? 'Succès' : 'Échec'}" <fr:tag value="#{auditConsultationBean.selectedLog.succes ? 'Succès' : 'Échec'}"
severity="#{auditConsultationBean.selectedLog.succes ? 'success' : 'danger'}" /> severity="#{auditConsultationBean.selectedLog.succes ? 'success' : 'danger'}" />
</div> </div>
</div> </div>
@@ -409,10 +407,9 @@
</div> </div>
<div class="flex justify-content-end mt-4"> <div class="flex justify-content-end mt-4">
<p:commandButton <fr:commandButton value="Fermer"
value="Fermer"
icon="pi pi-times" icon="pi pi-times"
styleClass="p-button-secondary" severity="secondary"
onclick="PF('auditLogDetailsDialog').hide()" onclick="PF('auditLogDetailsDialog').hide()"
type="button" /> type="button" />
</div> </div>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml"> template="/templates/main-template.xhtml">
<ui:define name="title">Tableau de Bord - Lions User Manager</ui:define> <ui:define name="title">Tableau de Bord - Lions User Manager</ui:define>
@@ -21,13 +22,12 @@
<i class="pi pi-home text-blue-500" style="font-size: 2rem"></i> <i class="pi pi-home text-blue-500" style="font-size: 2rem"></i>
<div> <div>
<h3 class="m-0 mb-1">Tableau de Bord</h3> <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>
</div> </div>
<p:commandButton <fr:commandButton value="Rafraîchir"
value="Rafraîchir"
icon="pi pi-refresh" icon="pi pi-refresh"
styleClass="p-button-secondary" severity="secondary"
action="#{dashboardBean.refreshStatistics}" action="#{dashboardBean.refreshStatistics}"
update=":formDashboard" /> update=":formDashboard" />
</div> </div>
@@ -35,100 +35,161 @@
</div> </div>
<!-- ================================================================ <!-- ================================================================
STATISTIQUES PRINCIPALES (4 KPI CARDS) STATISTIQUES PRINCIPALES - KPIs MÉTIER
================================================================ --> ================================================================ -->
<div class="col-12"> <div class="col-12">
<h5 class="mb-3">Statistiques Principales</h5> <h5 class="mb-3">Indicateurs Clés de Performance</h5>
</div> </div>
<!-- KPI 1: Utilisateurs Actifs --> <!-- KPI 1: Total Utilisateurs -->
<div class="col-12 md:col-6 lg:col-3"> <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"> <div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<p:commandButton <fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
outcome="/pages/user-manager/users/list"> outcome="/pages/user-manager/users/list">
<div class="flex align-items-start justify-content-between mb-3"> <div class="flex align-items-start justify-content-between">
<div> <div>
<div class="text-500 font-medium mb-1">Utilisateurs Actifs</div> <div class="text-500 font-medium mb-2 text-sm uppercase">Total Utilisateurs</div>
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalUsersDisplay}</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>
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle" <div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
style="width: 2.5rem; height: 2.5rem"> style="width: 3.5rem; height: 3.5rem">
<i class="pi pi-users text-blue-600 text-xl"></i> <i class="pi pi-users text-blue-600" style="font-size: 1.75rem"></i>
</div> </div>
</div> </div>
<div class="text-500 text-sm"> </fr:commandButton>
<i class="pi pi-arrow-right text-600"></i>
<span class="ml-2">Total utilisateurs</span>
</div>
</p:commandButton>
</div> </div>
</div> </div>
<!-- KPI 2: Rôles Realm --> <!-- KPI 2: Utilisateurs Actifs -->
<div class="col-12 md:col-6 lg:col-3"> <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"> <div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<p:commandButton <fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent" outcome="/pages/user-manager/users/list">
outcome="/pages/user-manager/roles/list"> <div class="flex align-items-start justify-content-between">
<div class="flex align-items-start justify-content-between mb-3">
<div> <div>
<div class="text-500 font-medium mb-1">Rôles Realm</div> <div class="text-500 font-medium mb-2 text-sm uppercase">Utilisateurs Actifs</div>
<div class="text-900 font-bold text-2xl">#{dashboardBean.totalRolesDisplay}</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>
<div class="flex align-items-center justify-content-center bg-green-100 border-circle" <div class="flex align-items-center justify-content-center bg-green-100 border-circle"
style="width: 2.5rem; height: 2.5rem"> style="width: 3.5rem; height: 3.5rem">
<i class="pi pi-shield text-green-600 text-xl"></i> <i class="pi pi-check-circle text-green-600" style="font-size: 1.75rem"></i>
</div> </div>
</div> </div>
<div class="text-500 text-sm"> </fr:commandButton>
<i class="pi pi-arrow-right text-600"></i>
<span class="ml-2">Rôles configurés</span>
</div>
</p:commandButton>
</div> </div>
</div> </div>
<!-- KPI 3: Actions Récentes --> <!-- KPI 3: Utilisateurs Inactifs -->
<div class="col-12 md:col-6 lg:col-3"> <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"> <div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<p:commandButton <fr:commandButton styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent"
styleClass="p-0 w-full text-left border-none bg-transparent hover:bg-transparent active:bg-transparent" outcome="/pages/user-manager/users/list">
outcome="/pages/user-manager/audit/logs"> <div class="flex align-items-start justify-content-between">
<div class="flex align-items-start justify-content-between mb-3">
<div> <div>
<div class="text-500 font-medium mb-1">Actions Récentes</div> <div class="text-500 font-medium mb-2 text-sm uppercase">Utilisateurs Inactifs</div>
<div class="text-900 font-bold text-2xl">#{dashboardBean.recentActionsDisplay}</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>
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle" <div class="flex align-items-center justify-content-center bg-orange-100 border-circle"
style="width: 2.5rem; height: 2.5rem"> style="width: 3.5rem; height: 3.5rem">
<i class="pi pi-history text-orange-600 text-xl"></i> <i class="pi pi-ban text-orange-600" style="font-size: 1.75rem"></i>
</div> </div>
</div> </div>
<div class="text-500 text-sm"> </fr:commandButton>
<i class="pi pi-arrow-right text-600"></i>
<span class="ml-2">Dernières 24h</span>
</div>
</p:commandButton>
</div> </div>
</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="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg"> <div class="card surface-0 border-round-lg hover:surface-100 cursor-pointer transition-colors transition-duration-150">
<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/audit/logs">
<div class="flex align-items-start justify-content-between">
<div> <div>
<div class="text-500 font-medium mb-1">Realm Actif</div> <div class="text-500 font-medium mb-2 text-sm uppercase">Taux de Succès</div>
<div class="text-900 font-bold text-xl" style="word-break: break-word;">lions-user-manager</div> <div class="text-900 font-bold text-4xl">#{dashboardBean.successRate24hDisplay}</div>
</div> <div class="text-600 text-sm mt-2">
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle" <i class="pi pi-chart-line mr-2"></i>
style="width: 2.5rem; height: 2.5rem"> Dernières 24h
<i class="pi pi-globe text-purple-600 text-xl"></i>
</div> </div>
</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é &amp; Performance</h5>
</div>
<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"> <div class="flex align-items-center gap-2">
<p:tag value="Opérationnel" severity="success" styleClass="text-xs" /> <i class="pi pi-check-circle text-green-500"></i>
<span class="text-500 text-sm">Realm Keycloak</span> <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> </div>
</div> </div>
@@ -145,31 +206,31 @@
<div class="grid"> <div class="grid">
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<p:commandButton <fr:commandButton value="Nouvel Utilisateur"
value="Nouvel Utilisateur"
icon="pi pi-user-plus" icon="pi pi-user-plus"
styleClass="w-full p-button-success mb-2" severity="success"
styleClass="w-full mb-2"
outcome="/pages/user-manager/users/create" /> outcome="/pages/user-manager/users/create" />
</div> </div>
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<p:commandButton <fr:commandButton value="Liste des Utilisateurs"
value="Liste des Utilisateurs"
icon="pi pi-users" icon="pi pi-users"
styleClass="w-full p-button-primary mb-2" severity="primary"
styleClass="w-full mb-2"
outcome="/pages/user-manager/users/list" /> outcome="/pages/user-manager/users/list" />
</div> </div>
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<p:commandButton <fr:commandButton value="Gestion des Rôles"
value="Gestion des Rôles"
icon="pi pi-shield" icon="pi pi-shield"
styleClass="w-full p-button-info mb-2" severity="info"
styleClass="w-full mb-2"
outcome="/pages/user-manager/roles/list" /> outcome="/pages/user-manager/roles/list" />
</div> </div>
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<p:commandButton <fr:commandButton value="Journal d'Audit"
value="Journal d'Audit"
icon="pi pi-history" icon="pi pi-history"
styleClass="w-full p-button-help mb-2" severity="help"
styleClass="w-full mb-2"
outcome="/pages/user-manager/audit/logs" /> outcome="/pages/user-manager/audit/logs" />
</div> </div>
</div> </div>
@@ -179,7 +240,113 @@
<i class="pi pi-lightbulb text-orange-500"></i> <i class="pi pi-lightbulb text-orange-500"></i>
<div> <div>
<div class="text-700 font-semibold text-sm">Conseil</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> </div>
</div> </div>
@@ -192,7 +359,7 @@
<div class="col-12 lg:col-6"> <div class="col-12 lg:col-6">
<div class="card h-full"> <div class="card h-full">
<div class="flex align-items-center gap-2 mb-4"> <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> <h5 class="m-0">Informations Système</h5>
</div> </div>
@@ -202,34 +369,12 @@
<div class="flex align-items-center justify-content-between"> <div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2"> <div class="flex align-items-center gap-2">
<i class="pi pi-tag text-500"></i> <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> </div>
<span class="font-semibold text-900">1.0.0</span> <span class="font-semibold text-900">1.0.0</span>
</div> </div>
</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 --> <!-- Framework -->
<div class="surface-50 border-round p-3"> <div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between"> <div class="flex align-items-center justify-content-between">
@@ -237,90 +382,18 @@
<i class="pi pi-code text-500"></i> <i class="pi pi-code text-500"></i>
<span class="text-600 font-medium">Framework</span> <span class="text-600 font-medium">Framework</span>
</div> </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>
</div> </div>
<!-- Interface --> <!-- Statut -->
<div class="surface-50 border-round p-3"> <div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between"> <div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2"> <div class="flex align-items-center gap-2">
<i class="pi pi-palette text-500"></i> <i class="pi pi-check-circle text-500"></i>
<span class="text-600 font-medium">Interface</span> <span class="text-600 font-medium">Statut Système</span>
</div> </div>
<span class="font-semibold text-900">PrimeFaces Freya</span> <fr:tag value="Opérationnel" severity="success" />
</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>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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>

View File

@@ -42,10 +42,13 @@
</div> </div>
<!-- ================================================================ <!-- ================================================================
FILTRES FILTRES & KPI INTÉGRÉS (Nombre d'or φ = 1.618 → 62%/38%)
================================================================ --> ================================================================ -->
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="grid">
<!-- =========== 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"> <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> <i class="pi pi-filter text-blue-500"></i>
Filtres Filtres
@@ -54,14 +57,11 @@
<h:form id="formFilters"> <h:form id="formFilters">
<div class="grid"> <div class="grid">
<!-- Realm --> <!-- Realm -->
<div class="col-12 md:col-4"> <div class="col-12">
<div class="field mb-0"> <fr:fieldSelect id="realmFilter"
<label for="realmFilter" class="block text-900 font-medium mb-2"> label="Realm"
Realm
</label>
<p:selectOneMenu id="realmFilter"
value="#{roleGestionBean.realmName}" value="#{roleGestionBean.realmName}"
styleClass="w-full"> iconLeft="pi pi-globe">
<f:selectItem itemLabel="Sélectionner un realm..." itemValue="" /> <f:selectItem itemLabel="Sélectionner un realm..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableRealms}" <f:selectItems value="#{roleGestionBean.availableRealms}"
var="realm" var="realm"
@@ -70,19 +70,15 @@
<p:ajax event="change" <p:ajax event="change"
listener="#{roleGestionBean.loadRealmRoles}" listener="#{roleGestionBean.loadRealmRoles}"
update=":formRealmRoles :formClientRoles :formKpis" /> update=":formRealmRoles :formClientRoles :formKpis" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
</div> </div>
<!-- Client --> <!-- Client -->
<div class="col-12 md:col-4"> <div class="col-12">
<div class="field mb-0"> <fr:fieldSelect id="clientFilter"
<label for="clientFilter" class="block text-900 font-medium mb-2"> label="Client (optionnel)"
Client (optionnel)
</label>
<p:selectOneMenu id="clientFilter"
value="#{roleGestionBean.clientName}" value="#{roleGestionBean.clientName}"
styleClass="w-full"> iconLeft="pi pi-box">
<f:selectItem itemLabel="Tous les clients" itemValue="" /> <f:selectItem itemLabel="Tous les clients" itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}" <f:selectItems value="#{roleGestionBean.availableClients}"
var="client" var="client"
@@ -91,116 +87,88 @@
<p:ajax event="change" <p:ajax event="change"
listener="#{roleGestionBean.loadClientRoles}" listener="#{roleGestionBean.loadClientRoles}"
update=":formClientRoles" /> update=":formClientRoles" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
</div> </div>
<!-- Type --> <!-- Type -->
<div class="col-12 md:col-4"> <div class="col-12">
<div class="field mb-0"> <fr:fieldSelect id="typeFilter"
<label for="typeFilter" class="block text-900 font-medium mb-2"> label="Type de rôle"
Type de rôle
</label>
<p:selectOneMenu id="typeFilter"
value="#{roleGestionBean.selectedTypeRole}" value="#{roleGestionBean.selectedTypeRole}"
styleClass="w-full"> iconLeft="pi pi-filter">
<f:selectItem itemLabel="Tous les types" itemValue="" /> <f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItems value="#{roleGestionBean.typeRoleOptions}" <f:selectItems value="#{roleGestionBean.typeRoleOptions}"
var="type" var="type"
itemLabel="#{type}" itemLabel="#{type}"
itemValue="#{type}" /> itemValue="#{type}" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
</div> </div>
</div> </div>
</h:form> </h:form>
</div> </div>
</div>
<!-- ================================================================ <!-- =========== KPI STATISTIQUES (38%) =========== -->
KPI CARDS <div class="col-12 lg:col-5">
================================================================ --> <h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
<div class="col-12"> <i class="pi pi-chart-bar text-purple-500"></i>
Statistiques <small class="text-500 font-normal">(φ = 1.618)</small>
</h3>
<h:form id="formKpis"> <h:form id="formKpis">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-6 lg:col-3"> <!-- KPI 1: Rôles Realm -->
<div class="card surface-0 border-round-lg"> <div class="col-12 md:col-6">
<div class="flex align-items-start justify-content-between mb-3"> <div class="surface-50 border-round p-3 text-center h-full">
<div> <div class="flex align-items-center justify-content-center bg-purple-100 border-circle mx-auto mb-2"
<div class="text-500 font-medium mb-1">Rôles Realm</div> style="width: 2rem; height: 2rem">
<div class="text-900 font-bold text-2xl">#{roleGestionBean.realmRoles.size()}</div> <i class="pi pi-shield text-purple-600 text-sm"></i>
</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>
</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>
<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> </div>
<div class="col-12 md:col-6 lg:col-3"> <!-- KPI 2: Rôles Client -->
<div class="card surface-0 border-round-lg"> <div class="col-12 md:col-6">
<div class="flex align-items-start justify-content-between mb-3"> <div class="surface-50 border-round p-3 text-center h-full">
<div> <div class="flex align-items-center justify-content-center bg-blue-100 border-circle mx-auto mb-2"
<div class="text-500 font-medium mb-1">Rôles Client</div> style="width: 2rem; height: 2rem">
<div class="text-900 font-bold text-2xl">#{roleGestionBean.clientRoles.size()}</div> <i class="pi pi-sitemap text-blue-600 text-sm"></i>
</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 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> </div>
<div class="col-12 md:col-6 lg:col-3"> <!-- KPI 3: Total Rôles -->
<div class="card surface-0 border-round-lg"> <div class="col-12 md:col-6">
<div class="flex align-items-start justify-content-between mb-3"> <div class="surface-50 border-round p-3 text-center h-full">
<div> <div class="flex align-items-center justify-content-center bg-green-100 border-circle mx-auto mb-2"
<div class="text-500 font-medium mb-1">Total Rôles</div> style="width: 2rem; height: 2rem">
<div class="text-900 font-bold text-2xl">#{roleGestionBean.allRoles.size()}</div> <i class="pi pi-check-circle text-green-600 text-sm"></i>
</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 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>
</div> </div>
<div class="col-12 md:col-6 lg:col-3"> <!-- KPI 4: Realm Actif -->
<div class="card surface-0 border-round-lg"> <div class="col-12 md:col-6">
<div class="flex align-items-start justify-content-between mb-3"> <div class="surface-50 border-round p-3 text-center h-full">
<div> <div class="flex align-items-center justify-content-center bg-orange-100 border-circle mx-auto mb-2"
<div class="text-500 font-medium mb-1">Realm Actif</div> style="width: 2rem; height: 2rem">
<div class="text-900 font-bold text-xl">#{roleGestionBean.realmName}</div> <i class="pi pi-database text-orange-600 text-sm"></i>
</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>
</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>
<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>
</div> </div>
</h:form> </h:form>
</div> </div>
</div>
</div>
</div>
<!-- ================================================================ <!-- ================================================================
RÔLES REALM RÔLES REALM
@@ -237,15 +205,18 @@
</p> </p>
</div> </div>
<div class="flex align-items-center gap-1"> <div class="flex align-items-center gap-1">
<p:commandButton icon="pi pi-trash" <fr:commandButton icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger" severity="danger"
rounded="true"
text="true"
size="small"
title="Supprimer" title="Supprimer"
action="#{roleGestionBean.deleteRealmRole(role.name)}" action="#{roleGestionBean.deleteRealmRole(role.name)}"
update=":formRealmRoles :formKpis"> update=":formRealmRoles :formKpis">
<p:confirm header="Confirmation" <p:confirm header="Confirmation"
message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?" message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?"
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </fr:commandButton>
</div> </div>
</div> </div>
@@ -317,15 +288,18 @@
</p> </p>
</div> </div>
<div class="flex align-items-center gap-1"> <div class="flex align-items-center gap-1">
<p:commandButton icon="pi pi-trash" <fr:commandButton icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger" severity="danger"
rounded="true"
text="true"
size="small"
title="Supprimer" title="Supprimer"
action="#{roleGestionBean.deleteClientRole(role.name)}" action="#{roleGestionBean.deleteClientRole(role.name)}"
update=":formClientRoles :formKpis"> update=":formClientRoles :formKpis">
<p:confirm header="Confirmation" <p:confirm header="Confirmation"
message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?" message="Voulez-vous vraiment supprimer le rôle '#{role.name}' ?"
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </fr:commandButton>
</div> </div>
</div> </div>
@@ -368,149 +342,107 @@
<!-- ================================================================ <!-- ================================================================
DIALOG CRÉATION RÔLE REALM DIALOG CRÉATION RÔLE REALM
================================================================ --> ================================================================ -->
<p:dialog header="Nouveau Rôle Realm" <fr:formDialog widgetVar="createRealmRoleDialog"
widgetVar="createRealmRoleDialog" header="Nouveau Rôle Realm"
modal="true" formId="formCreateRealmRole"
responsive="true" saveLabel="Créer"
width="600" cancelLabel="Annuler"
showEffect="fade" saveAction="#{roleGestionBean.createRealmRole}"
hideEffect="fade"> update=":formRealmRoles :formKpis :formCreateRealmRole"
<h:form id="formCreateRealmRole"> width="600">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<div class="field mb-3"> <fr:fieldInput id="realmRoleName"
<label for="realmRoleName" class="block text-900 font-medium mb-2"> label="Nom du rôle"
Nom du rôle <span class="text-red-500">*</span>
</label>
<p:inputText id="realmRoleName"
value="#{roleGestionBean.newRole.name}" value="#{roleGestionBean.newRole.name}"
styleClass="w-full"
required="true" required="true"
placeholder="ex: admin_lions"> placeholder="ex: admin_lions"
<f:validateLength minimum="2" maximum="100" /> iconLeft="pi pi-tag"
<f:validateRegex pattern="^[a-zA-Z0-9_-]+$" /> helpText="Lettres, chiffres, underscores et tirets uniquement">
</p:inputText> <f:validateLength for="input" minimum="2" maximum="100" />
<small class="text-500">Lettres, chiffres, underscores et tirets uniquement</small> <f:validateRegex for="input" pattern="^[a-zA-Z0-9_-]+$" />
</fr:fieldInput>
</div> </div>
<div class="field mb-0"> <div class="col-12">
<label for="realmRoleDesc" class="block text-900 font-medium mb-2"> <fr:fieldTextarea id="realmRoleDesc"
Description label="Description"
</label>
<p:inputTextarea id="realmRoleDesc"
value="#{roleGestionBean.newRole.description}" value="#{roleGestionBean.newRole.description}"
styleClass="w-full"
rows="3" rows="3"
placeholder="Description du rôle..."> placeholder="Description du rôle..."
</p:inputTextarea> iconLeft="pi pi-align-left" />
</div>
</div> </div>
</div> </div>
<p:messages id="messagesRealmRole" showDetail="true" closable="true" styleClass="mt-3"> <fr:message id="messagesRealmRole" showDetail="true" closable="true">
<p:autoUpdate /> <p:autoUpdate />
</p:messages> </fr:message>
</h:form> </fr:formDialog>
<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 &amp;&amp; !args.validationFailed) PF('createRealmRoleDialog').hide();" />
</f:facet>
</p:dialog>
<!-- ================================================================ <!-- ================================================================
DIALOG CRÉATION RÔLE CLIENT DIALOG CRÉATION RÔLE CLIENT
================================================================ --> ================================================================ -->
<p:dialog header="Nouveau Rôle Client" <fr:formDialog widgetVar="createClientRoleDialog"
widgetVar="createClientRoleDialog" header="Nouveau Rôle Client"
modal="true" formId="formCreateClientRole"
responsive="true" saveLabel="Créer"
width="600" cancelLabel="Annuler"
showEffect="fade" saveAction="#{roleGestionBean.createClientRole}"
hideEffect="fade"> update=":formClientRoles :formKpis :formCreateClientRole"
<h:form id="formCreateClientRole"> width="600">
<div class="grid"> <div class="grid">
<div class="col-12"> <div class="col-12">
<div class="field mb-3"> <fr:fieldInput id="clientRoleName"
<label for="clientRoleName" class="block text-900 font-medium mb-2"> label="Nom du rôle"
Nom du rôle <span class="text-red-500">*</span>
</label>
<p:inputText id="clientRoleName"
value="#{roleGestionBean.newRole.name}" value="#{roleGestionBean.newRole.name}"
styleClass="w-full"
required="true" required="true"
placeholder="ex: manager"> placeholder="ex: manager"
<f:validateLength minimum="2" maximum="100" /> iconLeft="pi pi-tag"
<f:validateRegex pattern="^[a-zA-Z0-9_-]+$" /> helpText="Lettres, chiffres, underscores et tirets uniquement">
</p:inputText> <f:validateLength for="input" minimum="2" maximum="100" />
<f:validateRegex for="input" pattern="^[a-zA-Z0-9_-]+$" />
</fr:fieldInput>
</div> </div>
<div class="field mb-3"> <div class="col-12">
<label for="clientName" class="block text-900 font-medium mb-2"> <fr:fieldSelect id="clientName"
Client <span class="text-red-500">*</span> label="Client"
</label>
<p:selectOneMenu id="clientName"
value="#{roleGestionBean.clientName}" value="#{roleGestionBean.clientName}"
styleClass="w-full" required="true"
required="true"> iconLeft="pi pi-box">
<f:selectItem itemLabel="Sélectionner un client..." itemValue="" /> <f:selectItem itemLabel="Sélectionner un client..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}" /> <f:selectItems value="#{roleGestionBean.availableClients}" />
</p:selectOneMenu> </fr:fieldSelect>
</div> </div>
<div class="field mb-0"> <div class="col-12">
<label for="clientRoleDesc" class="block text-900 font-medium mb-2"> <fr:fieldTextarea id="clientRoleDesc"
Description label="Description"
</label>
<p:inputTextarea id="clientRoleDesc"
value="#{roleGestionBean.newRole.description}" value="#{roleGestionBean.newRole.description}"
styleClass="w-full"
rows="3" rows="3"
placeholder="Description du rôle..."> placeholder="Description du rôle..."
</p:inputTextarea> iconLeft="pi pi-align-left" />
</div>
</div> </div>
</div> </div>
<p:messages id="messagesClientRole" showDetail="true" closable="true" styleClass="mt-3"> <fr:message id="messagesClientRole" showDetail="true" closable="true">
<p:autoUpdate /> <p:autoUpdate />
</p:messages> </fr:message>
</h:form> </fr:formDialog>
<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 &amp;&amp; !args.validationFailed) PF('createClientRoleDialog').hide();" />
</f:facet>
</p:dialog>
<!-- ================================================================ <!-- ================================================================
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" <p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
responsive="true" width="400"> responsive="true" width="400">
<p:commandButton value="Non" type="button" <fr:commandButton value="Non"
styleClass="p-button-text" type="button"
text="true"
icon="pi pi-times" /> icon="pi pi-times" />
<p:commandButton value="Oui" type="button" <fr:commandButton value="Oui"
styleClass="p-button-danger" type="button"
severity="danger"
icon="pi pi-check" /> icon="pi pi-check" />
</p:confirmDialog> </p:confirmDialog>
</ui:define> </ui:define>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
template="/templates/main-template.xhtml"> template="/templates/main-template.xhtml">
@@ -24,31 +25,43 @@
<div class="card"> <div class="card">
<h5>Informations du compte</h5> <h5>Informations du compte</h5>
<h:form id="formAccountInfo"> <h:form id="formAccountInfo">
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-4, col-12 md:col-8"> <div class="grid">
<p:outputLabel for="username" value="Nom d'utilisateur" /> <div class="col-12 md:col-6">
<p:inputText id="username" <fr:fieldInput id="username"
label="Nom d'utilisateur"
value="#{userSessionBean.username}" value="#{userSessionBean.username}"
readonly="true" disabled="true"
styleClass="w-full" /> iconLeft="pi pi-user"
helpText="Votre identifiant unique" />
</div>
<p:outputLabel for="email" value="Email" /> <div class="col-12 md:col-6">
<p:inputText id="email" <fr:fieldInput id="email"
label="Email"
value="#{userSessionBean.email}" value="#{userSessionBean.email}"
readonly="true" disabled="true"
styleClass="w-full" /> iconLeft="pi pi-envelope"
helpText="Votre adresse email" />
</div>
<p:outputLabel for="fullName" value="Nom complet" /> <div class="col-12 md:col-6">
<p:inputText id="fullName" <fr:fieldInput id="fullName"
label="Nom complet"
value="#{userSessionBean.fullName}" value="#{userSessionBean.fullName}"
readonly="true" disabled="true"
styleClass="w-full" /> iconLeft="pi pi-id-card"
helpText="Votre nom complet" />
</div>
<p:outputLabel for="mainRole" value="Rôle principal" /> <div class="col-12 md:col-6">
<p:inputText id="mainRole" <fr:fieldInput id="mainRole"
label="Rôle principal"
value="#{userSessionBean.mainRole}" value="#{userSessionBean.mainRole}"
readonly="true" disabled="true"
styleClass="w-full" /> iconLeft="pi pi-shield"
</p:panelGrid> helpText="Votre rôle dans l'application" />
</div>
</div>
</h:form> </h:form>
</div> </div>
</div> </div>
@@ -59,35 +72,34 @@
<h5>Préférences</h5> <h5>Préférences</h5>
<h:form id="formPreferences"> <h:form id="formPreferences">
<div class="flex flex-column gap-3"> <div class="flex flex-column gap-3">
<div class="flex align-items-center justify-content-between"> <fr:fieldSelect id="componentTheme"
<span class="text-600">Thème des composants</span> label="Thème des composants"
<p:selectOneMenu value="#{guestPreferences.componentTheme}" value="#{guestPreferences.componentTheme}"
styleClass="w-12rem"> iconLeft="pi pi-palette">
<f:selectItems value="#{guestPreferences.componentThemes}" <f:selectItems value="#{guestPreferences.componentThemes}"
var="theme" var="theme"
itemLabel="#{theme.name}" itemLabel="#{theme.name}"
itemValue="#{theme.file}" /> itemValue="#{theme.file}" />
<p:ajax event="change" update="@form" /> <p:ajax event="change" update="@form" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
<div class="flex align-items-center justify-content-between"> <fr:fieldSelect id="darkMode"
<span class="text-600">Mode sombre</span> label="Mode sombre"
<p:selectOneMenu value="#{guestPreferences.darkMode}" value="#{guestPreferences.darkMode}"
styleClass="w-12rem"> iconLeft="pi pi-moon">
<f:selectItem itemLabel="Clair" itemValue="light" /> <f:selectItem itemLabel="Clair" itemValue="light" />
<f:selectItem itemLabel="Sombre" itemValue="dark" /> <f:selectItem itemLabel="Sombre" itemValue="dark" />
<p:ajax event="change" update="@form" /> <p:ajax event="change" update="@form" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
<div class="flex align-items-center justify-content-between"> <fr:fieldSelect id="inputStyle"
<span class="text-600">Style d'input</span> label="Style d'input"
<p:selectOneMenu value="#{guestPreferences.inputStyle}" value="#{guestPreferences.inputStyle}"
styleClass="w-12rem"> iconLeft="pi pi-sliders-h">
<f:selectItem itemLabel="Outlined" itemValue="outlined" /> <f:selectItem itemLabel="Outlined" itemValue="outlined" />
<f:selectItem itemLabel="Filled" itemValue="filled" /> <f:selectItem itemLabel="Filled" itemValue="filled" />
<p:ajax event="change" update="@form" /> <p:ajax event="change" update="@form" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
</div> </div>
</h:form> </h:form>
</div> </div>
@@ -97,27 +109,24 @@
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<h5>Actions</h5> <h5>Actions</h5>
<div class="flex gap-2"> <div class="flex flex-wrap gap-2">
<h:form> <h:form>
<p:commandButton <fr:commandButton value="Rafraîchir les informations"
value="Rafraîchir les informations"
icon="pi pi-refresh" icon="pi pi-refresh"
styleClass="p-button-secondary" severity="secondary"
action="#{userSessionBean.loadUserInfo}" action="#{userSessionBean.loadUserInfo}"
update="formAccountInfo" /> update="formAccountInfo" />
</h:form> </h:form>
<h:form> <h:form>
<p:commandButton <fr:commandButton value="Changer le mot de passe"
value="Changer le mot de passe"
icon="pi pi-key" icon="pi pi-key"
styleClass="p-button-info" severity="info"
outcome="/pages/user-manager/users/profile" /> outcome="/pages/user-manager/users/profile" />
</h:form> </h:form>
<h:form> <h:form>
<p:commandButton <fr:commandButton value="Sauvegarder les préférences"
value="Sauvegarder les préférences"
icon="pi pi-save" icon="pi pi-save"
styleClass="p-button-success" severity="success"
action="#{settingsBean.savePreferences}" action="#{settingsBean.savePreferences}"
update="@form" /> update="@form" />
</h:form> </h:form>

View File

@@ -4,6 +4,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml"> template="/templates/main-template.xhtml">
<ui:define name="title">Synchronisation Keycloak - Lions User Manager</ui:define> <ui:define name="title">Synchronisation Keycloak - Lions User Manager</ui:define>

View File

@@ -26,16 +26,19 @@
</div> </div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<p:commandButton <fr:commandButton
icon="pi pi-question-circle" icon="pi pi-question-circle"
styleClass="p-button-rounded p-button-text p-button-help" rounded="true"
text="true"
severity="help"
title="Aide" title="Aide"
type="button" type="button"
onclick="PF('helpDialog').show();" /> onclick="PF('helpDialog').show();" />
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text"> <fr:button value="Retour"
<i class="pi pi-arrow-left mr-2"></i> icon="pi pi-arrow-left"
Retour severity="secondary"
</h:link> text="true"
outcome="/pages/user-manager/users/list" />
</div> </div>
</div> </div>
</div> </div>
@@ -125,14 +128,9 @@
<div class="grid"> <div class="grid">
<!-- Mot de passe --> <!-- Mot de passe -->
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<div class="field"> <fr:fieldPassword id="password"
<label for="password" class="block text-900 font-medium mb-2"> label="Mot de passe"
<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}" value="#{userCreationBean.password}"
styleClass="w-full"
required="true" required="true"
feedback="true" feedback="true"
toggleMask="true" toggleMask="true"
@@ -140,36 +138,36 @@
weakLabel="Faible" weakLabel="Faible"
goodLabel="Moyen" goodLabel="Moyen"
strongLabel="Fort" strongLabel="Fort"
placeholder="Minimum 8 caractères"> placeholder="Minimum 8 caractères"
<f:validateLength minimum="8" maximum="100" /> iconLeft="pi pi-lock"
</p:password> helpText="Au moins 8 caractères avec lettres et chiffres">
<small class="text-500"> <f:validateLength for="input" minimum="8" maximum="100" />
<i class="pi pi-shield mr-1"></i> <p:ajax event="keyup"
Au moins 8 caractères avec lettres et chiffres delay="500"
</small> listener="#{userCreationBean.validatePasswordMatch}"
</div> update="passwordConfirm passwordConfirmMsg"
process="@this passwordConfirm" />
</fr:fieldPassword>
</div> </div>
<!-- Confirmation mot de passe --> <!-- Confirmation mot de passe -->
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<div class="field"> <fr:fieldPassword id="passwordConfirm"
<label for="passwordConfirm" class="block text-900 font-medium mb-2"> label="Confirmer le mot de passe"
<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}" value="#{userCreationBean.passwordConfirm}"
styleClass="w-full"
required="true" required="true"
feedback="false" feedback="false"
toggleMask="true" toggleMask="true"
placeholder="Confirmer le mot de passe"> placeholder="Confirmer le mot de passe"
</p:password> iconLeft="pi pi-lock"
<small class="text-500"> helpText="Doit correspondre au mot de passe">
<i class="pi pi-info-circle mr-1"></i> <p:ajax event="keyup"
Doit correspondre au mot de passe delay="500"
</small> listener="#{userCreationBean.validatePasswordMatch}"
</div> update="passwordConfirmMsg"
process="@this password" />
</fr:fieldPassword>
<p:message id="passwordConfirmMsg" for="passwordConfirm" display="text" styleClass="mt-2" />
</div> </div>
</div> </div>
@@ -220,26 +218,20 @@
<div class="surface-50 border-round p-3"> <div class="surface-50 border-round p-3">
<div class="flex flex-column gap-3"> <div class="flex flex-column gap-3">
<!-- Compte activé --> <!-- Compte activé -->
<div class="flex align-items-center"> <fr:fieldCheckbox id="enabled"
<p:selectBooleanCheckbox id="enabled" label=""
value="#{userCreationBean.newUser.enabled}"> value="#{userCreationBean.newUser.enabled}"
</p:selectBooleanCheckbox> checkboxLabel="Compte activé">
<label for="enabled" class="ml-2 mb-0 cursor-pointer"> <small class="block text-500 mt-1">L'utilisateur peut se connecter immédiatement</small>
<span class="font-semibold text-900">Compte activé</span> </fr:fieldCheckbox>
<small class="block text-500">L'utilisateur peut se connecter immédiatement</small>
</label>
</div>
<!-- Email vérifié --> <!-- Email vérifié -->
<div class="flex align-items-center"> <fr:fieldCheckbox id="emailVerified"
<p:selectBooleanCheckbox id="emailVerified" label=""
value="#{userCreationBean.newUser.emailVerified}"> value="#{userCreationBean.newUser.emailVerified}"
</p:selectBooleanCheckbox> checkboxLabel="Email vérifié">
<label for="emailVerified" class="ml-2 mb-0 cursor-pointer"> <small class="block text-500 mt-1">Marquer l'email comme vérifié</small>
<span class="font-semibold text-900">Email vérifié</span> </fr:fieldCheckbox>
<small class="block text-500">Marquer l'email comme vérifié</small>
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -332,16 +324,17 @@
validateClient="true" /> validateClient="true" />
<!-- Bouton Réinitialiser --> <!-- Bouton Réinitialiser -->
<p:commandButton value="Réinitialiser" <fr:commandButton value="Réinitialiser"
icon="pi pi-refresh" icon="pi pi-refresh"
styleClass="p-button-secondary p-button-outlined" severity="secondary"
outlined="true"
action="#{userCreationBean.resetForm}" action="#{userCreationBean.resetForm}"
update=":formUserCreation" update=":formUserCreation"
immediate="true"> immediate="true">
<p:confirm header="Confirmation" <p:confirm header="Confirmation"
message="Voulez-vous vraiment réinitialiser le formulaire ?" message="Voulez-vous vraiment réinitialiser le formulaire ?"
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </fr:commandButton>
<!-- Bouton Annuler --> <!-- Bouton Annuler -->
<fr:commandButton value="Annuler" <fr:commandButton value="Annuler"

View File

@@ -91,6 +91,7 @@
placeholder="ex: jean.dupont@example.com" placeholder="ex: jean.dupont@example.com"
helpText="Adresse email valide"> helpText="Adresse email valide">
<f:validateRegex for="input" pattern="^[A-Za-z0-9+_.-]+@(.+)$" /> <f:validateRegex for="input" pattern="^[A-Za-z0-9+_.-]+@(.+)$" />
<p:ajax event="keyup" delay="500" update="previewPanel" />
</fr:fieldInput> </fr:fieldInput>
</div> </div>
@@ -103,6 +104,7 @@
placeholder="ex: Jean" placeholder="ex: Jean"
helpText="Prénom de l'utilisateur"> helpText="Prénom de l'utilisateur">
<f:validateLength for="input" minimum="2" maximum="100" /> <f:validateLength for="input" minimum="2" maximum="100" />
<p:ajax event="keyup" delay="500" update="previewPanel" />
</fr:fieldInput> </fr:fieldInput>
</div> </div>
@@ -115,6 +117,7 @@
placeholder="ex: Dupont" placeholder="ex: Dupont"
helpText="Nom de famille de l'utilisateur"> helpText="Nom de famille de l'utilisateur">
<f:validateLength for="input" minimum="2" maximum="100" /> <f:validateLength for="input" minimum="2" maximum="100" />
<p:ajax event="keyup" delay="500" update="previewPanel" />
</fr:fieldInput> </fr:fieldInput>
</div> </div>
@@ -157,26 +160,16 @@
<div class="surface-50 border-round p-3"> <div class="surface-50 border-round p-3">
<div class="flex flex-column gap-3"> <div class="flex flex-column gap-3">
<!-- Compte activé --> <!-- Compte activé -->
<div class="flex align-items-center"> <fr:fieldCheckbox id="enabled"
<p:selectBooleanCheckbox id="enabled" label="Compte activé"
value="#{userProfilBean.user.enabled}"> value="#{userProfilBean.user.enabled}"
</p:selectBooleanCheckbox> helpText="L'utilisateur peut se connecter" />
<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>
<!-- Email vérifié --> <!-- Email vérifié -->
<div class="flex align-items-center"> <fr:fieldCheckbox id="emailVerified"
<p:selectBooleanCheckbox id="emailVerified" label="Emailrifié"
value="#{userProfilBean.user.emailVerified}"> value="#{userProfilBean.user.emailVerified}"
</p:selectBooleanCheckbox> helpText="Marquer l'email comme vérifié" />
<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>
</div> </div>
</div> </div>
</div> </div>
@@ -193,9 +186,11 @@
<div class="card sticky" style="top: 1rem;"> <div class="card sticky" style="top: 1rem;">
<div class="flex align-items-center gap-2 mb-4"> <div class="flex align-items-center gap-2 mb-4">
<i class="pi pi-eye text-blue-500" style="font-size: 1.5rem"></i> <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> </div>
<h:panelGroup id="previewPanel">
<!-- Avatar Preview --> <!-- Avatar Preview -->
<div class="text-center mb-4"> <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);"> <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> </div>
</div> </div>
</h:panelGroup>
</div> </div>
</div> </div>
@@ -309,17 +306,9 @@
</div> </div>
<!-- ================================================================ <!-- ================================================================
DIALOG DE CONFIRMATION DIALOG DE CONFIRMATION (Freya Extension)
================================================================ --> ================================================================ -->
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade" <!-- Suppression désactivée - utiliser la page liste pour supprimer des utilisateurs -->
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>
</ui:define> </ui:define>
</ui:composition> </ui:composition>

View File

@@ -27,10 +27,25 @@
</div> </div>
</div> </div>
<div class="flex gap-2"> <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 <fr:commandButton
value="Rafraîchir" value="Rafraîchir"
icon="pi pi-refresh" icon="pi pi-refresh"
severity="secondary" severity="secondary"
outlined="true"
action="#{userListBean.refreshData}" action="#{userListBean.refreshData}"
update=":formUserList" update=":formUserList"
process="@this" /> process="@this" />
@@ -149,76 +164,67 @@
SECTION RECHERCHE ET FILTRES SECTION RECHERCHE ET FILTRES
================================================================ --> ================================================================ -->
<div class="col-12"> <div class="col-12">
<div class="card"> <fr:panel header="Recherche et Filtres"
<div class="flex align-items-center gap-2 mb-3"> toggleable="true"
<i class="pi pi-search text-blue-500" style="font-size: 1.5rem"></i> collapsed="false">
<h5 class="m-0">Recherche et Filtres</h5> <f:facet name="icons">
</div> <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="grid">
<div class="col-12 md:col-6 lg:col-4"> <div class="col-12 md:col-6 lg:col-4">
<div class="field"> <fr:fieldInput id="searchText"
<label for="searchText" class="block text-900 font-medium mb-2"> label="Recherche"
<i class="pi pi-search text-500 mr-1"></i>
Recherche
</label>
<p:inputText id="searchText"
value="#{userListBean.searchText}" value="#{userListBean.searchText}"
styleClass="w-full"
placeholder="Nom, email..."> placeholder="Nom, email...">
<p:ajax event="keyup" <p:ajax event="keyup"
delay="500" delay="500"
update=":formUserList:userTable" update=":formUserList:userTable"
listener="#{userListBean.search}" /> listener="#{userListBean.search}" />
</p:inputText> </fr:fieldInput>
</div>
</div> </div>
<div class="col-12 md:col-6 lg:col-3"> <div class="col-12 md:col-6 lg:col-3">
<div class="field"> <fr:fieldSelect id="realmFilter"
<label for="realmFilter" class="block text-900 font-medium mb-2"> label="Realm"
<i class="pi pi-globe text-500 mr-1"></i> value="#{userListBean.realmName}">
Realm
</label>
<p:selectOneMenu id="realmFilter"
value="#{userListBean.realmName}"
styleClass="w-full">
<f:selectItems value="#{userListBean.availableRealms}" /> <f:selectItems value="#{userListBean.availableRealms}" />
<p:ajax event="change" <p:ajax event="change"
update=":formUserList:userTable" update=":formUserList:userTable"
listener="#{userListBean.search}" /> listener="#{userListBean.search}" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
</div> </div>
<div class="col-12 md:col-6 lg:col-3"> <div class="col-12 md:col-6 lg:col-3">
<div class="field"> <fr:fieldSelect id="statutFilter"
<label for="statutFilter" class="block text-900 font-medium mb-2"> label="Statut"
<i class="pi pi-filter text-500 mr-1"></i> value="#{userListBean.selectedStatut}">
Statut
</label>
<p:selectOneMenu id="statutFilter"
value="#{userListBean.selectedStatut}"
styleClass="w-full">
<f:selectItem itemLabel="Tous" itemValue="#{null}" /> <f:selectItem itemLabel="Tous" itemValue="#{null}" />
<f:selectItems value="#{userListBean.statutOptions}" /> <f:selectItems value="#{userListBean.statutOptions}" />
<p:ajax update=":formUserList:userTable" <p:ajax event="change"
update=":formUserList:userTable"
listener="#{userListBean.search}" /> listener="#{userListBean.search}" />
</p:selectOneMenu> </fr:fieldSelect>
</div>
</div> </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 <fr:commandButton
value="Réinitialiser" value="Réinitialiser"
icon="pi pi-refresh" icon="pi pi-refresh"
severity="secondary" severity="secondary"
outlined="true"
styleClass="w-full" styleClass="w-full"
action="#{userListBean.resetSearch}" action="#{userListBean.resetSearch}"
update=":formUserList:userTable @form" /> update=":formUserList:userTable @form"
</div> rendered="#{userListBean.searchText != null or userListBean.selectedStatut != null}" />
</div> </div>
</div> </div>
</fr:panel>
</div> </div>
<!-- ================================================================ <!-- ================================================================
@@ -244,16 +250,16 @@
var="user" var="user"
rowKey="#{user.id}" rowKey="#{user.id}"
paginator="true" paginator="true"
rows="#{userListBean.pageSize != null ? userListBean.pageSize : 10}" rows="#{userListBean.pageSize != null ? userListBean.pageSize : 25}"
rowsPerPageTemplate="10,20,50" rowsPerPageTemplate="10,25,50,100"
emptyMessage="Aucun utilisateur trouvé" emptyMessage="Aucun utilisateur trouvé"
reflow="true" responsiveLayout="scroll"
styleClass="p-datatable-striped"> styleClass="p-datatable-striped">
<p:ajax event="page" listener="#{userListBean.onPageChange}" update=":formUserList:userTable :formUserList:formMessages" /> <p:ajax event="page" listener="#{userListBean.onPageChange}" update=":formUserList:userTable :formUserList:formMessages" />
<!-- Colonne Avatar + Username --> <!-- 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="flex align-items-center gap-3">
<div class="border-circle bg-primary text-white flex align-items-center justify-content-center" <div class="border-circle bg-primary text-white flex align-items-center justify-content-center"
style="width: 42px; height: 42px; flex-shrink: 0;"> style="width: 42px; height: 42px; flex-shrink: 0;">
@@ -269,7 +275,7 @@
</p:column> </p:column>
<!-- Colonne Email --> <!-- 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"> <div class="flex align-items-center gap-2">
<i class="pi pi-envelope text-500"></i> <i class="pi pi-envelope text-500"></i>
<span class="text-900">#{user.email}</span> <span class="text-900">#{user.email}</span>
@@ -280,13 +286,13 @@
</p:column> </p:column>
<!-- Colonne Statut --> <!-- 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'}" <fr:tag value="#{user.enabled ? 'ACTIF' : 'INACTIF'}"
severity="#{user.enabled ? 'success' : 'danger'}" /> severity="#{user.enabled ? 'success' : 'danger'}" />
</p:column> </p:column>
<!-- Colonne Rôles --> <!-- 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"> <div class="flex flex-wrap gap-1">
<h:outputText value="Aucun rôle" styleClass="text-500 text-sm" <h:outputText value="Aucun rôle" styleClass="text-500 text-sm"
rendered="#{user.realmRoles == null or user.realmRoles.size() == 0}" /> rendered="#{user.realmRoles == null or user.realmRoles.size() == 0}" />
@@ -304,39 +310,41 @@
</p:column> </p:column>
<!-- Colonne Actions --> <!-- Colonne Actions -->
<p:column headerText="Actions" style="width: 250px; text-align: center"> <p:column headerText="Actions" style="width: 120px; text-align: center" priority="4">
<div class="flex gap-1 justify-content-center flex-wrap"> <div class="flex gap-1 justify-content-center">
<!-- Bouton Voir Profil --> <!-- Bouton Voir (Action principale) -->
<p:button icon="pi pi-eye" <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" title="Voir le profil"
outcome="/pages/user-manager/users/view"> outcome="/pages/user-manager/users/view">
<f:param name="userId" value="#{user.id}" /> <f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" /> <f:param name="realm" value="#{userListBean.realmName}" />
</p:button> </p:button>
<!-- Bouton Modifier --> <!-- Menu Actions Secondaires -->
<p:button icon="pi pi-pencil" <p:splitButton icon="pi pi-ellipsis-v"
styleClass="p-button-rounded p-button-text p-button-sm" styleClass="p-button-rounded p-button-sm p-button-secondary"
title="Modifier" menuStyleClass="text-left">
<p:menuitem value="Modifier"
icon="pi pi-pencil"
outcome="/pages/user-manager/users/edit"> outcome="/pages/user-manager/users/edit">
<f:param name="userId" value="#{user.id}" /> <f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" /> <f:param name="realm" value="#{userListBean.realmName}" />
</p:button> </p:menuitem>
<!-- Bouton Gérer les Rôles --> <p:menuitem value="Gérer les Rôles"
<p:button icon="pi pi-key" icon="pi pi-key"
styleClass="p-button-rounded p-button-text p-button-sm p-button-help" outcome="/pages/user-manager/users/edit">
title="Gérer les rôles"
outcome="/pages/user-manager/roles/assign">
<f:param name="userId" value="#{user.id}" /> <f:param name="userId" value="#{user.id}" />
<f:param name="realm" value="#{userListBean.realmName}" /> <f:param name="realm" value="#{userListBean.realmName}" />
</p:button> </p:menuitem>
<!-- Bouton Désactiver (si actif) --> <p:divider />
<p:commandButton icon="pi pi-ban"
styleClass="p-button-rounded p-button-text p-button-sm p-button-warning" <!-- Désactiver (si utilisateur actif) -->
title="Désactiver" <p:menuitem value="Désactiver"
icon="pi pi-ban"
styleClass="text-orange-500"
action="#{userListBean.deactivateUserAction}" action="#{userListBean.deactivateUserAction}"
update=":formUserList:userTable :formUserList:formMessages" update=":formUserList:userTable :formUserList:formMessages"
process="@this" process="@this"
@@ -345,124 +353,213 @@
<p:confirm header="Désactiver l'utilisateur" <p:confirm header="Désactiver l'utilisateur"
message="Voulez-vous vraiment désactiver l'utilisateur #{user.username} ?" message="Voulez-vous vraiment désactiver l'utilisateur #{user.username} ?"
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </p:menuitem>
<!-- Bouton Activer (si inactif) --> <!-- Activer (si utilisateur inactif) -->
<fr:commandButton icon="pi pi-check" <p:menuitem value="Activer"
rounded="true" icon="pi pi-check"
text="true" styleClass="text-green-500"
size="small"
severity="success"
title="Activer"
action="#{userListBean.activateUserAction}" action="#{userListBean.activateUserAction}"
update=":formUserList:userTable :formUserList:formMessages" update=":formUserList:userTable :formUserList:formMessages"
process="@this" process="@this"
rendered="#{not user.enabled}"> rendered="#{!user.enabled}">
<f:attribute name="userId" value="#{user.id}" /> <f:attribute name="userId" value="#{user.id}" />
</fr:commandButton> <p:confirm header="Activer l'utilisateur"
message="Voulez-vous vraiment activer l'utilisateur #{user.username} ?"
icon="pi pi-exclamation-triangle" />
</p:menuitem>
<!-- Bouton Supprimer --> <p:menuitem value="Supprimer"
<p:commandButton icon="pi pi-trash" icon="pi pi-trash"
styleClass="p-button-rounded p-button-text p-button-sm p-button-danger" styleClass="text-red-500"
title="Supprimer"
action="#{userListBean.deleteUserAction}" action="#{userListBean.deleteUserAction}"
update=":formUserList:userTable :formUserList:formMessages" update=":formUserList:userTable :formUserList:formMessages"
process="@this"> process="@this">
<f:attribute name="userId" value="#{user.id}" /> <f:attribute name="userId" value="#{user.id}" />
<p:confirm header="Supprimer l'utilisateur" <p:confirm header="Supprimer l'utilisateur"
message="Voulez-vous vraiment supprimer définitivement l'utilisateur #{user.username} ? Cette action est irréversible." message="Voulez-vous vraiment supprimer définitivement l'utilisateur #{user.username} ? Cette action est IRRÉVERSIBLE."
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </p:menuitem>
</p:splitButton>
</div> </div>
</p:column> </p:column>
</p:dataTable> </p:dataTable>
</div> </div>
</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> </div>
</h:form> </h:form>
<!-- ================================================================ <!-- ================================================================
DIALOG D'IMPORT DIALOG D'IMPORT CSV (Freya formDialog)
================================================================ --> ================================================================ -->
<p:dialog id="importUsersDialog" <p:dialog widgetVar="importUsersDialog"
widgetVar="importUsersDialog" header="Importer des Utilisateurs depuis CSV"
header="Importer des Utilisateurs"
modal="true" modal="true"
resizable="false" responsive="true"
styleClass="w-full md:w-30rem"> width="600"
<h:form id="formImportUsers"> showEffect="fade"
<div class="flex flex-column gap-3"> hideEffect="fade"
<p class="text-600"> closeOnEscape="true">
Importez des utilisateurs depuis un fichier CSV ou JSON.
</p> <h:form id="formImportDialog">
<p:fileUpload mode="simple" <!-- 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" skinSimple="true"
accept=".csv,.json" label="Choisir un fichier CSV"
label="Sélectionner un fichier" /> chooseIcon="pi pi-folder-open"
<div class="flex justify-content-end gap-2 mt-3"> accept=".csv"
<fr:commandButton value="Annuler" 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" icon="pi pi-times"
severity="secondary" severity="secondary"
onclick="PF('importUsersDialog').hide()" onclick="PF('importResultDialog').hide()"
type="button" /> type="button" />
<fr:commandButton value="Importer"
icon="pi pi-upload"
severity="success"
action="#{userListBean.importUsers}"
update=":formUserList"
oncomplete="PF('importUsersDialog').hide()" />
</div>
</div> </div>
</h:panelGroup>
</h:form> </h:form>
</p:dialog> </p:dialog>

View File

@@ -333,26 +333,29 @@
<span>Gestion du Profil</span> <span>Gestion du Profil</span>
</h4> </h4>
<div class="flex flex-column gap-2"> <div class="flex flex-column gap-2">
<p:commandButton value="Modifier mon profil" <fr:commandButton value="Modifier mon profil"
icon="pi pi-pencil" icon="pi pi-pencil"
styleClass="p-button-outlined w-full justify-content-start" outlined="true"
styleClass="w-full justify-content-start"
disabled="true"> disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité gérée par Keycloak"/> <f:attribute name="data-tooltip" value="Fonctionnalité gérée par Keycloak"/>
</p:commandButton> </fr:commandButton>
<p:commandButton value="Changer mon mot de passe" <fr:commandButton value="Changer mon mot de passe"
icon="pi pi-key" icon="pi pi-key"
styleClass="p-button-outlined w-full justify-content-start" outlined="true"
styleClass="w-full justify-content-start"
disabled="true"> disabled="true">
<f:attribute name="data-tooltip" value="Utilisez le portail Keycloak"/> <f:attribute name="data-tooltip" value="Utilisez le portail Keycloak"/>
</p:commandButton> </fr:commandButton>
<p:commandButton value="Paramètres de sécurité" <fr:commandButton value="Paramètres de sécurité"
icon="pi pi-shield" icon="pi pi-shield"
styleClass="p-button-outlined w-full justify-content-start" outlined="true"
styleClass="w-full justify-content-start"
disabled="true"> disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/> <f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton> </fr:commandButton>
</div> </div>
</div> </div>
</div> </div>
@@ -365,28 +368,33 @@
<span>Sessions et Sécurité</span> <span>Sessions et Sécurité</span>
</h4> </h4>
<div class="flex flex-column gap-2"> <div class="flex flex-column gap-2">
<p:commandButton value="Voir mes sessions actives" <fr:commandButton value="Voir mes sessions actives"
icon="pi pi-desktop" icon="pi pi-desktop"
styleClass="p-button-outlined p-button-info w-full justify-content-start" outlined="true"
severity="info"
styleClass="w-full justify-content-start"
disabled="true"> disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/> <f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton> </fr:commandButton>
<p:commandButton value="Historique des connexions" <fr:commandButton value="Historique des connexions"
icon="pi pi-history" icon="pi pi-history"
styleClass="p-button-outlined p-button-secondary w-full justify-content-start" outlined="true"
severity="secondary"
styleClass="w-full justify-content-start"
disabled="true"> disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/> <f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton> </fr:commandButton>
<p:commandButton value="Se déconnecter" <fr:commandButton value="Se déconnecter"
icon="pi pi-sign-out" icon="pi pi-sign-out"
styleClass="p-button-danger w-full justify-content-start" severity="danger"
styleClass="w-full justify-content-start"
action="#{userSessionBean.logout}"> action="#{userSessionBean.logout}">
<p:confirm header="Confirmation de déconnexion" <p:confirm header="Confirmation de déconnexion"
message="Êtes-vous sûr de vouloir vous déconnecter ?" message="Êtes-vous sûr de vouloir vous déconnecter ?"
icon="pi pi-exclamation-triangle" /> icon="pi pi-exclamation-triangle" />
</p:commandButton> </fr:commandButton>
</div> </div>
</div> </div>
</div> </div>
@@ -397,15 +405,18 @@
</div> </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" <p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
responsive="true" width="400"> responsive="true" width="400">
<p:commandButton value="Non" type="button" <fr:commandButton value="Non"
styleClass="p-button-text" type="button"
text="true"
icon="pi pi-times" /> icon="pi pi-times" />
<p:commandButton value="Oui" type="button" <fr:commandButton value="Oui"
styleClass="p-button-danger" type="button"
severity="danger"
icon="pi pi-check" /> icon="pi pi-check" />
</p:confirmDialog> </p:confirmDialog>

View File

@@ -144,7 +144,7 @@
styleClass="text-500 text-sm" styleClass="text-500 text-sm"
rendered="#{userProfilBean.user.realmRoles == null or userProfilBean.user.realmRoles.size() == 0}" /> rendered="#{userProfilBean.user.realmRoles == null or userProfilBean.user.realmRoles.size() == 0}" />
<ui:repeat value="#{userProfilBean.user.realmRoles}" var="role"> <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> </ui:repeat>
</div> </div>
</div> </div>
@@ -152,9 +152,9 @@
<div class="mb-3 pb-3 border-bottom-1 surface-border"> <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> <label class="block text-600 font-medium mb-1 text-sm">Statut du compte</label>
<div class="flex align-items-center"> <div class="flex align-items-center">
<p:tag value="#{userProfilBean.user.enabled ? 'ACTIF' : 'INACTIF'}" <fr:tag value="#{userProfilBean.user.enabled ? 'ACTIF' : 'INACTIF'}"
severity="#{userProfilBean.user.enabled ? 'success' : 'danger'}" severity="#{userProfilBean.user.enabled ? 'success' : 'danger'}"
styleClass="text-sm"></p:tag> styleClass="text-sm" />
</div> </div>
</div> </div>
@@ -209,7 +209,7 @@
icon="pi pi-key" icon="pi pi-key"
severity="help" severity="help"
styleClass="w-full" 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="userId" value="#{userProfilBean.userId}" />
<f:param name="realm" value="#{userProfilBean.realmName}" /> <f:param name="realm" value="#{userProfilBean.realmName}" />
</fr:commandButton> </fr:commandButton>

View File

@@ -37,7 +37,6 @@
<!-- Gestion Rôles --> <!-- Gestion Rôles -->
<p:submenu id="m_roles" label="Gestion Rôles" icon="pi pi-shield"> <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_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> </p:submenu>
<!-- Audit --> <!-- Audit -->
@@ -45,10 +44,11 @@
<p:menuitem id="m_audit_logs" value="Journal d'Audit" icon="pi pi-file-o" outcome="/pages/user-manager/audit/logs" /> <p:menuitem id="m_audit_logs" value="Journal d'Audit" icon="pi pi-file-o" outcome="/pages/user-manager/audit/logs" />
</p:submenu> </p:submenu>
<!-- Synchronisation --> <!-- Synchronisation - DÉSACTIVÉ: page stub non implémentée
<p:submenu id="m_sync" label="Synchronisation" icon="pi pi-sync"> <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:menuitem id="m_sync_dashboard" value="Dashboard" icon="pi pi-dashboard" outcome="/pages/user-manager/sync/dashboard" />
</p:submenu> </p:submenu>
-->
<!-- Administration (visible uniquement pour les admins) --> <!-- Administration (visible uniquement pour les admins) -->
<p:submenu id="m_admin" label="Administration" icon="pi pi-cog" rendered="#{userSessionBean.hasRole('admin')}"> <p:submenu id="m_admin" label="Administration" icon="pi pi-cog" rendered="#{userSessionBean.hasRole('admin')}">

View File

@@ -3,6 +3,7 @@
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
xmlns:fr="http://primefaces.org/freya"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"> xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!-- <!--
@@ -44,6 +45,7 @@
</ui:include> </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="severity" value="#{empty severity ? 'primary' : severity}" />
<c:set var="size" value="#{empty size ? 'normal' : size}" /> <c:set var="size" value="#{empty size ? 'normal' : size}" />
<c:set var="disabled" value="#{empty disabled ? false : disabled}" /> <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="hasAction" value="#{empty hasAction ? false : hasAction}" />
<c:set var="hasOutcome" value="#{empty hasOutcome ? false : hasOutcome}" /> <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:choose>
<c:when test="#{hasAction}"> <c:when test="#{hasAction}">
<p:commandButton <fr:commandButton
value="#{value}" value="#{value}"
icon="#{not empty icon ? icon : ''}" icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}" severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="#{disabled}" disabled="#{disabled}"
action="#{action}" action="#{action}"
update="#{not empty update ? update : '@form'}" update="#{not empty update ? update : '@form'}"
@@ -99,10 +68,12 @@
onclick="#{not empty onclick ? onclick : ''}" /> onclick="#{not empty onclick ? onclick : ''}" />
</c:when> </c:when>
<c:when test="#{hasOutcome}"> <c:when test="#{hasOutcome}">
<p:commandButton <fr:commandButton
value="#{value}" value="#{value}"
icon="#{not empty icon ? icon : ''}" icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}" severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="#{disabled}" disabled="#{disabled}"
outcome="#{outcome}" outcome="#{outcome}"
update="#{not empty update ? update : '@form'}" update="#{not empty update ? update : '@form'}"
@@ -110,19 +81,23 @@
onclick="#{not empty onclick ? onclick : ''}" /> onclick="#{not empty onclick ? onclick : ''}" />
</c:when> </c:when>
<c:when test="#{not empty onclick}"> <c:when test="#{not empty onclick}">
<p:commandButton <fr:commandButton
value="#{value}" value="#{value}"
icon="#{not empty icon ? icon : ''}" icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}" severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="#{disabled}" disabled="#{disabled}"
type="button" type="button"
onclick="#{onclick}" /> onclick="#{onclick}" />
</c:when> </c:when>
<c:otherwise> <c:otherwise>
<p:commandButton <fr:commandButton
value="#{value}" value="#{value}"
icon="#{not empty icon ? icon : ''}" icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}" severity="#{severity}"
size="#{size != 'normal' ? size : null}"
styleClass="#{styleClass}"
disabled="true" disabled="true"
title="Aucune action définie" /> title="Aucune action définie" />
</c:otherwise> </c:otherwise>

View File

@@ -78,8 +78,9 @@ class DashboardBeanTest {
assertEquals(100L, dashboardBean.getTotalUsers()); assertEquals(100L, dashboardBean.getTotalUsers());
assertEquals(1L, dashboardBean.getTotalRoles()); assertEquals(1L, dashboardBean.getTotalRoles());
assertEquals(55L, dashboardBean.getRecentActions()); assertEquals(55L, dashboardBean.getActionsLast24h());
assertEquals("100", dashboardBean.getTotalUsersDisplay()); assertEquals("100", dashboardBean.getTotalUsersDisplay());
assertEquals("55", dashboardBean.getActionsLast24hDisplay());
} }
@Test @Test

View File

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

View File

@@ -176,10 +176,10 @@ public interface UserService {
String exportUsersToCSV(@NotNull UserSearchCriteriaDTO criteria); 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 csvContent contenu CSV
* @param realmName nom du realm * @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);
} }

View File

@@ -22,6 +22,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
@@ -407,6 +408,86 @@ public class UserResource {
} }
} }
/**
* Exporter les utilisateurs en CSV
*/
@GET
@Path("/export/csv")
@Operation(summary = "Exporter les utilisateurs en CSV")
@APIResponses({
@APIResponse(responseCode = "200", description = "Fichier CSV généré avec succès"),
@APIResponse(responseCode = "400", description = "Realm manquant ou invalide"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin", "user_manager", "user_viewer"})
@Produces(MediaType.TEXT_PLAIN)
public Response exportUsersToCSV(@QueryParam("realm") @NotBlank String realmName) {
log.info("GET /api/users/export/csv - realm: {}", realmName);
try {
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.pageSize(10000) // Export complet sans pagination
.page(0)
.build();
String csvContent = userService.exportUsersToCSV(criteria);
String filename = "users_export_" +
LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss")) +
".csv";
return Response.ok(csvContent)
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
.build();
} catch (Exception e) {
log.error("Erreur lors de l'export CSV des utilisateurs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
/**
* Importer des utilisateurs depuis CSV avec rapport détaillé
*/
@POST
@Path("/import/csv")
@Operation(summary = "Importer des utilisateurs depuis un fichier CSV")
@APIResponses({
@APIResponse(responseCode = "200", description = "Import terminé avec rapport détaillé"),
@APIResponse(responseCode = "400", description = "Fichier CSV vide ou invalide"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin", "user_manager"})
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_JSON)
public Response importUsersFromCSV(
@QueryParam("realm") @NotBlank String realmName,
String csvContent) {
log.info("POST /api/users/import/csv - realm: {}", realmName);
try {
if (csvContent == null || csvContent.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Le contenu CSV est vide"))
.build();
}
dev.lions.user.manager.dto.importexport.ImportResultDTO result = userService.importUsersFromCSV(csvContent, realmName);
log.info("{} utilisateur(s) importé(s) dans le realm {} ({} erreur(s))",
result.getSuccessCount(), realmName, result.getErrorCount());
return Response.ok(result).build();
} catch (Exception e) {
log.error("Erreur lors de l'import CSV des utilisateurs", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
// ==================== DTOs internes ==================== // ==================== DTOs internes ====================
@Schema(description = "Requête de réinitialisation de mot de passe") @Schema(description = "Requête de réinitialisation de mot de passe")

View File

@@ -2,8 +2,12 @@ package dev.lions.user.manager.service.impl;
import dev.lions.user.manager.dto.audit.AuditLogDTO; import dev.lions.user.manager.dto.audit.AuditLogDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit; import dev.lions.user.manager.enums.audit.TypeActionAudit;
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
import dev.lions.user.manager.server.impl.mapper.AuditLogMapper;
import dev.lions.user.manager.service.AuditService; import dev.lions.user.manager.service.AuditService;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -19,19 +23,49 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Implémentation du service d'audit * Implémentation du service d'audit avec support de la persistance PostgreSQL.
* *
* NOTES: * <p><b>Architecture Hybride:</b></p>
* - Cette implémentation utilise un stockage en mémoire pour le développement * <ul>
* - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache) * <li><b>Cache en mémoire</b> - Pour les logs récents (performances)</li>
* - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés * <li><b>Persistance PostgreSQL</b> - Pour l'historique long terme (activable via config)</li>
* </ul>
*
* <p><b>Configuration:</b></p>
* <ul>
* <li>{@code lions.audit.enabled} - Active/désactive l'audit (défaut: true)</li>
* <li>{@code lions.audit.log-to-database} - Active la persistance DB (défaut: false en dev, true en prod)</li>
* <li>{@code lions.audit.cache-size} - Taille max du cache mémoire (défaut: 10000)</li>
* <li>{@code lions.audit.retention-days} - Durée de rétention en jours (défaut: 365)</li>
* </ul>
*
* <p><b>Modes de Fonctionnement:</b></p>
* <pre>
* Mode DEV (logToDatabase=false):
* - Stockage en mémoire uniquement
* - Logs perdus au redémarrage
* - Performances maximales
*
* Mode PROD (logToDatabase=true):
* - Persistance PostgreSQL
* - Cache mémoire pour requêtes fréquentes
* - Historique complet préservé
* </pre>
*
* @author Lions Development Team
* @version 2.0.0
* @since 2026-01-02
*/ */
@ApplicationScoped @ApplicationScoped
@Slf4j @Slf4j
public class AuditServiceImpl implements AuditService { public class AuditServiceImpl implements AuditService {
// Stockage en mémoire (à remplacer par une DB en production) // ==================== DÉPENDANCES ====================
private final Map<String, AuditLogDTO> auditLogs = new ConcurrentHashMap<>();
@Inject
AuditLogMapper auditLogMapper;
// ==================== CONFIGURATION ====================
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true") @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
boolean auditEnabled; boolean auditEnabled;
@@ -39,7 +73,24 @@ public class AuditServiceImpl implements AuditService {
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false") @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false")
boolean logToDatabase; boolean logToDatabase;
@ConfigProperty(name = "lions.audit.cache-size", defaultValue = "10000")
int cacheSize;
@ConfigProperty(name = "lions.audit.retention-days", defaultValue = "365")
int retentionDays;
// ==================== STOCKAGE ====================
/**
* Cache en mémoire pour les logs récents.
* <p>Limité à {@code cacheSize} entrées. Les plus anciens sont supprimés automatiquement.</p>
*/
private final Map<String, AuditLogDTO> auditLogsCache = new ConcurrentHashMap<>();
// ==================== MÉTHODES PRINCIPALES ====================
@Override @Override
@Transactional
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) { public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
if (!auditEnabled) { if (!auditEnabled) {
log.debug("Audit désactivé, log ignoré"); log.debug("Audit désactivé, log ignoré");
@@ -56,7 +107,7 @@ public class AuditServiceImpl implements AuditService {
auditLog.setDateAction(LocalDateTime.now()); auditLog.setDateAction(LocalDateTime.now());
} }
// Log structuré pour les systèmes de logging (Graylog, Elasticsearch, etc.) // Log structuré pour les systèmes de logging externes (Graylog, Elasticsearch, etc.)
log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}", log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}",
auditLog.getTypeAction(), auditLog.getTypeAction(),
auditLog.getActeurUsername(), auditLog.getActeurUsername(),
@@ -65,15 +116,30 @@ public class AuditServiceImpl implements AuditService {
auditLog.getIpAddress(), auditLog.getIpAddress(),
auditLog.getDescription()); auditLog.getDescription());
// Stocker en mémoire // Stocker en base de données si activé
auditLogs.put(auditLog.getId(), auditLog); if (logToDatabase) {
try {
AuditLogEntity entity = auditLogMapper.toEntity(auditLog);
// Le mapper s'occupe du mapping automatique via @Mapping annotations
// Ajout des champs additionnels non mappés automatiquement
entity.setRealmName(auditLog.getRealmName());
// TODO: Si logToDatabase = true, persister dans PostgreSQL via Panache entity.persist();
// Exemple:
// if (logToDatabase) { log.debug("Log d'audit persisté en base de données avec ID: {}", entity.id);
// AuditLogEntity entity = AuditLogMapper.toEntity(auditLog); } catch (Exception e) {
// entity.persist(); log.error("Erreur lors de la persistance du log d'audit en base de données", e);
// } // On ne lance pas d'exception pour ne pas bloquer le processus métier
}
}
// Ajouter au cache mémoire (pour performances)
auditLogsCache.put(auditLog.getId(), auditLog);
// Nettoyer le cache si trop grand
if (auditLogsCache.size() > cacheSize) {
cleanOldestCacheEntries();
}
return auditLog; return auditLog;
} }
@@ -88,7 +154,7 @@ public class AuditServiceImpl implements AuditService {
String description) { String description) {
AuditLogDTO auditLog = AuditLogDTO.builder() AuditLogDTO auditLog = AuditLogDTO.builder()
.acteurUserId(acteurUserId) .acteurUserId(acteurUserId)
.acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant .acteurUsername(acteurUserId)
.typeAction(typeAction) .typeAction(typeAction)
.ressourceType(ressourceType) .ressourceType(ressourceType)
.ressourceId(ressourceId != null ? ressourceId : "") .ressourceId(ressourceId != null ? ressourceId : "")
@@ -111,7 +177,7 @@ public class AuditServiceImpl implements AuditService {
String errorMessage) { String errorMessage) {
AuditLogDTO auditLog = AuditLogDTO.builder() AuditLogDTO auditLog = AuditLogDTO.builder()
.acteurUserId(acteurUserId) .acteurUserId(acteurUserId)
.acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant .acteurUsername(acteurUserId)
.typeAction(typeAction) .typeAction(typeAction)
.ressourceType(ressourceType) .ressourceType(ressourceType)
.ressourceId(ressourceId != null ? ressourceId : "") .ressourceId(ressourceId != null ? ressourceId : "")
@@ -123,13 +189,18 @@ public class AuditServiceImpl implements AuditService {
logAction(auditLog); logAction(auditLog);
} }
// ==================== MÉTHODES DE RECHERCHE ====================
@Override @Override
public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId, public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId,
LocalDateTime dateDebut, LocalDateTime dateDebut,
LocalDateTime dateFin, LocalDateTime dateFin,
int page, int page,
int pageSize) { int pageSize) {
return searchLogs(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize); if (logToDatabase) {
return searchLogsFromDatabase(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
}
return searchLogsFromCache(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
} }
@Override @Override
@@ -139,7 +210,13 @@ public class AuditServiceImpl implements AuditService {
LocalDateTime dateFin, LocalDateTime dateFin,
int page, int page,
int pageSize) { int pageSize) {
return searchLogs(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize) if (logToDatabase) {
return searchLogsFromDatabase(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize)
.stream()
.filter(log -> ressourceId.equals(log.getRessourceId()))
.collect(Collectors.toList());
}
return searchLogsFromCache(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize)
.stream() .stream()
.filter(log -> ressourceId.equals(log.getRessourceId())) .filter(log -> ressourceId.equals(log.getRessourceId()))
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -152,7 +229,10 @@ public class AuditServiceImpl implements AuditService {
LocalDateTime dateFin, LocalDateTime dateFin,
int page, int page,
int pageSize) { int pageSize) {
return searchLogs(null, dateDebut, dateFin, typeAction, null, null, page, pageSize); if (logToDatabase) {
return searchLogsFromDatabase(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
}
return searchLogsFromCache(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
} }
@Override @Override
@@ -161,8 +241,11 @@ public class AuditServiceImpl implements AuditService {
LocalDateTime dateFin, LocalDateTime dateFin,
int page, int page,
int pageSize) { int pageSize) {
// Pour l'instant, on retourne tous les logs car on n'a pas de champ realmName dans AuditLogDTO if (logToDatabase) {
return searchLogs(null, dateDebut, dateFin, null, null, null, page, pageSize); List<AuditLogEntity> entities = AuditLogEntity.findByRealm(realmName);
return auditLogMapper.toDTOList(entities);
}
return searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);
} }
@Override @Override
@@ -171,7 +254,10 @@ public class AuditServiceImpl implements AuditService {
LocalDateTime dateFin, LocalDateTime dateFin,
int page, int page,
int pageSize) { int pageSize) {
return searchLogs(null, dateDebut, dateFin, null, null, false, page, pageSize); if (logToDatabase) {
return searchLogsFromDatabase(null, dateDebut, dateFin, null, null, false, page, pageSize);
}
return searchLogsFromCache(null, dateDebut, dateFin, null, null, false, page, pageSize);
} }
@Override @Override
@@ -180,29 +266,22 @@ public class AuditServiceImpl implements AuditService {
LocalDateTime dateFin, LocalDateTime dateFin,
int page, int page,
int pageSize) { int pageSize) {
// Les actions critiques sont USER_DELETE, ROLE_DELETE, etc. List<AuditLogDTO> allLogs = logToDatabase ?
return auditLogs.values().stream() searchLogsFromDatabase(null, dateDebut, dateFin, null, null, null, page, pageSize) :
searchLogsFromCache(null, dateDebut, dateFin, null, null, null, page, pageSize);
return allLogs.stream()
.filter(log -> { .filter(log -> {
TypeActionAudit type = log.getTypeAction(); TypeActionAudit type = log.getTypeAction();
return type == TypeActionAudit.USER_DELETE || return type == TypeActionAudit.USER_DELETE ||
type == TypeActionAudit.ROLE_DELETE || type == TypeActionAudit.ROLE_DELETE ||
type == TypeActionAudit.SESSION_REVOKE_ALL; type == TypeActionAudit.SESSION_REVOKE_ALL;
}) })
.filter(log -> {
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
return false;
}
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
return false;
}
return true;
})
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
.skip((long) page * pageSize)
.limit(pageSize)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
// ==================== MÉTHODES STATISTIQUES ====================
@Override @Override
public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName, public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName,
LocalDateTime dateDebut, LocalDateTime dateDebut,
@@ -230,30 +309,15 @@ public class AuditServiceImpl implements AuditService {
return result; return result;
} }
@Override
public String exportToCSV(@NotBlank String realmName,
LocalDateTime dateDebut,
LocalDateTime dateFin) {
List<String> csvLines = exportLogsToCSV(dateDebut, dateFin);
return String.join("\n", csvLines);
}
@Override
public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
long beforeCount = auditLogs.size();
auditLogs.entrySet().removeIf(entry ->
entry.getValue().getDateAction().isBefore(dateLimite)
);
long afterCount = auditLogs.size();
return beforeCount - afterCount;
}
@Override @Override
public Map<String, Object> getAuditStatistics(@NotBlank String realmName, public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
LocalDateTime dateDebut, LocalDateTime dateDebut,
LocalDateTime dateFin) { LocalDateTime dateFin) {
Map<String, Object> stats = new java.util.HashMap<>(); Map<String, Object> stats = new java.util.HashMap<>();
stats.put("total", auditLogs.values().stream()
long total = logToDatabase ?
AuditLogEntity.findByPeriod(dateDebut, dateFin).size() :
auditLogsCache.values().stream()
.filter(log -> { .filter(log -> {
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
return false; return false;
@@ -263,135 +327,213 @@ public class AuditServiceImpl implements AuditService {
} }
return true; return true;
}) })
.count());
stats.put("success", getSuccessCount(dateDebut, dateFin));
stats.put("failure", getFailureCount(dateDebut, dateFin));
stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin));
stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin));
return stats;
}
// Méthode privée helper pour la recherche
private List<AuditLogDTO> searchLogs(String acteurUsername, LocalDateTime dateDebut,
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);
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
.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);
return 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;
})
.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);
return 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;
})
.collect(Collectors.groupingBy(
AuditLogDTO::getActeurUsername,
Collectors.counting()
));
}
private long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
log.debug("Comptage des échecs entre {} et {}", dateDebut, dateFin);
return auditLogs.values().stream()
.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;
}
return true;
})
.count(); .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;
} }
private long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) { // ==================== EXPORT / PURGE ====================
log.debug("Comptage des succès entre {} et {}", dateDebut, dateFin);
return auditLogs.values().stream() @Override
.filter(log -> { public String exportToCSV(@NotBlank String realmName,
if (!log.isSuccessful()) { LocalDateTime dateDebut,
return false; // On ne compte que les succès LocalDateTime dateFin) {
List<String> csvLines = exportLogsToCSV(dateDebut, dateFin);
return String.join("\n", csvLines);
} }
@Override
@Transactional
public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
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 cacheRemoved = beforeCacheCount - auditLogsCache.size();
log.info("Supprimé {} logs du cache mémoire avant {}", cacheRemoved, dateLimite);
return purgedCount + cacheRemoved;
}
// ==================== MÉTHODES PRIVÉES ====================
/**
* 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 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());
}
/**
* 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");
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)) { if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
return false; return false;
} }
if (dateFin != null && log.getDateAction().isAfter(dateFin)) { if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
return false; 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;
return true;
})
.collect(Collectors.groupingBy(AuditLogDTO::getTypeAction, Collectors.counting()));
}
private Map<String, Long> getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
if (logToDatabase) {
List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
return entities.stream()
.collect(Collectors.groupingBy(AuditLogEntity::getAuteurAction, 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;
return true;
})
.collect(Collectors.groupingBy(AuditLogDTO::getActeurUsername, Collectors.counting()));
}
private long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
if (logToDatabase) {
return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream()
.filter(e -> !e.getSuccess())
.count();
}
return auditLogsCache.values().stream()
.filter(log -> !log.isSuccessful())
.filter(log -> {
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) {
if (logToDatabase) {
return AuditLogEntity.findByPeriod(dateDebut, dateFin).stream()
.filter(AuditLogEntity::getSuccess)
.count();
}
return auditLogsCache.values().stream()
.filter(AuditLogDTO::isSuccessful)
.filter(log -> {
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
return true; return true;
}) })
.count(); .count();
@@ -401,23 +543,24 @@ public class AuditServiceImpl implements AuditService {
log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin); log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin);
List<String> csvLines = new ArrayList<>(); List<String> csvLines = new ArrayList<>();
// En-tête CSV
csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur"); csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur");
// Données List<AuditLogDTO> logs;
auditLogs.values().stream() if (logToDatabase) {
List<AuditLogEntity> entities = AuditLogEntity.findByPeriod(dateDebut, dateFin);
logs = auditLogMapper.toDTOList(entities);
} else {
logs = auditLogsCache.values().stream()
.filter(log -> { .filter(log -> {
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) return false;
return false; if (dateFin != null && log.getDateAction().isAfter(dateFin)) return false;
}
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
return false;
}
return true; return true;
}) })
.sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction())) .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction()))
.forEach(log -> { .collect(Collectors.toList());
}
logs.forEach(log -> {
String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"", String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"",
log.getId(), log.getId(),
log.getDateAction(), log.getDateAction(),
@@ -437,20 +580,31 @@ public class AuditServiceImpl implements AuditService {
return csvLines; return csvLines;
} }
// ==================== Méthodes utilitaires ==================== // ==================== MÉTHODES UTILITAIRES ====================
/** /**
* Retourne le nombre total de logs en mémoire * Retourne le nombre total de logs (cache + DB).
*/ */
public long getTotalCount() { public long getTotalCount() {
return auditLogs.size(); if (logToDatabase) {
return AuditLogEntity.count();
}
return auditLogsCache.size();
} }
/** /**
* Vide tous les logs (ATTENTION: à utiliser uniquement en développement) * Vide tous les logs (ATTENTION: à utiliser uniquement en développement).
*/ */
@Transactional
public void clearAll() { public void clearAll() {
log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire"); log.warn("ATTENTION: Suppression de tous les logs d'audit");
auditLogs.clear();
if (logToDatabase) {
AuditLogEntity.deleteAll();
log.warn("Supprimé tous les logs de la base de données");
}
auditLogsCache.clear();
log.warn("Vidé le cache mémoire");
} }
} }

View File

@@ -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();
}
}

View File

@@ -608,11 +608,17 @@ public class UserServiceImpl implements UserService {
} }
@Override @Override
public int importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) { public dev.lions.user.manager.dto.importexport.ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) {
log.info("Import des utilisateurs depuis CSV pour le realm {}", realmName); log.info("Import des utilisateurs depuis CSV pour le realm {}", realmName);
dev.lions.user.manager.dto.importexport.ImportResultDTO result = dev.lions.user.manager.dto.importexport.ImportResultDTO.builder()
.totalLines(0)
.successCount(0)
.errorCount(0)
.errors(new java.util.ArrayList<>())
.build();
String[] lines = csvContent.split("\\r?\\n"); String[] lines = csvContent.split("\\r?\\n");
int count = 0;
int startIndex = 0; int startIndex = 0;
// Skip header if present // Skip header if present
@@ -620,48 +626,159 @@ public class UserServiceImpl implements UserService {
startIndex = 1; startIndex = 1;
} }
result.setTotalLines(lines.length - startIndex);
for (int i = startIndex; i < lines.length; i++) { for (int i = startIndex; i < lines.length; i++) {
int lineNumber = i + 1;
String line = lines[i].trim(); String line = lines[i].trim();
if (line.isEmpty())
continue; if (line.isEmpty()) {
continue; // Ignore empty lines
}
try { try {
// Parse CSV line
String[] parts = parseCSVLine(line); String[] parts = parseCSVLine(line);
if (parts.length < 5) { if (parts.length < 5) {
log.warn("Ligne CSV invalide ignorée (pas assez de colonnes): {}", line); result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.INVALID_FORMAT)
.message("Nombre de colonnes insuffisant (attendu: 5, trouvé: " + parts.length + ")")
.build());
continue; continue;
} }
String username = parts[0]; String username = CsvValidationHelper.clean(parts[0]);
String email = parts[1]; String email = CsvValidationHelper.clean(parts[1]);
String firstName = parts[2]; String firstName = CsvValidationHelper.clean(parts[2]);
String lastName = parts[3]; String lastName = CsvValidationHelper.clean(parts[3]);
boolean enabled = Boolean.parseBoolean(parts[4]); String enabledStr = CsvValidationHelper.clean(parts[4]);
if (username == null || username.isBlank()) { // Validate username
log.warn("Username manquant à la ligne {}", i + 1); String usernameError = CsvValidationHelper.validateUsername(username);
if (usernameError != null) {
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
.field("username")
.message(usernameError)
.build());
continue; continue;
} }
// Validate email
String emailError = CsvValidationHelper.validateEmail(email);
if (emailError != null) {
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
.field("email")
.message(emailError)
.build());
continue;
}
// Validate firstName
String firstNameError = CsvValidationHelper.validateName(firstName, "Prénom");
if (firstNameError != null) {
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
.field("firstName")
.message(firstNameError)
.build());
continue;
}
// Validate lastName
String lastNameError = CsvValidationHelper.validateName(lastName, "Nom");
if (lastNameError != null) {
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
.field("lastName")
.message(lastNameError)
.build());
continue;
}
// Validate enabled
String enabledError = CsvValidationHelper.validateBoolean(enabledStr);
if (enabledError != null) {
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.VALIDATION_ERROR)
.field("enabled")
.message(enabledError)
.build());
continue;
}
boolean enabled = CsvValidationHelper.parseBoolean(enabledStr);
// Check if user already exists
try {
java.util.Optional<UserDTO> existingUser = getUserByUsername(username, realmName);
if (existingUser.isPresent()) {
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.DUPLICATE_USER)
.field("username")
.message("Utilisateur déjà existant: " + username)
.build());
continue;
}
} catch (Exception e) {
// User doesn't exist, continue with creation
}
// Create user
UserDTO userDTO = UserDTO.builder() UserDTO userDTO = UserDTO.builder()
.username(username) .username(username)
.email(email.isBlank() ? null : email) .email(email)
.prenom(firstName.isBlank() ? null : firstName) .prenom(firstName)
.nom(lastName.isBlank() ? null : lastName) .nom(lastName)
.enabled(enabled) .enabled(enabled)
.build(); .build();
try {
createUser(userDTO, realmName); createUser(userDTO, realmName);
count++; result.setSuccessCount(result.getSuccessCount() + 1);
log.debug("✅ Utilisateur créé: {} (ligne {})", username, lineNumber);
} catch (Exception e) {
result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.CREATION_ERROR)
.message("Erreur lors de la création de l'utilisateur")
.details(e.getMessage())
.build());
}
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de l'import de la ligne {}: {}", i + 1, e.getMessage()); log.error("Erreur inattendue lors du traitement de la ligne {}: {}", lineNumber, e.getMessage(), e);
// Continue with next line result.addError(dev.lions.user.manager.dto.importexport.ImportResultDTO.ImportErrorDTO.builder()
.lineNumber(lineNumber)
.lineContent(line)
.errorType(dev.lions.user.manager.dto.importexport.ImportResultDTO.ErrorType.SYSTEM_ERROR)
.message("Erreur système")
.details(e.getMessage())
.build());
} }
} }
log.info("✅ {} utilisateurs importés avec succès", count); // Generate summary message
return count; result.generateMessage();
log.info(result.getMessage());
return result;
} }
private String escape(String data) { private String escape(String data) {

View File

@@ -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
-- =============================================================================

View File

@@ -220,12 +220,15 @@ class UserServiceImplTest {
lenient().doNothing().when(userResource) lenient().doNothing().when(userResource)
.resetPassword(any(org.keycloak.representations.idm.CredentialRepresentation.class)); .resetPassword(any(org.keycloak.representations.idm.CredentialRepresentation.class));
String csvContent = "username,email,firstName,lastName,enabled\n" + String csvContent = "username,prenom,nom,email\n" +
"imported,imp@test.com,Imp,Orter,true"; "imported,Imp,Orter,imp@test.com";
int count = userService.importUsersFromCSV(csvContent, REALM); dev.lions.user.manager.dto.importexport.ImportResultDTO result =
userService.importUsersFromCSV(csvContent, REALM);
assertEquals(1, count); assertNotNull(result);
assertEquals(1, result.getSuccessCount());
assertEquals(0, result.getErrorCount());
verify(usersResource, atLeastOnce()).create(argThat(u -> u.getUsername().equals("imported"))); verify(usersResource, atLeastOnce()).create(argThat(u -> u.getUsername().equals("imported")));
} }
} }

View File

@@ -22,7 +22,7 @@
<quarkus.version>3.15.1</quarkus.version> <quarkus.version>3.15.1</quarkus.version>
<quarkus-primefaces.version>3.15.1</quarkus-primefaces.version> <quarkus-primefaces.version>3.15.1</quarkus-primefaces.version>
<primefaces.version>14.0.5</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> <keycloak.version>26.0.4</keycloak.version>
<lombok.version>1.18.30</lombok.version> <lombok.version>1.18.30</lombok.version>
<mapstruct.version>1.5.5.Final</mapstruct.version> <mapstruct.version>1.5.5.Final</mapstruct.version>