feat(lum-client): logos Lions + refonte index + enrichissement user view/profil

- Logos Lions (3 variantes : branded, dark, standard) dans freya-layout/images
- index.html + index.xhtml : refonte
- pages/user-manager/users/view.xhtml : enrichissement affichage détails user (+427 lignes)
- UserProfilBean, AuditConsultationBean, UserSessionBean : ajustements
- templates/menu.xhtml + topbar.xhtml : ajustements layout
- pom.xml : mises à jour mineures
- .gitignore ajouté
- UserCreationBeanTest : ajustements

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
dahoud
2026-04-21 12:46:56 +00:00
parent d19ee7cd25
commit 8b8903252a
14 changed files with 1000 additions and 1011 deletions

111
.gitignore vendored Normal file
View File

@@ -0,0 +1,111 @@
# ============================================
# Quarkus JSF Frontend .gitignore
# ============================================
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Quarkus
.quarkus/
quarkus.log
# IDE
.idea/
*.iml
*.ipr
*.iws
.vscode/
.classpath
.project
.settings/
.factorypath
.apt_generated/
.apt_generated_tests/
# Eclipse
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.loadpath
.recommenders
# IntelliJ
out/
.idea_modules/
# Logs
*.log
*.log.*
logs/
# OS
.DS_Store
Thumbs.db
*.pid
# Java
*.class
*.jar
!.mvn/wrapper/maven-wrapper.jar
*.war
*.ear
hs_err_pid*
# JSF/Faces specific
**/META-INF/resources/.faces-config.xml.jsfdia
**/javax.faces.resource/
# PrimeFaces cache
**/primefaces_resource_cache/
# Node modules (if using npm/webpack)
node_modules/
npm-debug.log
yarn-error.log
package-lock.json
yarn.lock
# Static resources compiled
src/main/resources/META-INF/resources/dist/
src/main/resources/META-INF/resources/assets/vendor/
# Application secrets
*.jks
*.p12
*.pem
*.key
*-secret.properties
application-local.properties
application-dev-override.properties
# Docker
.dockerignore
docker-compose.override.yml
# Database
*.db
*.sqlite
*.h2.db
# Test
test-output/
.gradle/
build/
# Temporary
.tmp/
temp/

View File

@@ -149,6 +149,9 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>21</release>
</configuration>
</plugin> </plugin>
<plugin> <plugin>

View File

@@ -18,10 +18,8 @@ import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -47,8 +45,8 @@ public class AuditConsultationBean implements Serializable {
private AuditLogDTO selectedLog; private AuditLogDTO selectedLog;
private String acteurUsername; private String acteurUsername;
private Date dateDebut; private LocalDateTime dateDebut;
private Date dateFin; private LocalDateTime dateFin;
private TypeActionAudit selectedTypeAction; private TypeActionAudit selectedTypeAction;
private String ressourceType; private String ressourceType;
private Boolean succes; private Boolean succes;
@@ -212,9 +210,23 @@ public class AuditConsultationBean implements Serializable {
} }
} }
private String toIsoString(Date date) { public void loadLogsByActeur(String acteur) {
if (date == null) return null; try {
LocalDateTime ldt = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); List<AuditLogDTO> result = auditServiceClient.getLogsByActor(acteur, 100);
auditLogs = result != null ? result : new ArrayList<>();
totalRecords = auditLogs.size();
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des logs par acteur: " + e.getMessage());
addErrorMessage("Erreur lors du chargement : " + e.getMessage());
}
}
public void loadLogsByRealm(String realmName) {
LOGGER.warning("loadLogsByRealm non supporté côté serveur pour le realm : " + realmName);
}
private String toIsoString(LocalDateTime ldt) {
if (ldt == null) return null;
return ldt.format(DATE_FORMATTER); return ldt.format(DATE_FORMATTER);
} }

View File

