feat: Implémentation complète page Profil utilisateur

- ProfileView.java: Bean avec données OIDC/Keycloak
- Extraction: nom, email, rôles, dernière connexion, expiration
- Interface complète: infos personnelles, rôles, sécurité
- Bouton redirection vers compte Keycloak
- Design moderne avec PrimeFaces + Flex

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
dahoud
2025-11-09 00:06:05 +00:00
parent a5e553cec0
commit e23ed3f451
2 changed files with 411 additions and 6 deletions

View File

@@ -0,0 +1,218 @@
package dev.lions.btpxpress.view;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.jwt.JsonWebToken;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Bean pour gérer la page de profil utilisateur.
*
* @author BTP Xpress Team
* @version 1.0
*/
@Named("profileView")
@RequestScoped
@Getter
@Setter
@Slf4j
public class ProfileView {
@Inject
SecurityIdentity securityIdentity;
@Inject
@IdToken
JsonWebToken idToken;
private String nomComplet;
private String prenom;
private String nom;
private String email;
private String username;
private String telephone;
private String organisation;
private List<String> roles;
private String derniereConnexion;
private String tokenExpiration;
@PostConstruct
public void init() {
try {
log.info("Initialisation du profil utilisateur");
if (securityIdentity != null && securityIdentity.getPrincipal() != null && idToken != null) {
// Nom complet
nomComplet = idToken.getClaim("name");
if (nomComplet == null || nomComplet.trim().isEmpty()) {
nomComplet = idToken.getClaim("preferred_username");
}
if (nomComplet == null || nomComplet.trim().isEmpty()) {
nomComplet = securityIdentity.getPrincipal().getName();
}
// Prénom et nom
prenom = idToken.getClaim("given_name");
nom = idToken.getClaim("family_name");
// Email
email = idToken.getClaim("email");
// Username
username = idToken.getClaim("preferred_username");
if (username == null || username.trim().isEmpty()) {
username = securityIdentity.getPrincipal().getName();
}
// Téléphone
telephone = idToken.getClaim("phone_number");
// Organisation
organisation = idToken.getClaim("organization");
// Rôles
roles = new ArrayList<>();
Set<String> userRoles = securityIdentity.getRoles();
if (userRoles != null) {
for (String role : userRoles) {
// Formatage des rôles pour affichage
String formattedRole = role.replace("_", " ").replace("-", " ");
formattedRole = capitalizeWords(formattedRole);
roles.add(formattedRole);
}
}
// Dernière connexion (auth_time claim)
Long authTime = idToken.getClaim("auth_time");
if (authTime != null) {
LocalDateTime dateTime = LocalDateTime.ofInstant(
Instant.ofEpochSecond(authTime),
ZoneId.systemDefault()
);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
derniereConnexion = dateTime.format(formatter);
} else {
derniereConnexion = "Non disponible";
}
// Expiration du token
Long exp = idToken.getExpirationTime();
if (exp != null) {
LocalDateTime dateTime = LocalDateTime.ofInstant(
Instant.ofEpochSecond(exp),
ZoneId.systemDefault()
);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
tokenExpiration = dateTime.format(formatter);
} else {
tokenExpiration = "Non disponible";
}
log.info("Profil chargé pour: {}", nomComplet);
} else {
log.warn("SecurityIdentity ou IdToken non disponible");
setDefaultValues();
}
} catch (Exception e) {
log.error("Erreur lors de l'initialisation du profil", e);
setDefaultValues();
}
}
/**
* Valeurs par défaut si les données ne sont pas disponibles.
*/
private void setDefaultValues() {
nomComplet = "Utilisateur";
email = "utilisateur@btpxpress.com";
username = "utilisateur";
roles = new ArrayList<>();
roles.add("Utilisateur");
derniereConnexion = "Non disponible";
tokenExpiration = "Non disponible";
}
/**
* Capitalize first letter of each word.
*/
private String capitalizeWords(String str) {
if (str == null || str.isEmpty()) {
return str;
}
String[] words = str.toLowerCase().split(" ");
StringBuilder result = new StringBuilder();
for (String word : words) {
if (!word.isEmpty()) {
result.append(Character.toUpperCase(word.charAt(0)))
.append(word.substring(1))
.append(" ");
}
}
return result.toString().trim();
}
/**
* Redirige vers la page de changement de mot de passe Keycloak.
*/
public void changerMotDePasse() {
try {
FacesContext facesContext = FacesContext.getCurrentInstance();
jakarta.faces.context.ExternalContext externalContext = facesContext.getExternalContext();
// URL de la page account de Keycloak
String keycloakAccountUrl = "https://security.lions.dev/realms/btpxpress/account/";
externalContext.redirect(keycloakAccountUrl);
facesContext.responseComplete();
} catch (Exception e) {
log.error("Erreur lors de la redirection vers Keycloak account", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR,
"Erreur", "Impossible de rediriger vers la page de gestion du compte"));
}
}
/**
* Retourne les initiales de l'utilisateur pour l'avatar.
*/
public String getInitiales() {
if (nomComplet == null || nomComplet.trim().isEmpty()) {
return "U";
}
String[] parts = nomComplet.trim().split("\\s+");
if (parts.length >= 2) {
return String.valueOf(parts[0].charAt(0)).toUpperCase() +
String.valueOf(parts[1].charAt(0)).toUpperCase();
} else if (parts.length == 1) {
return parts[0].substring(0, Math.min(2, parts[0].length())).toUpperCase();
}
return "U";
}
/**
* Retourne le nombre total de rôles.
*/
public int getNombreRoles() {
return roles != null ? roles.size() : 0;
}
}

