Compare commits
13 Commits
197816d179
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e23ed3f451 | ||
|
|
a5e553cec0 | ||
|
|
df0243d4f8 | ||
|
|
99bf1be24e | ||
|
|
488b8632f9 | ||
|
|
03f83de218 | ||
|
|
31b1b35a65 | ||
|
|
8ea24f81a7 | ||
|
|
5442c77559 | ||
|
|
a9109242eb | ||
|
|
f7e2f9235e | ||
|
|
3733289b21 | ||
|
|
447bcd22dc |
13
Dockerfile
13
Dockerfile
@@ -14,8 +14,8 @@ RUN mvn dependency:go-offline -B
|
|||||||
# Copier le code source
|
# Copier le code source
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
# Build de l'application
|
# Build de l'application (uber-jar pour compatibilité lionsctl)
|
||||||
RUN mvn clean package -DskipTests -B
|
RUN mvn clean package -DskipTests -B -Dquarkus.package.type=uber-jar
|
||||||
|
|
||||||
## Stage 2 : Runtime image
|
## Stage 2 : Runtime image
|
||||||
FROM eclipse-temurin:17-jre-alpine
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
@@ -29,10 +29,11 @@ RUN apk add --no-cache curl
|
|||||||
RUN addgroup -g 185 -S appuser && adduser -u 185 -S appuser -G appuser
|
RUN addgroup -g 185 -S appuser && adduser -u 185 -S appuser -G appuser
|
||||||
RUN mkdir -p /deployments && chown -R appuser:appuser /deployments
|
RUN mkdir -p /deployments && chown -R appuser:appuser /deployments
|
||||||
|
|
||||||
# Copier le JAR depuis le build
|
# Copier le JAR depuis le build (lionsctl utilise uber-jar)
|
||||||
|
# Note: Le fichier sera btpxpress-client-1.0.0-runner.jar
|
||||||
COPY --from=build --chown=appuser:appuser /build/target/*-runner.jar /deployments/app.jar
|
COPY --from=build --chown=appuser:appuser /build/target/*-runner.jar /deployments/app.jar
|
||||||
|
|
||||||
EXPOSE 8081
|
EXPOSE 8080
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
# Variables d'environnement JVM optimisées
|
# Variables d'environnement JVM optimisées
|
||||||
@@ -40,7 +41,7 @@ ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss
|
|||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
CMD curl -f http://localhost:8081/q/health/ready || exit 1
|
CMD curl -f http://localhost:8080/q/health/ready || exit 1
|
||||||
|
|
||||||
ENTRYPOINT [ "java", "-jar", "/deployments/app.jar" ]
|
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/app.jar"]
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,8 @@ RUN mvn dependency:go-offline -B
|
|||||||
# Copier le code source
|
# Copier le code source
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
# Build de l'application avec profil production
|
# Build de l'application avec profil production (fast-jar par défaut)
|
||||||
RUN mvn clean package -DskipTests -B \
|
RUN mvn clean package -DskipTests -B
|
||||||
-Dquarkus.package.type=uber-jar \
|
|
||||||
-Dquarkus.profile=prod
|
|
||||||
|
|
||||||
## Stage 2 : Image de production optimisée et sécurisée
|
## Stage 2 : Image de production optimisée et sécurisée
|
||||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
|
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
|
||||||
@@ -28,7 +26,7 @@ ENV LANGUAGE='fr_FR:fr'
|
|||||||
# Variables d'environnement de production
|
# Variables d'environnement de production
|
||||||
# Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes
|
# Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes
|
||||||
ENV QUARKUS_PROFILE=prod
|
ENV QUARKUS_PROFILE=prod
|
||||||
ENV QUARKUS_HTTP_PORT=8081
|
ENV QUARKUS_HTTP_PORT=8080
|
||||||
ENV QUARKUS_HTTP_HOST=0.0.0.0
|
ENV QUARKUS_HTTP_HOST=0.0.0.0
|
||||||
|
|
||||||
# Configuration Keycloak/OIDC (production)
|
# Configuration Keycloak/OIDC (production)
|
||||||
@@ -57,11 +55,11 @@ RUN mkdir -p /deployments /app/logs && \
|
|||||||
# Passer à l'utilisateur non-root pour la sécurité
|
# Passer à l'utilisateur non-root pour la sécurité
|
||||||
USER 185
|
USER 185
|
||||||
|
|
||||||
# Copier l'application depuis le builder
|
# Copier l'application depuis le builder (format fast-jar Quarkus)
|
||||||
COPY --from=builder --chown=185 /app/target/*-runner.jar /deployments/app.jar
|
COPY --from=builder --chown=185 /app/target/quarkus-app/ /deployments/
|
||||||
|
|
||||||
# Exposer le port
|
# Exposer le port
|
||||||
EXPOSE 8081
|
EXPOSE 8080
|
||||||
|
|
||||||
# Variables JVM optimisées pour production avec sécurité
|
# Variables JVM optimisées pour production avec sécurité
|
||||||
ENV JAVA_OPTS="-Xmx768m -Xms256m \
|
ENV JAVA_OPTS="-Xmx768m -Xms256m \
|
||||||
@@ -79,8 +77,8 @@ ENV JAVA_OPTS="-Xmx768m -Xms256m \
|
|||||||
|
|
||||||
# Health check avec endpoints Quarkus
|
# Health check avec endpoints Quarkus
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
|
||||||
CMD curl -f http://localhost:8081/q/health/ready || exit 1
|
CMD curl -f http://localhost:8080/q/health/ready || exit 1
|
||||||
|
|
||||||
# Point d'entrée avec profil production
|
# Point d'entrée avec profil production (format fast-jar)
|
||||||
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/app.jar"]
|
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"]
|
||||||
|
|
||||||
|
|||||||
12
pom.xml
12
pom.xml
@@ -18,6 +18,15 @@
|
|||||||
<skipTests>false</skipTests>
|
<skipTests>false</skipTests>
|
||||||
<freya.theme.version>5.0.0-jakarta</freya.theme.version>
|
<freya.theme.version>5.0.0-jakarta</freya.theme.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>lions-maven-repo</id>
|
||||||
|
<name>Lions Dev Maven Repository</name>
|
||||||
|
<url>https://git.lions.dev/lionsdev/btpxpress-maven-repo/raw/branch/main</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -47,8 +56,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.primefaces</groupId>
|
<groupId>org.primefaces</groupId>
|
||||||
<artifactId>freya</artifactId>
|
<artifactId>freya</artifactId>
|
||||||
<version>5.0.0</version>
|
<version>${freya.theme.version}</version>
|
||||||
<type>war</type>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.faces</groupId>
|
<groupId>jakarta.faces</groupId>
|
||||||
|
|||||||
218
src/main/java/dev/lions/btpxpress/view/ProfileView.java
Normal file
218
src/main/java/dev/lions/btpxpress/view/ProfileView.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,64 +1,150 @@
|
|||||||
package dev.lions.btpxpress.view;
|
package dev.lions.btpxpress.view;
|
||||||
|
|
||||||
|
import io.quarkus.oidc.IdToken;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.enterprise.context.SessionScoped;
|
import jakarta.enterprise.context.SessionScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
import jakarta.inject.Named;
|
import jakarta.inject.Named;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bean de session pour gérer les informations de l'utilisateur connecté.
|
* Bean de session pour gérer les informations de l'utilisateur connecté.
|
||||||
*
|
*
|
||||||
* <p>Ce bean stocke les informations de session de l'utilisateur authentifié,
|
* <p>Ce bean stocke les informations de session de l'utilisateur authentifié,
|
||||||
* telles que le nom, l'email, l'avatar, et les statistiques rapides.</p>
|
* telles que le nom, l'email, l'avatar, et les statistiques rapides.</p>
|
||||||
*
|
*
|
||||||
* @author BTP Xpress Team
|
* @author BTP Xpress Team
|
||||||
* @version 1.0
|
* @version 1.0
|
||||||
*/
|
*/
|
||||||
@Named("userSession")
|
@Named("userSession")
|
||||||
@SessionScoped
|
@SessionScoped
|
||||||
@Getter
|
@Slf4j
|
||||||
@Setter
|
|
||||||
public class UserSessionBean implements Serializable {
|
public class UserSessionBean implements Serializable {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private String nomComplet;
|
@Inject
|
||||||
private String email;
|
SecurityIdentity securityIdentity;
|
||||||
private String avatarUrl;
|
|
||||||
private String role;
|
@Inject
|
||||||
private int nombreNotificationsNonLues;
|
@IdToken
|
||||||
private int nombreMessagesNonLus;
|
JsonWebToken idToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise les données de l'utilisateur connecté.
|
* Récupère le nom complet de l'utilisateur depuis le token OIDC.
|
||||||
|
* Méthode dynamique qui récupère les informations à chaque appel.
|
||||||
*/
|
*/
|
||||||
@PostConstruct
|
public String getNomComplet() {
|
||||||
public void init() {
|
try {
|
||||||
// TODO: Récupérer depuis le token JWT ou la session OIDC
|
if (securityIdentity != null && securityIdentity.getPrincipal() != null && idToken != null) {
|
||||||
nomComplet = "Jean Dupont";
|
// Nom complet (preferred_username ou name)
|
||||||
email = "jean.dupont@btpxpress.com";
|
String nom = idToken.getClaim("name");
|
||||||
avatarUrl = "/resources/freya-layout/images/avatar-profilemenu.png";
|
if (nom == null || nom.trim().isEmpty()) {
|
||||||
role = "Gestionnaire de Projets";
|
nom = idToken.getClaim("preferred_username");
|
||||||
nombreNotificationsNonLues = 5;
|
}
|
||||||
nombreMessagesNonLus = 3;
|
if (nom == null || nom.trim().isEmpty()) {
|
||||||
|
nom = securityIdentity.getPrincipal().getName();
|
||||||
|
}
|
||||||
|
return nom != null ? nom : "Utilisateur";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération du nom complet", e);
|
||||||
|
}
|
||||||
|
return "Utilisateur";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'email de l'utilisateur depuis le token OIDC.
|
||||||
|
*/
|
||||||
|
public String getEmail() {
|
||||||
|
try {
|
||||||
|
if (securityIdentity != null && securityIdentity.getPrincipal() != null && idToken != null) {
|
||||||
|
String email = idToken.getClaim("email");
|
||||||
|
return email != null ? email : "utilisateur@btpxpress.com";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération de l'email", e);
|
||||||
|
}
|
||||||
|
return "utilisateur@btpxpress.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne l'URL de l'avatar (par défaut).
|
||||||
|
*/
|
||||||
|
public String getAvatarUrl() {
|
||||||
|
return "/resources/freya-layout/images/avatar-profilemenu.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le rôle de l'utilisateur depuis SecurityIdentity.
|
||||||
|
*/
|
||||||
|
public String getRole() {
|
||||||
|
try {
|
||||||
|
if (securityIdentity != null && securityIdentity.getRoles() != null && !securityIdentity.getRoles().isEmpty()) {
|
||||||
|
String role = securityIdentity.getRoles().iterator().next();
|
||||||
|
// Formatage du rôle pour affichage (enlever préfixes)
|
||||||
|
role = role.replace("_", " ").replace("-", " ");
|
||||||
|
return capitalizeWords(role);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération du rôle", e);
|
||||||
|
}
|
||||||
|
return "Utilisateur";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nombre de notifications non lues (TODO: implémenter via API).
|
||||||
|
*/
|
||||||
|
public int getNombreNotificationsNonLues() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nombre de messages non lus (TODO: implémenter via API).
|
||||||
|
*/
|
||||||
|
public int getNombreMessagesNonLus() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne les initiales de l'utilisateur pour l'avatar.
|
* Retourne les initiales de l'utilisateur pour l'avatar.
|
||||||
*
|
*
|
||||||
* @return Les initiales (ex: "JD" pour "Jean Dupont")
|
* @return Les initiales (ex: "JD" pour "Jean Dupont")
|
||||||
*/
|
*/
|
||||||
public String getInitiales() {
|
public String getInitiales() {
|
||||||
|
String nomComplet = getNomComplet();
|
||||||
if (nomComplet == null || nomComplet.trim().isEmpty()) {
|
if (nomComplet == null || nomComplet.trim().isEmpty()) {
|
||||||
return "U";
|
return "U";
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] parts = nomComplet.trim().split("\\s+");
|
String[] parts = nomComplet.trim().split("\\s+");
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
return String.valueOf(parts[0].charAt(0)).toUpperCase() +
|
return String.valueOf(parts[0].charAt(0)).toUpperCase() +
|
||||||
String.valueOf(parts[1].charAt(0)).toUpperCase();
|
String.valueOf(parts[1].charAt(0)).toUpperCase();
|
||||||
} else if (parts.length == 1) {
|
} else if (parts.length == 1) {
|
||||||
return parts[0].substring(0, Math.min(2, parts[0].length())).toUpperCase();
|
return parts[0].substring(0, Math.min(2, parts[0].length())).toUpperCase();
|
||||||
@@ -67,13 +153,52 @@ public class UserSessionBean implements Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action de déconnexion.
|
* Action de déconnexion OIDC/Keycloak.
|
||||||
*
|
* Redirige vers l'endpoint de logout Keycloak pour détruire la session.
|
||||||
* @return La page de login
|
*
|
||||||
|
* @return Null pour déclencher une redirection externe
|
||||||
*/
|
*/
|
||||||
public String deconnecter() {
|
public String deconnecter() {
|
||||||
// TODO: Implémenter la déconnexion OIDC/Keycloak
|
try {
|
||||||
return "/login?faces-redirect=true";
|
log.info("Déconnexion de l'utilisateur: {}", getNomComplet());
|
||||||
|
|
||||||
|
jakarta.faces.context.FacesContext facesContext = jakarta.faces.context.FacesContext.getCurrentInstance();
|
||||||
|
jakarta.faces.context.ExternalContext externalContext = facesContext.getExternalContext();
|
||||||
|
|
||||||
|
// Construction de l'URL de logout Keycloak
|
||||||
|
String keycloakLogoutUrl = "https://security.lions.dev/realms/btpxpress/protocol/openid-connect/logout";
|
||||||
|
|
||||||
|
// URL de redirection après logout
|
||||||
|
String baseUrl = externalContext.getRequestScheme() + "://" +
|
||||||
|
externalContext.getRequestServerName() + ":" +
|
||||||
|
externalContext.getRequestServerPort() +
|
||||||
|
externalContext.getRequestContextPath();
|
||||||
|
|
||||||
|
String postLogoutRedirectUri = baseUrl + "/";
|
||||||
|
|
||||||
|
// Construire l'URL complète avec les paramètres
|
||||||
|
StringBuilder logoutUrl = new StringBuilder(keycloakLogoutUrl);
|
||||||
|
logoutUrl.append("?post_logout_redirect_uri=").append(java.net.URLEncoder.encode(postLogoutRedirectUri, "UTF-8"));
|
||||||
|
|
||||||
|
// Ajouter le id_token_hint si disponible
|
||||||
|
if (idToken != null && idToken.getRawToken() != null) {
|
||||||
|
logoutUrl.append("&id_token_hint=").append(java.net.URLEncoder.encode(idToken.getRawToken(), "UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Redirection vers Keycloak logout: {}", keycloakLogoutUrl);
|
||||||
|
|
||||||
|
// Invalider la session HTTP locale
|
||||||
|
externalContext.invalidateSession();
|
||||||
|
|
||||||
|
// Rediriger vers Keycloak logout
|
||||||
|
externalContext.redirect(logoutUrl.toString());
|
||||||
|
facesContext.responseComplete();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la déconnexion", e);
|
||||||
|
return "/login?faces-redirect=true";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,10 +107,11 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<h:form>
|
<h:form>
|
||||||
<h:commandLink action="#{userSession.deconnecter()}" styleClass="logout-link">
|
<p:commandButton action="#{userSession.deconnecter()}"
|
||||||
<i class="pi pi-sign-out"></i>
|
value="Logout"
|
||||||
<span>Logout</span>
|
styleClass="logout-link p-button-text"
|
||||||
</h:commandLink>
|
ajax="false"
|
||||||
|
icon="pi pi-sign-out"/>
|
||||||
</h:form>
|
</h:form>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jakarta.faces.VALIDATE_EMPTY_FIELDS=auto
|
|||||||
quarkus.arc.remove-unused-beans=true
|
quarkus.arc.remove-unused-beans=true
|
||||||
|
|
||||||
# Serveur HTTP
|
# Serveur HTTP
|
||||||
quarkus.http.port=8081
|
quarkus.http.port=8080
|
||||||
quarkus.http.host=0.0.0.0
|
quarkus.http.host=0.0.0.0
|
||||||
|
|
||||||
# CORS Configuration pour production
|
# CORS Configuration pour production
|
||||||
@@ -41,6 +41,7 @@ quarkus.http.cors.access-control-allow-credentials=true
|
|||||||
quarkus.oidc.enabled=true
|
quarkus.oidc.enabled=true
|
||||||
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/btpxpress
|
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/btpxpress
|
||||||
quarkus.oidc.client-id=btpxpress-frontend
|
quarkus.oidc.client-id=btpxpress-frontend
|
||||||
|
quarkus.oidc.credentials.secret=0Ph4e31lQQuonodmLQG3JycehbFL1Hei
|
||||||
quarkus.oidc.application-type=web-app
|
quarkus.oidc.application-type=web-app
|
||||||
quarkus.oidc.tls.verification=required
|
quarkus.oidc.tls.verification=required
|
||||||
|
|
||||||
@@ -78,12 +79,14 @@ quarkus.http.auth.proactive=true
|
|||||||
quarkus.security.deny-unannotated-endpoints=false
|
quarkus.security.deny-unannotated-endpoints=false
|
||||||
|
|
||||||
# Permissions pour accès public aux ressources statiques et pages publiques
|
# Permissions pour accès public aux ressources statiques et pages publiques
|
||||||
quarkus.http.auth.permission.public.paths=/*.css,/*.js,/*.png,/*.jpg,/*.jpeg,/*.gif,/*.svg,/*.woff,/*.woff2,/*.ttf,/*.eot,/resources/*
|
# NOTE: Quarkus supporte seulement * pour UN segment (pas **)
|
||||||
|
# Désactiver auth proactive permet aux ressources JSF d'être servies
|
||||||
|
quarkus.http.auth.permission.public.paths=/resources/*,/jakarta.faces.resource/*,/static/*,/webjars/*,/api/auth/*
|
||||||
quarkus.http.auth.permission.public.policy=permit
|
quarkus.http.auth.permission.public.policy=permit
|
||||||
|
|
||||||
# Authentification requise pour toutes les autres pages
|
# Authentification requise pour les pages de l'application
|
||||||
quarkus.http.auth.permission.authenticated.paths=/*
|
quarkus.http.auth.permission.app.paths=/app/*,/dashboard/*,/chantiers/*,/clients/*,/employes/*,/equipes/*,/devis/*,/factures/*,/materiels/*,/maintenance/*,/stock/*,/planning/*,/rapports/*,/admin/*
|
||||||
quarkus.http.auth.permission.authenticated.policy=authenticated
|
quarkus.http.auth.permission.app.policy=authenticated
|
||||||
|
|
||||||
# Configuration API Backend
|
# Configuration API Backend
|
||||||
btpxpress.api.base-url=${BTPXPRESS_API_BASE_URL:https://api.btpxpress.lions.dev}
|
btpxpress.api.base-url=${BTPXPRESS_API_BASE_URL:https://api.btpxpress.lions.dev}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jakarta.faces.VALIDATE_EMPTY_FIELDS=auto
|
|||||||
|
|
||||||
quarkus.arc.remove-unused-beans=false
|
quarkus.arc.remove-unused-beans=false
|
||||||
|
|
||||||
quarkus.http.port=8081
|
quarkus.http.port=8080
|
||||||
quarkus.http.cors=true
|
quarkus.http.cors=true
|
||||||
quarkus.http.cors.origins=http://localhost:8080,https://security.lions.dev
|
quarkus.http.cors.origins=http://localhost:8080,https://security.lions.dev
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user