@@ -192,6 +192,34 @@ public class UserProfilBean implements Serializable {
} }
} }
/**
* Obtenir les initiales de l'utilisateur pour l'avatar
*/
public String getUserInitials() {
if (user == null) return "?";
String p = (user.getPrenom() != null && !user.getPrenom().isEmpty())
? user.getPrenom().substring(0, 1) : "";
String n = (user.getNom() != null && !user.getNom().isEmpty())
? user.getNom().substring(0, 1) : "";
String initials = (p + n).toUpperCase();
return initials.isEmpty() ? "?" : initials;
}
/**
* Obtenir le nom du rôle principal (libellé lisible)
*/
public String getPrimaryRoleName() {
if (user == null || user.getRealmRoles() == null || user.getRealmRoles().isEmpty()) {
return "Utilisateur";
}
java.util.List<String> roles = user.getRealmRoles();
if (roles.contains("admin")) return "Administrateur";
if (roles.contains("user_manager")) return "Gestionnaire";
if (roles.contains("user_viewer")) return "Consultant";
if (roles.contains("auditor")) return "Auditeur";
return roles.get(0);
}
// Méthodes compatibles avec user-actions.xhtml (qui passe l'ID en paramètre) // Méthodes compatibles avec user-actions.xhtml (qui passe l'ID en paramètre)
public void activateUser(String userId) { public void activateUser(String userId) {

View File

@@ -341,12 +341,9 @@ public class UserSessionBean implements Serializable {
FacesContext facesContext = FacesContext.getCurrentInstance(); FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext(); ExternalContext externalContext = facesContext.getExternalContext();
// Invalider la session HTTP locale // NE PAS invalider la session ici — Quarkus OIDC a besoin des tokens
externalContext.invalidateSession(); // (stockés en session) pour construire l'URL end_session_endpoint de Keycloak
// avec id_token_hint. La session sera invalidée par Quarkus après le logout.
// Rediriger vers l'endpoint de logout OIDC de Quarkus
// Quarkus gère la déconnexion Keycloak (end_session_endpoint) + redirection
// post-logout
String contextPath = externalContext.getRequestContextPath(); String contextPath = externalContext.getRequestContextPath();
externalContext.redirect(contextPath + "/auth/logout"); externalContext.redirect(contextPath + "/auth/logout");
facesContext.responseComplete(); facesContext.responseComplete();
@@ -354,7 +351,6 @@ public class UserSessionBean implements Serializable {
return null; return null;
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("Erreur lors de la déconnexion: " + e.getMessage()); LOGGER.severe("Erreur lors de la déconnexion: " + e.getMessage());
// En cas d'erreur, rediriger vers la page d'accueil
return "/?faces-redirect=true"; return "/?faces-redirect=true";
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,58 +1,191 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:h="http://xmlns.jcp.org/jsf/html"
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"
lang="fr"> template="/templates/main-template.xhtml">
<h:head> <ui:define name="title">Accueil - Lions User Manager</ui:define>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lions User Manager - Gestion des Utilisateurs Keycloak</title>
<!-- PrimeFaces Freya Theme --> <ui:define name="content">
<h:outputStylesheet name="primefaces-freya/theme.css" /> <div class="grid">
<h:outputStylesheet name="css/primeicons.css" library="freya-layout" /> <!-- ================================================================
<h:outputStylesheet name="css/primeflex.min.css" library="freya-layout" /> EN-TÊTE AVEC LOGO ET BIENVENUE
</h:head> ================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex flex-column align-items-center text-center py-4">
<h:graphicImage name="freya-layout/images/lions-logo.png"
style="height: 160px; width: auto;" alt="Lions User Manager" />
<h:body> <h2 class="text-900 font-bold text-3xl mt-4 mb-2">
<div class="flex align-items-center justify-content-center" style="min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);"> Bienvenue, #{userSessionBean.fullName}
<div class="card" style="width: 90%; max-width: 600px; text-align: center;"> </h2>
<div class="flex flex-column align-items-center gap-3 p-5"> <p class="text-600 text-lg m-0 mb-3">
<i class="pi pi-users text-6xl text-primary"></i> Gestion centralisée des utilisateurs Keycloak
<h1 class="text-4xl font-bold m-0">Lions User Manager</h1> </p>
<p class="text-xl text-600 m-0">Gestion centralisée des utilisateurs Keycloak</p>
<div class="flex flex-column gap-2 mt-4" style="width: 100%;"> <div class="flex align-items-center gap-2">
<span class="inline-flex align-items-center gap-2 bg-green-100 text-green-700 px-3 py-2 border-round font-semibold">
<i class="pi pi-circle-fill" style="font-size: 0.5rem;"></i>
<span>Connecté</span>
</span>
<span class="inline-flex align-items-center bg-blue-100 text-blue-700 px-3 py-1 border-round font-semibold text-sm"
style="text-transform: uppercase; letter-spacing: 0.5px;">
#{userSessionBean.primaryRole}
</span>
</div>
</div>
</div>
</div>
<!-- ================================================================
ACCÈS RAPIDE
================================================================ -->
<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-bolt text-orange-500"></i>
Accès Rapide
</h3>
<div class="grid">
<!-- Gestion Utilisateurs -->
<div class="col-12 md:col-6 lg:col-3">
<h:link outcome="/pages/user-manager/users/list" styleClass="no-underline"> <h:link outcome="/pages/user-manager/users/list" styleClass="no-underline">
<p:commandButton value="Accéder à la Gestion des Utilisateurs" <div class="surface-50 border-round p-4 h-full hover:surface-100 transition-all transition-duration-200 cursor-pointer">
icon="pi pi-users" <div class="flex align-items-center justify-content-between mb-3">
styleClass="w-full p-button-lg" /> <div class="flex align-items-center justify-content-center bg-blue-100 border-round"
style="width: 48px; height: 48px;">
<i class="pi pi-users text-blue-600 text-2xl"></i>
</div>
<i class="pi pi-angle-right text-400"></i>
</div>
<h4 class="text-900 font-semibold m-0 mb-1">Utilisateurs</h4>
<p class="text-600 text-sm m-0">Gérer les comptes utilisateurs</p>
</div>
</h:link> </h:link>
</div>
<!-- Gestion Rôles -->
<div class="col-12 md:col-6 lg:col-3">
<h:link outcome="/pages/user-manager/roles/list" styleClass="no-underline"> <h:link outcome="/pages/user-manager/roles/list" styleClass="no-underline">
<p:commandButton value="Gestion des Rôles" <div class="surface-50 border-round p-4 h-full hover:surface-100 transition-all transition-duration-200 cursor-pointer">
icon="pi pi-shield" <div class="flex align-items-center justify-content-between mb-3">
styleClass="w-full p-button-lg p-button-outlined" /> <div class="flex align-items-center justify-content-center bg-green-100 border-round"
style="width: 48px; height: 48px;">
<i class="pi pi-shield text-green-600 text-2xl"></i>
</div>
<i class="pi pi-angle-right text-400"></i>
</div>
<h4 class="text-900 font-semibold m-0 mb-1">Rôles</h4>
<p class="text-600 text-sm m-0">Configurer les rôles et permissions</p>
</div>
</h:link> </h:link>
</div>
<!-- Journal d'Audit -->
<div class="col-12 md:col-6 lg:col-3">
<h:link outcome="/pages/user-manager/audit/logs" styleClass="no-underline"> <h:link outcome="/pages/user-manager/audit/logs" styleClass="no-underline">
<p:commandButton value="Journal d'Audit" <div class="surface-50 border-round p-4 h-full hover:surface-100 transition-all transition-duration-200 cursor-pointer">
icon="pi pi-history" <div class="flex align-items-center justify-content-between mb-3">
styleClass="w-full p-button-lg p-button-outlined" /> <div class="flex align-items-center justify-content-center bg-orange-100 border-round"
style="width: 48px; height: 48px;">
<i class="pi pi-history text-orange-600 text-2xl"></i>
</div>
<i class="pi pi-angle-right text-400"></i>
</div>
<h4 class="text-900 font-semibold m-0 mb-1">Audit</h4>
<p class="text-600 text-sm m-0">Consulter le journal d'activité</p>
</div>
</h:link> </h:link>
</div> </div>
<div class="mt-4 text-600"> <!-- Synchronisation -->
<p class="m-0">Version 1.0.0</p> <div class="col-12 md:col-6 lg:col-3">
<p class="m-0 text-sm">Module réutilisable pour l'écosystème LionsDev</p> <h:link outcome="/pages/user-manager/sync/dashboard" styleClass="no-underline">
<div class="surface-50 border-round p-4 h-full hover:surface-100 transition-all transition-duration-200 cursor-pointer">
<div class="flex align-items-center justify-content-between mb-3">
<div class="flex align-items-center justify-content-center bg-purple-100 border-round"
style="width: 48px; height: 48px;">
<i class="pi pi-sync text-purple-600 text-2xl"></i>
</div>
<i class="pi pi-angle-right text-400"></i>
</div>
<h4 class="text-900 font-semibold m-0 mb-1">Synchronisation</h4>
<p class="text-600 text-sm m-0">Synchroniser avec Keycloak</p>
</div>
</h:link>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</h:body>
</html> <!-- ================================================================
TABLEAU DE BORD RAPIDE
================================================================ -->
<div class="col-12 md:col-6">
<div class="card h-full">
<h3 class="text-900 font-semibold text-lg mb-3 flex align-items-center gap-2">
<i class="pi pi-info-circle text-blue-500"></i>
Informations Système
</h3>
<div class="flex flex-column gap-3">
<div class="flex align-items-center justify-content-between pb-2 border-bottom-1 surface-border">
<span class="text-600">Application</span>
<span class="text-900 font-semibold">Lions User Manager</span>
</div>
<div class="flex align-items-center justify-content-between pb-2 border-bottom-1 surface-border">
<span class="text-600">Version</span>
<span class="text-900 font-semibold">1.0.0</span>
</div>
<div class="flex align-items-center justify-content-between pb-2 border-bottom-1 surface-border">
<span class="text-600">Realm Keycloak</span>
<span class="text-900 font-semibold">lions-user-manager</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Statut</span>
<p:tag value="Opérationnel" severity="success" />
</div>
</div>
</div>
</div>
<div class="col-12 md:col-6">
<div class="card h-full">
<h3 class="text-900 font-semibold text-lg mb-3 flex align-items-center gap-2">
<i class="pi pi-user text-blue-500"></i>
Ma Session
</h3>
<div class="flex flex-column gap-3">
<div class="flex align-items-center justify-content-between pb-2 border-bottom-1 surface-border">
<span class="text-600">Utilisateur</span>
<span class="text-900 font-semibold">#{userSessionBean.username}</span>
</div>
<div class="flex align-items-center justify-content-between pb-2 border-bottom-1 surface-border">
<span class="text-600">Email</span>
<span class="text-900 font-semibold">#{userSessionBean.email}</span>
</div>
<div class="flex align-items-center justify-content-between pb-2 border-bottom-1 surface-border">
<span class="text-600">Rôle principal</span>
<p:tag value="#{userSessionBean.primaryRole}" severity="info" />
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Actions</span>
<div class="flex gap-2">
<h:link outcome="/pages/user-manager/users/profile" styleClass="p-button p-button-sm p-button-text">
<i class="pi pi-user mr-1"></i> Mon Profil
</h:link>
<h:link outcome="/pages/user-manager/dashboard" styleClass="p-button p-button-sm p-button-outlined">
<i class="pi pi-home mr-1"></i> Dashboard
</h:link>
</div>
</div>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,8 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:p="http://primefaces.org/ui" xmlns:fr="http://primefaces.org/freya" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" template="/templates/main-template.xhtml"> xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
template="/templates/main-template.xhtml">
<f:metadata> <f:metadata>
<f:viewParam name="userId" value="#{userProfilBean.userId}" /> <f:viewParam name="userId" value="#{userProfilBean.userId}" />
@@ -20,13 +23,10 @@
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<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"> <h2 class="text-900 font-semibold text-xl m-0">
<i class="pi pi-user text-blue-500" style="font-size: 2rem"></i> <i class="pi pi-user text-blue-500 mr-2"></i>
<div> Profil Utilisateur
<h3 class="m-0 mb-1">Profil de l'Utilisateur</h3> </h2>
<p class="text-600 m-0">Détails et informations de l'utilisateur</p>
</div>
</div>
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text"> <h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text">
<i class="pi pi-arrow-left mr-2"></i> <i class="pi pi-arrow-left mr-2"></i>
Retour à la liste Retour à la liste
@@ -36,28 +36,394 @@
</div> </div>
<!-- ================================================================ <!-- ================================================================
CARTE PROFIL PRINCIPAL & ACTIONS CONTENU PRINCIPAL (si utilisateur trouvé)
================================================================ --> ================================================================ -->
<ui:fragment rendered="#{userProfilBean.user != null}"> <ui:fragment rendered="#{userProfilBean.user != null}">
<!-- ============================================================
CARTE PROFIL PRINCIPAL
============================================================ -->
<div class="col-12"> <div class="col-12">
<div class="card p-0 overflow-hidden"> <div class="card">
<ui:include src="/templates/components/user-management/user-card.xhtml"> <div class="grid">
<ui:param name="user" value="#{userProfilBean.user}" /> <!-- Photo de profil et informations principales -->
<ui:param name="showActions" value="true" /> <div class="col-12 md:col-4">
<ui:param name="layout" value="horizontal" /> <div class="text-center mb-4">
<ui:param name="showOrganisation" value="true" /> <!-- Avatar avec gradient -->
<ui:param name="showRoles" value="true" /> <div style="width: 140px; height: 140px; 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 1.5rem auto; font-size: 3.5rem; font-weight: bold; color: white; box-shadow: 0 8px 24px rgba(0,0,0,0.12);">
<ui:param name="clickable" value="false" /> #{userProfilBean.userInitials}
<ui:param name="actionBean" value="#{userProfilBean}" /> </div>
<ui:param name="showEdit" value="true" />
<ui:param name="editOutcome" value="/pages/user-manager/users/edit" /> <!-- Nom complet -->
<ui:param name="hasDeleteAction" value="true" /> <h3 class="text-900 font-semibold text-2xl mb-2">
<ui:param name="deleteAction" value="#{userProfilBean.deleteUser}" /> #{userProfilBean.user.prenom} #{userProfilBean.user.nom}
</ui:include> </h3>
<!-- Username -->
<p class="text-500 mb-2">@#{userProfilBean.user.username}</p>
<!-- Email -->
<p class="text-600 mb-3 flex align-items-center justify-content-center gap-2">
<i class="pi pi-envelope"></i>
#{userProfilBean.user.email}
<h:panelGroup rendered="#{userProfilBean.user.emailVerified}">
<i class="pi pi-check-circle text-green-500" title="Email vérifié"></i>
</h:panelGroup>
</p>
<!-- Badge de statut -->
<div class="inline-flex align-items-center justify-content-center gap-2 mb-3">
<h:panelGroup rendered="#{userProfilBean.user.enabled}">
<span class="inline-flex align-items-center gap-2 bg-green-100 text-green-700 px-3 py-2 border-round font-semibold" style="font-size: 1rem;">
<i class="pi pi-circle-fill" style="font-size: 0.5rem; animation: pulse 2s ease-in-out infinite;"></i>
<span>Actif</span>
</span>
</h:panelGroup>
<h:panelGroup rendered="#{not userProfilBean.user.enabled}">
<span class="inline-flex align-items-center gap-2 bg-red-100 text-red-700 px-3 py-2 border-round font-semibold" style="font-size: 1rem;">
<i class="pi pi-circle-fill" style="font-size: 0.5rem;"></i>
<span>Inactif</span>
</span>
</h:panelGroup>
</div>
<!-- Badge du rôle principal -->
<div class="flex justify-content-center">
<span class="inline-flex align-items-center bg-blue-100 text-blue-700 px-3 py-1 border-round font-semibold text-sm" style="text-transform: uppercase; letter-spacing: 0.5px;">
#{userProfilBean.primaryRoleName}
</span>
</div>
</div>
</div>
<!-- Informations détaillées -->
<div class="col-12 md:col-8">
<div class="grid">
<!-- Colonne gauche: Informations personnelles -->
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold text-lg mb-3 flex align-items-center gap-2">
<i class="pi pi-user text-blue-500"></i>
Informations Personnelles
</h4>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Nom d'utilisateur</label>
<p class="text-900 font-semibold m-0">#{userProfilBean.user.username}</p>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Prénom</label>
<p class="text-900 font-semibold m-0">#{userProfilBean.user.prenom}</p>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Nom</label>
<p class="text-900 font-semibold m-0">#{userProfilBean.user.nom}</p>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Adresse email</label>
<div class="flex align-items-center gap-2">
<p class="text-900 font-semibold m-0">#{userProfilBean.user.email}</p>
<h:panelGroup rendered="#{userProfilBean.user.emailVerified}">
<i class="pi pi-check-circle text-green-500" title="Email vérifié"></i>
</h:panelGroup>
<h:panelGroup rendered="#{not userProfilBean.user.emailVerified}">
<i class="pi pi-times-circle text-red-400" title="Email non vérifié"></i>
</h:panelGroup>
</div>
</div>
<h:panelGroup rendered="#{not empty userProfilBean.user.telephone}">
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Téléphone</label>
<p class="text-900 font-semibold m-0">#{userProfilBean.user.telephone}</p>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{not empty userProfilBean.user.langue}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Langue</label>
<p class="text-900 font-semibold m-0">#{userProfilBean.user.langue}</p>
</div>
</h:panelGroup>
</div>
<!-- Colonne droite: Rôles et permissions -->
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold text-lg mb-3 flex align-items-center gap-2">
<i class="pi pi-shield text-purple-500"></i>
Rôles et Permissions
</h4>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-2 text-sm">Rôles assignés</label>
<div class="flex flex-wrap gap-2">
<c:choose>
<c:when test="#{not empty userProfilBean.user.realmRoles and not userProfilBean.user.realmRoles.isEmpty()}">
<ui:repeat value="#{userProfilBean.user.realmRoles}" var="role">
<p:badge value="#{role}" severity="info" styleClass="text-sm"></p:badge>
</ui:repeat>
</c:when>
<c:otherwise>
<span class="text-500 font-italic text-sm">Aucun rôle attribué</span>
</c:otherwise>
</c:choose>
</div>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-2 text-sm">Rôle principal</label>
<div class="flex align-items-center">
<p:badge value="#{userProfilBean.primaryRoleName}"
severity="success"
styleClass="text-sm">
</p:badge>
</div>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Niveau d'accès</label>
<p class="text-900 font-semibold m-0">#{userProfilBean.primaryRoleName}</p>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-2 text-sm">Statut du compte</label>
<div class="flex align-items-center">
<h:panelGroup rendered="#{userProfilBean.user.enabled}">
<p:badge value="Actif" severity="success" styleClass="text-sm"></p:badge>
</h:panelGroup>
<h:panelGroup rendered="#{not userProfilBean.user.enabled}">
<p:badge value="Inactif" severity="danger" styleClass="text-sm"></p:badge>
</h:panelGroup>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ============================================================
INFORMATIONS DU COMPTE
============================================================ -->
<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-id-card text-teal-500"></i>
Informations du Compte
</h3>
<div class="grid">
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold mb-3">Contact et Organisation</h4>
<h:panelGroup rendered="#{not empty userProfilBean.user.organisation}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Organisation</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-building text-blue-500"></i>
<p class="text-900 m-0">#{userProfilBean.user.organisation}</p>
</div>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{not empty userProfilBean.user.departement}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Département</label>
<p class="text-900 m-0">#{userProfilBean.user.departement}</p>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{not empty userProfilBean.user.fonction}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Fonction</label>
<p class="text-900 m-0">#{userProfilBean.user.fonction}</p>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{not empty userProfilBean.user.pays}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Pays</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-globe text-purple-500"></i>
<p class="text-900 m-0">#{userProfilBean.user.pays}</p>
</div>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{not empty userProfilBean.user.ville}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Ville</label>
<p class="text-900 m-0">#{userProfilBean.user.ville}</p>
</div>
</h:panelGroup>
</div>
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold mb-3">Activité et Sécurité</h4>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Realm Keycloak</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-server text-blue-500"></i>
<p class="text-700 m-0 text-sm font-mono bg-bluegray-50 p-2 border-round">
#{userProfilBean.realmName}
</p>
</div>
</div>
<h:panelGroup rendered="#{userProfilBean.user.derniereConnexion != null}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Dernière connexion</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-clock text-green-500"></i>
<p class="text-700 m-0 text-sm">
<h:outputText value="#{userProfilBean.user.derniereConnexion}">
<f:convertDateTime pattern="dd/MM/yyyy à HH:mm:ss" timeZone="Europe/Paris" type="localDateTime"/>
</h:outputText>
</p>
</div>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{userProfilBean.user.activeSessions != null}">
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Sessions actives</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-desktop text-purple-500"></i>
<p class="text-900 font-semibold m-0">#{userProfilBean.user.activeSessions}</p>
</div>
</div>
</h:panelGroup>
<h:panelGroup rendered="#{not empty userProfilBean.user.groups and not userProfilBean.user.groups.isEmpty()}">
<div class="mb-3">
<label class="block text-600 font-medium mb-2 text-sm">Groupes</label>
<div class="flex flex-wrap gap-2">
<ui:repeat value="#{userProfilBean.user.groups}" var="group">
<p:badge value="#{group}" severity="warning" styleClass="text-sm"></p:badge>
</ui:repeat>
</div>
</div>
</h:panelGroup>
</div>
</div>
</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="formUserActions">
<div class="grid">
<!-- Gestion du Profil -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 h-full">
<h4 class="text-900 font-semibold mb-3 flex align-items-center gap-2">
<i class="pi pi-pencil text-blue-500"></i>
<span>Gestion du Profil</span>
</h4>
<div class="flex flex-column gap-2">
<h:link outcome="/pages/user-manager/users/edit"
styleClass="p-button p-button-outlined w-full justify-content-start">
<f:param name="userId" value="#{userProfilBean.userId}" />
<f:param name="realm" value="#{userProfilBean.realmName}" />
<i class="pi pi-pencil mr-2"></i>
Modifier l'utilisateur
</h:link>
<p:commandButton value="Réinitialiser le mot de passe"
icon="pi pi-key"
styleClass="p-button-outlined p-button-warning w-full justify-content-start"
action="#{userProfilBean.resetPassword}"
update=":formUserActions"
oncomplete="PF('dlgResetPassword').show()">
</p:commandButton>
<p:commandButton value="Envoyer email de vérification"
icon="pi pi-envelope"
styleClass="p-button-outlined p-button-info w-full justify-content-start"
action="#{userProfilBean.sendVerificationEmail}"
update="@form"
rendered="#{not userProfilBean.user.emailVerified}">
<p:confirm header="Confirmation"
message="Envoyer un email de vérification à #{userProfilBean.user.email} ?"
icon="pi pi-envelope" />
</p:commandButton>
</div>
</div>
</div>
<!-- Gestion du Compte -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 h-full">
<h4 class="text-900 font-semibold mb-3 flex align-items-center gap-2">
<i class="pi pi-shield text-purple-500"></i>
<span>Gestion du Compte</span>
</h4>
<div class="flex flex-column gap-2">
<p:commandButton value="Activer le compte"
icon="pi pi-check-circle"
styleClass="p-button-outlined p-button-success w-full justify-content-start"
action="#{userProfilBean.activateUser}"
update="@form"
rendered="#{not userProfilBean.user.enabled}">
<p:confirm header="Confirmation d'activation"
message="Activer le compte de #{userProfilBean.user.prenom} #{userProfilBean.user.nom} ?"
icon="pi pi-check-circle" />
</p:commandButton>
<p:commandButton value="Désactiver le compte"
icon="pi pi-ban"
styleClass="p-button-outlined p-button-warning w-full justify-content-start"
action="#{userProfilBean.deactivateUser}"
update="@form"
rendered="#{userProfilBean.user.enabled}">
<p:confirm header="Confirmation de désactivation"
message="Désactiver le compte de #{userProfilBean.user.prenom} #{userProfilBean.user.nom} ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
<p:commandButton value="Déconnecter toutes les sessions"
icon="pi pi-sign-out"
styleClass="p-button-outlined p-button-secondary w-full justify-content-start"
action="#{userProfilBean.logoutAllSessions}"
update="@form">
<p:confirm header="Confirmation"
message="Déconnecter toutes les sessions actives de #{userProfilBean.user.prenom} #{userProfilBean.user.nom} ?"
icon="pi pi-sign-out" />
</p:commandButton>
<p:commandButton value="Supprimer l'utilisateur"
icon="pi pi-trash"
styleClass="p-button-danger w-full justify-content-start"
action="#{userProfilBean.deleteUser}"
update="@form">
<p:confirm header="Suppression définitive"
message="Supprimer définitivement #{userProfilBean.user.prenom} #{userProfilBean.user.nom} ? Cette action est irréversible."
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</div>
</div>
</div>
</div>
</h:form>
</div> </div>
</div> </div>
</ui:fragment> </ui:fragment>
<!-- ================================================================
UTILISATEUR NON TROUVÉ
================================================================ -->
<ui:fragment rendered="#{userProfilBean.user == null}"> <ui:fragment rendered="#{userProfilBean.user == null}">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
@@ -80,6 +446,13 @@
<p:autoUpdate /> <p:autoUpdate />
</p:growl> </p:growl>
<!-- Animation CSS pour le badge "Actif" -->
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</ui:define> </ui:define>
</ui:composition> </ui:composition>

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

View File

@@ -13,7 +13,7 @@
<div class="menu-wrapper"> <div class="menu-wrapper">
<div class="sidebar-logo"> <div class="sidebar-logo">
<a href="/pages/user-manager/dashboard"> <a href="/pages/user-manager/dashboard">
<p:graphicImage name="images/logo-freya-single.svg" library="freya-layout" /> <h:graphicImage name="freya-layout/images/lions-logo.png" style="height: 36px; width: auto;" alt="Lions" />
</a> </a>
<a href="#" class="sidebar-pin" title="Toggle Menu"> <a href="#" class="sidebar-pin" title="Toggle Menu">
<span class="pin"></span> <span class="pin"></span>

View File

@@ -19,7 +19,8 @@
<i class="pi pi-bars"/> <i class="pi pi-bars"/>
</a> </a>
<h:link id="logolink" outcome="/pages/user-manager/dashboard" styleClass="layout-topbar-logo"> <h:link id="logolink" outcome="/pages/user-manager/dashboard" styleClass="layout-topbar-logo">
<p:graphicImage name="images/#{guestPreferences.lightLogo ? 'logo-freya-white.svg' : 'logo-freya.svg'}" library="freya-layout" /> <h:graphicImage name="freya-layout/images/#{guestPreferences.lightLogo ? 'lions-logo-dark.png' : 'lions-logo.png'}"
style="height: 40px; width: auto;" alt="Lions User Manager" />
</h:link> </h:link>
</div> </div>

View File

@@ -54,6 +54,7 @@ class UserCreationBeanTest {
@Test @Test
void testInit() { void testInit() {
userCreationBean.setDefaultRealm("master");
userCreationBean.init(); userCreationBean.init();
assertNotNull(userCreationBean.getNewUser()); assertNotNull(userCreationBean.getNewUser());
@@ -90,9 +91,8 @@ class UserCreationBeanTest {
userCreationBean.setPasswordConfirm("password123"); userCreationBean.setPasswordConfirm("password123");
userCreationBean.setRealmName(REALM_NAME); userCreationBean.setRealmName(REALM_NAME);
String result = userCreationBean.createUser(); userCreationBean.createUser();
assertEquals("userListPage", result);
verify(userServiceClient).createUser(any(UserDTO.class), eq(REALM_NAME)); verify(userServiceClient).createUser(any(UserDTO.class), eq(REALM_NAME));
verify(userServiceClient).resetPassword(eq("user-123"), eq(REALM_NAME), verify(userServiceClient).resetPassword(eq("user-123"), eq(REALM_NAME),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class)); any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
@@ -104,9 +104,8 @@ class UserCreationBeanTest {
userCreationBean.setPassword(""); userCreationBean.setPassword("");
userCreationBean.setPasswordConfirm(""); userCreationBean.setPasswordConfirm("");
String result = userCreationBean.createUser(); userCreationBean.createUser();
assertNull(result);
verify(userServiceClient, never()).createUser(any(), anyString()); verify(userServiceClient, never()).createUser(any(), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
} }
@@ -116,9 +115,8 @@ class UserCreationBeanTest {
userCreationBean.setPassword("password1"); userCreationBean.setPassword("password1");
userCreationBean.setPasswordConfirm("password2"); userCreationBean.setPasswordConfirm("password2");
String result = userCreationBean.createUser(); userCreationBean.createUser();
assertNull(result);
verify(userServiceClient, never()).createUser(any(), anyString()); verify(userServiceClient, never()).createUser(any(), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
} }
@@ -128,9 +126,8 @@ class UserCreationBeanTest {
userCreationBean.setPassword("short"); userCreationBean.setPassword("short");
userCreationBean.setPasswordConfirm("short"); userCreationBean.setPasswordConfirm("short");
String result = userCreationBean.createUser(); userCreationBean.createUser();
assertNull(result);
verify(userServiceClient, never()).createUser(any(), anyString()); verify(userServiceClient, never()).createUser(any(), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
} }
@@ -144,9 +141,8 @@ class UserCreationBeanTest {
userCreationBean.setPasswordConfirm("password123"); userCreationBean.setPasswordConfirm("password123");
userCreationBean.setRealmName(REALM_NAME); userCreationBean.setRealmName(REALM_NAME);
String result = userCreationBean.createUser(); userCreationBean.createUser();
assertNull(result);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
} }
@@ -172,9 +168,12 @@ class UserCreationBeanTest {
@Test @Test
void testCancel() { void testCancel() {
String result = userCreationBean.cancel(); jakarta.faces.context.ExternalContext externalContext =
mock(jakarta.faces.context.ExternalContext.class);
when(facesContext.getExternalContext()).thenReturn(externalContext);
userCreationBean.cancel();
assertEquals("userListPage", result);
assertNotNull(userCreationBean.getNewUser()); assertNotNull(userCreationBean.getNewUser());
} }
} }