View File

@@ -1,8 +1,8 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" <ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html" xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core" xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/template.xhtml"> template="/WEB-INF/template.xhtml">
<ui:define name="title">Mon Profil - BTP Xpress</ui:define> <ui:define name="title">Mon Profil - BTP Xpress</ui:define>
@@ -10,14 +10,201 @@
<ui:define name="content"> <ui:define name="content">
<div class="layout-dashboard"> <div class="layout-dashboard">
<div class="grid"> <div class="grid">
<!-- En-tête de profil -->
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<h1>Mon Profil</h1> <div class="flex align-items-center gap-3 mb-4">
<p>Module en cours de développement...</p> <div class="avatar-circle" style="width: 80px; height: 80px; background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-700) 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: bold;">
#{profileView.initiales}
</div>
<div class="flex-1">
<h1 class="text-3xl font-bold mb-2">#{profileView.nomComplet}</h1>
<p class="text-xl text-600 mb-0">
<i class="pi pi-briefcase mr-2"></i>
<ui:repeat value="#{profileView.roles}" var="role" varStatus="status">
#{role}<h:outputText value=", " rendered="#{!status.last}"/>
</ui:repeat>
</p>
</div>
</div>
</div>
</div>
<!-- Informations personnelles -->
<div class="col-12 md:col-6">
<div class="card">
<div class="flex align-items-center justify-content-between mb-4">
<h2 class="text-2xl font-bold mb-0">
<i class="pi pi-user mr-2"></i>
Informations Personnelles
</h2>
</div>
<p:divider/>
<div class="grid">
<div class="col-12 mb-3">
<label class="block text-600 font-semibold mb-2">Nom d'utilisateur</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-at mr-2 text-primary"></i>
<span class="font-medium">#{profileView.username}</span>
</div>
</div>
<div class="col-12 mb-3">
<label class="block text-600 font-semibold mb-2">Email</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-envelope mr-2 text-primary"></i>
<span class="font-medium">#{profileView.email}</span>
</div>
</div>
<div class="col-12 mb-3" rendered="#{profileView.prenom != null}">
<label class="block text-600 font-semibold mb-2">Prénom</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-user mr-2 text-primary"></i>
<span class="font-medium">#{profileView.prenom}</span>
</div>
</div>
<div class="col-12 mb-3" rendered="#{profileView.nom != null}">
<label class="block text-600 font-semibold mb-2">Nom</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-user mr-2 text-primary"></i>
<span class="font-medium">#{profileView.nom}</span>
</div>
</div>
<div class="col-12 mb-3" rendered="#{profileView.telephone != null}">
<label class="block text-600 font-semibold mb-2">Téléphone</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-phone mr-2 text-primary"></i>
<span class="font-medium">#{profileView.telephone}</span>
</div>
</div>
<div class="col-12 mb-3" rendered="#{profileView.organisation != null}">
<label class="block text-600 font-semibold mb-2">Organisation</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-building mr-2 text-primary"></i>
<span class="font-medium">#{profileView.organisation}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Sécurité et rôles -->
<div class="col-12 md:col-6">
<!-- Rôles et permissions -->
<div class="card mb-4">
<h2 class="text-2xl font-bold mb-4">
<i class="pi pi-shield mr-2"></i>
Rôles et Permissions
</h2>
<p:divider/>
<div class="mb-3">
<div class="flex align-items-center justify-content-between mb-2">
<span class="text-600 font-semibold">Rôles attribués</span>
<p:badge value="#{profileView.nombreRoles}" severity="info"/>
</div>
<div class="flex flex-wrap gap-2 mt-3">
<ui:repeat value="#{profileView.roles}" var="role">
<p:chip label="#{role}" icon="pi pi-tag" styleClass="bg-primary"/>
</ui:repeat>
</div>
</div>
</div>
<!-- Informations de sécurité -->
<div class="card">
<h2 class="text-2xl font-bold mb-4">
<i class="pi pi-lock mr-2"></i>
Sécurité
</h2>
<p:divider/>
<div class="grid">
<div class="col-12 mb-3">
<label class="block text-600 font-semibold mb-2">Dernière connexion</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-clock mr-2 text-primary"></i>
<span class="font-medium">#{profileView.derniereConnexion}</span>
</div>
</div>
<div class="col-12 mb-3">
<label class="block text-600 font-semibold mb-2">Expiration de la session</label>
<div class="p-3 surface-100 border-round">
<i class="pi pi-calendar mr-2 text-primary"></i>
<span class="font-medium">#{profileView.tokenExpiration}</span>
</div>
</div>
<div class="col-12">
<h:form>
<p:commandButton value="Gérer mon compte Keycloak"
icon="pi pi-cog"
action="#{profileView.changerMotDePasse()}"
ajax="false"
styleClass="w-full"
severity="secondary"/>
</h:form>
</div>
</div>
</div>
</div>
<!-- Statistiques d'activité (optionnel - peut être enrichi plus tard) -->
<div class="col-12">
<div class="card">
<h2 class="text-2xl font-bold mb-4">
<i class="pi pi-chart-bar mr-2"></i>
Activité Récente
</h2>
<p:divider/>
<div class="grid">
<div class="col-12 md:col-4">
<div class="p-4 border-round border-1 surface-border text-center">
<div class="text-4xl font-bold text-primary mb-2">
<i class="pi pi-folder"></i>
</div>
<div class="text-600 mb-1">Projets actifs</div>
<div class="text-2xl font-bold">-</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="p-4 border-round border-1 surface-border text-center">
<div class="text-4xl font-bold text-green-500 mb-2">
<i class="pi pi-check-circle"></i>
</div>
<div class="text-600 mb-1">Tâches complétées</div>
<div class="text-2xl font-bold">-</div>
</div>
</div>
<div class="col-12 md:col-4">
<div class="p-4 border-round border-1 surface-border text-center">
<div class="text-4xl font-bold text-orange-500 mb-2">
<i class="pi pi-bell"></i>
</div>
<div class="text-600 mb-1">Notifications</div>
<div class="text-2xl font-bold">-</div>
</div>
</div>
</div>
<p:messages id="messages" showDetail="true" closable="true"/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</ui:define> </ui:define>
</ui:composition> </ui:composition>