diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..012e4b9 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,85 @@ +#### +# Dockerfile de production pour Lions User Manager Client (Frontend) +# Multi-stage build optimisé avec sécurité renforcée +# Basé sur la structure de btpxpress-client +#### + +## Stage 1 : Build avec Maven +FROM maven:3.9.6-eclipse-temurin-17 AS builder + +WORKDIR /app + +# Copier pom.xml et télécharger les dépendances (cache Docker) +COPY pom.xml . +RUN mvn dependency:go-offline -B + +# Copier le code source +COPY src ./src + +# Build de l'application avec profil production (fast-jar par défaut) +RUN mvn clean package -DskipTests -B -Dquarkus.profile=prod + +## Stage 2 : Image de production optimisée et sécurisée +FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 + +ENV LANGUAGE='fr_FR:fr' + +# Variables d'environnement de production +# Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes +ENV QUARKUS_PROFILE=prod +ENV QUARKUS_HTTP_PORT=8080 +ENV QUARKUS_HTTP_HOST=0.0.0.0 + +# Configuration Keycloak/OIDC (production) +ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/master +ENV QUARKUS_OIDC_CLIENT_ID=lions-user-manager-client +ENV QUARKUS_OIDC_ENABLED=true +ENV QUARKUS_OIDC_TLS_VERIFICATION=required + +# Configuration API Backend +ENV LIONS_USER_MANAGER_BACKEND_URL=https://api.lions.dev/lions-user-manager + +# Configuration CORS +ENV QUARKUS_HTTP_CORS_ORIGINS=https://user-manager.lions.dev,https://admin.lions.dev +ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true + +# Installer curl pour les health checks +USER root +RUN microdnf install -y curl && \ + microdnf clean all && \ + rm -rf /var/cache/yum + +# Créer les répertoires et permissions pour utilisateur non-root +RUN mkdir -p /deployments /app/logs && \ + chown -R 185:185 /deployments /app/logs + +# Passer à l'utilisateur non-root pour la sécurité +USER 185 + +# Copier l'application depuis le builder (format fast-jar Quarkus) +COPY --from=builder --chown=185 /app/target/quarkus-app/ /deployments/ + +# Exposer le port +EXPOSE 8080 + +# Variables JVM optimisées pour production avec sécurité +ENV JAVA_OPTS="-Xmx768m -Xms256m \ + -XX:+UseG1GC \ + -XX:MaxGCPauseMillis=200 \ + -XX:+UseStringDeduplication \ + -XX:+ParallelRefProcEnabled \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:HeapDumpPath=/app/logs/heapdump.hprof \ + -Djava.security.egd=file:/dev/./urandom \ + -Djava.awt.headless=true \ + -Dfile.encoding=UTF-8 \ + -Djava.util.logging.manager=org.jboss.logmanager.LogManager \ + -Dquarkus.profile=${QUARKUS_PROFILE}" + +# Health check avec endpoints Quarkus +HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ + CMD curl -f http://localhost:8080/q/health/ready || exit 1 + +# Point d'entrée avec profil production (format fast-jar) +ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"] + diff --git a/pom.xml b/pom.xml index 71aafee..864ef71 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ lions-user-manager-client-quarkus-primefaces-freya + 1.0.1 jar Lions User Manager - Client (Quarkus + PrimeFaces Freya) @@ -21,6 +22,7 @@ dev.lions.user.manager lions-user-manager-server-api + 1.0.0 @@ -65,6 +67,12 @@ 5.0.0 + + + dev.lions + primefaces-freya-extension + + io.quarkiverse.omnifaces @@ -91,6 +99,26 @@ test + + io.quarkus + quarkus-junit5-mockito + test + + + + org.mockito + mockito-core + 5.7.0 + test + + + + org.mockito + mockito-junit-jupiter + 5.7.0 + test + + io.rest-assured rest-assured diff --git a/src/main/java/dev/lions/user/manager/client/api/AuditRestClient.java b/src/main/java/dev/lions/user/manager/client/api/AuditRestClient.java new file mode 100644 index 0000000..82170a1 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/api/AuditRestClient.java @@ -0,0 +1,52 @@ +package dev.lions.user.manager.client.api; + +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.util.List; +import java.util.Map; + +@RegisterRestClient(configKey = "user-api") +@Path("/api/audit") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public interface AuditRestClient { + + @GET + List searchLogs( + @QueryParam("acteur") String acteur, + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin, + @QueryParam("typeAction") TypeActionAudit typeAction, + @QueryParam("ressourceType") String ressourceType, + @QueryParam("succes") Boolean succes, + @QueryParam("page") int page, + @QueryParam("pageSize") int pageSize); + + @GET + @Path("/stats/actions") + Map getActionStatistics( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @GET + @Path("/stats/activity") + Map getUserActivityStatistics( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @GET + @Path("/stats/failures") + long getFailureCount( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @GET + @Path("/stats/successes") + long getSuccessCount( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); +} diff --git a/src/main/java/dev/lions/user/manager/client/api/HealthRestClient.java b/src/main/java/dev/lions/user/manager/client/api/HealthRestClient.java new file mode 100644 index 0000000..4963a02 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/api/HealthRestClient.java @@ -0,0 +1,23 @@ +package dev.lions.user.manager.client.api; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.util.Map; + +@RegisterRestClient(configKey = "user-api") +@Path("/api/health") +@Produces(MediaType.APPLICATION_JSON) +public interface HealthRestClient { + + @GET + @Path("/keycloak") + Map getKeycloakHealth(); + + @GET + @Path("/status") + Map getServiceStatus(); +} diff --git a/src/main/java/dev/lions/user/manager/client/api/RoleRestClient.java b/src/main/java/dev/lions/user/manager/client/api/RoleRestClient.java new file mode 100644 index 0000000..e9af811 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/api/RoleRestClient.java @@ -0,0 +1,57 @@ +package dev.lions.user.manager.client.api; + +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.util.List; + +@RegisterRestClient(configKey = "user-api") +@Path("/api/roles") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public interface RoleRestClient { + + @GET + @Path("/realm") + List getAllRealmRoles(@QueryParam("realm") String realmName); + + @POST + @Path("/realm") + RoleDTO createRealmRole(@QueryParam("realm") String realmName, RoleDTO role); + + @GET + @Path("/realm/{roleName}") + RoleDTO getRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName); + + @PUT + @Path("/realm/{roleName}") + RoleDTO updateRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName, + RoleDTO role); + + @DELETE + @Path("/realm/{roleName}") + void deleteRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName); + + @POST + @Path("/users/{userId}/realm-roles") + void assignRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName, + RoleAssignmentRequest request); + + @DELETE + @Path("/users/{userId}/realm-roles") + void revokeRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName, + RoleAssignmentRequest request); + + @GET + @Path("/users/{userId}/realm-roles") + List getUserRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName); + + // Inner class for role assignment request + class RoleAssignmentRequest { + public List roleNames; + } +} diff --git a/src/main/java/dev/lions/user/manager/client/api/SyncRestClient.java b/src/main/java/dev/lions/user/manager/client/api/SyncRestClient.java new file mode 100644 index 0000000..6740723 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/api/SyncRestClient.java @@ -0,0 +1,31 @@ +package dev.lions.user.manager.client.api; + +import dev.lions.user.manager.dto.sync.HealthStatusDTO; +import dev.lions.user.manager.dto.sync.SyncResultDTO; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "user-api") +@Path("/api/sync") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public interface SyncRestClient { + + @POST + @Path("/{realmName}/users") + void syncUsers(@PathParam("realmName") String realmName); + + @POST + @Path("/{realmName}/roles") + void syncRoles(@PathParam("realmName") String realmName); + + @POST + @Path("/{realmName}/all") + SyncResultDTO syncAll(@PathParam("realmName") String realmName); // Assumant que syncAll retourne un résultat + // détaillé + + @GET + @Path("/health") + HealthStatusDTO getHealthStatus(); +} diff --git a/src/main/java/dev/lions/user/manager/client/api/UserRestClient.java b/src/main/java/dev/lions/user/manager/client/api/UserRestClient.java new file mode 100644 index 0000000..7a8c1e7 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/api/UserRestClient.java @@ -0,0 +1,73 @@ +package dev.lions.user.manager.client.api; + +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.util.List; + +@RegisterRestClient(configKey = "user-api") +@Path("/api/users") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public interface UserRestClient { + + @GET + UserSearchResultDTO searchUsers( + @QueryParam("realm") String realmName, + @QueryParam("search") String searchTerm, + @QueryParam("username") String username, + @QueryParam("email") String email, + @QueryParam("prenom") String prenom, + @QueryParam("nom") String nom, + @QueryParam("enabled") Boolean enabled, + @QueryParam("page") int page, + @QueryParam("pageSize") int pageSize); + + @GET + @Path("/{id}") + UserDTO getUserById(@PathParam("id") String id, @QueryParam("realm") String realmName); + + @POST + Response createUser(@QueryParam("realm") String realmName, UserDTO user); + + @PUT + @Path("/{id}") + UserDTO updateUser(@PathParam("id") String id, @QueryParam("realm") String realmName, UserDTO user); + + @DELETE + @Path("/{id}") + void deleteUser(@PathParam("id") String id, @QueryParam("realm") String realmName, + @QueryParam("hard") boolean hardDelete); + + @PUT + @Path("/{id}/activate") + void activateUser(@PathParam("id") String id, @QueryParam("realm") String realmName); + + @PUT + @Path("/{id}/deactivate") + void deactivateUser(@PathParam("id") String id, @QueryParam("realm") String realmName, + @QueryParam("reason") String reason); + + @POST + @Path("/{id}/reset-password") + void resetPassword(@PathParam("id") String id, @QueryParam("realm") String realmName, PasswordResetRequest request); + + @POST + @Path("/{id}/send-verify-email") + void sendVerificationEmail(@PathParam("id") String id, @QueryParam("realm") String realmName); + + @GET + @Path("/export/csv") + @Produces(MediaType.TEXT_PLAIN) + String exportUsersToCSV(@QueryParam("realm") String realmName); + + // Inner class for password reset request DTO + class PasswordResetRequest { + public String password; + public boolean temporary; + } +} diff --git a/src/main/java/dev/lions/user/manager/client/filter/AuthHeaderFactory.java b/src/main/java/dev/lions/user/manager/client/filter/AuthHeaderFactory.java new file mode 100644 index 0000000..06d29bc --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/filter/AuthHeaderFactory.java @@ -0,0 +1,52 @@ +package dev.lions.user.manager.client.filter; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; + +import java.util.logging.Logger; + +/** + * Factory pour ajouter automatiquement le token OIDC Bearer + * dans les headers des requêtes REST Client vers le backend. + * + * Ce factory est nécessaire car bearer-token-propagation ne fonctionne pas + * pour les appels depuis les managed beans JSF vers le backend. + * + * @author Lions User Manager + * @version 1.0.0 + */ +@ApplicationScoped +public class AuthHeaderFactory implements ClientHeadersFactory { + + private static final Logger LOGGER = Logger.getLogger(AuthHeaderFactory.class.getName()); + + @Inject + JsonWebToken jwt; + + @Override + public MultivaluedMap update( + MultivaluedMap incomingHeaders, + MultivaluedMap clientOutgoingHeaders) { + + MultivaluedMap result = new MultivaluedHashMap<>(); + + try { + // Vérifier si le JWT est disponible et non expiré + if (jwt != null && jwt.getRawToken() != null && !jwt.getRawToken().isEmpty()) { + String token = jwt.getRawToken(); + result.add("Authorization", "Bearer " + token); + LOGGER.fine("Token Bearer ajouté au header Authorization"); + } else { + LOGGER.warning("Token JWT non disponible ou vide - impossible d'ajouter le header Authorization"); + } + } catch (Exception e) { + LOGGER.severe("Erreur lors de l'ajout du token Bearer: " + e.getMessage()); + } + + return result; + } +} diff --git a/src/main/java/dev/lions/user/manager/client/service/AuditServiceClient.java b/src/main/java/dev/lions/user/manager/client/service/AuditServiceClient.java index 947b933..c393e15 100644 --- a/src/main/java/dev/lions/user/manager/client/service/AuditServiceClient.java +++ b/src/main/java/dev/lions/user/manager/client/service/AuditServiceClient.java @@ -1,9 +1,11 @@ package dev.lions.user.manager.client.service; +import dev.lions.user.manager.client.filter.AuthHeaderFactory; import dev.lions.user.manager.dto.audit.AuditLogDTO; import dev.lions.user.manager.enums.audit.TypeActionAudit; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import java.time.LocalDateTime; @@ -15,6 +17,7 @@ import java.util.Map; */ @Path("/api/audit") @RegisterRestClient(configKey = "lions-user-manager-api") +@RegisterClientHeaders(AuthHeaderFactory.class) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public interface AuditServiceClient { @@ -33,7 +36,7 @@ public interface AuditServiceClient { ); @GET - @Path("/acteur/{acteurUsername}") + @Path("/actor/{acteurUsername}") List getLogsByActeur( @PathParam("acteurUsername") String acteurUsername, @QueryParam("limit") @DefaultValue("100") int limit @@ -67,36 +70,43 @@ public interface AuditServiceClient { ); @GET - @Path("/statistics/actions") + @Path("/stats/actions") Map getActionStatistics( @QueryParam("dateDebut") String dateDebut, @QueryParam("dateFin") String dateFin ); @GET - @Path("/statistics/users") + @Path("/stats/users") Map getUserActivityStatistics( @QueryParam("dateDebut") String dateDebut, @QueryParam("dateFin") String dateFin ); @GET - @Path("/statistics/failures") - Long getFailureCount( + @Path("/stats/failures") + CountResponse getFailureCount( @QueryParam("dateDebut") String dateDebut, @QueryParam("dateFin") String dateFin ); @GET - @Path("/statistics/successes") - Long getSuccessCount( + @Path("/stats/success") + CountResponse getSuccessCount( @QueryParam("dateDebut") String dateDebut, @QueryParam("dateFin") String dateFin ); + /** + * DTO pour les réponses de comptage + */ + class CountResponse { + public long count; + } + @GET @Path("/export/csv") - @Produces("text/csv") + @Produces(MediaType.TEXT_PLAIN) String exportLogsToCSV( @QueryParam("dateDebut") String dateDebut, @QueryParam("dateFin") String dateFin diff --git a/src/main/java/dev/lions/user/manager/client/service/RealmAssignmentServiceClient.java b/src/main/java/dev/lions/user/manager/client/service/RealmAssignmentServiceClient.java new file mode 100644 index 0000000..2c615e5 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/service/RealmAssignmentServiceClient.java @@ -0,0 +1,101 @@ +package dev.lions.user.manager.client.service; + +import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.util.List; + +/** + * REST Client pour le service de gestion des affectations de realms + */ +@Path("/api/realm-assignments") +@RegisterRestClient(configKey = "lions-user-manager-api") +@RegisterClientHeaders(AuthHeaderFactory.class) +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public interface RealmAssignmentServiceClient { + + // ==================== Consultation ==================== + + @GET + List getAllAssignments(); + + @GET + @Path("/user/{userId}") + List getAssignmentsByUser(@PathParam("userId") String userId); + + @GET + @Path("/realm/{realmName}") + List getAssignmentsByRealm(@PathParam("realmName") String realmName); + + @GET + @Path("/{assignmentId}") + RealmAssignmentDTO getAssignmentById(@PathParam("assignmentId") String assignmentId); + + // ==================== Vérification ==================== + + @GET + @Path("/check") + CheckResponse canManageRealm( + @QueryParam("userId") String userId, + @QueryParam("realmName") String realmName + ); + + @GET + @Path("/authorized-realms/{userId}") + AuthorizedRealmsResponse getAuthorizedRealms(@PathParam("userId") String userId); + + // ==================== Modification ==================== + + @POST + RealmAssignmentDTO assignRealmToUser(RealmAssignmentDTO assignment); + + @DELETE + @Path("/user/{userId}/realm/{realmName}") + void revokeRealmFromUser( + @PathParam("userId") String userId, + @PathParam("realmName") String realmName + ); + + @DELETE + @Path("/user/{userId}") + void revokeAllRealmsFromUser(@PathParam("userId") String userId); + + @PUT + @Path("/{assignmentId}/deactivate") + void deactivateAssignment(@PathParam("assignmentId") String assignmentId); + + @PUT + @Path("/{assignmentId}/activate") + void activateAssignment(@PathParam("assignmentId") String assignmentId); + + @PUT + @Path("/super-admin/{userId}") + void setSuperAdmin( + @PathParam("userId") String userId, + @QueryParam("superAdmin") Boolean superAdmin + ); + + // ==================== Classes de réponse ==================== + + /** + * Réponse de vérification d'accès + */ + class CheckResponse { + public boolean canManage; + public String userId; + public String realmName; + } + + /** + * Réponse des realms autorisés + */ + class AuthorizedRealmsResponse { + public List realms; + public boolean isSuperAdmin; + } +} diff --git a/src/main/java/dev/lions/user/manager/client/service/RealmServiceClient.java b/src/main/java/dev/lions/user/manager/client/service/RealmServiceClient.java new file mode 100644 index 0000000..44fdbdb --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/service/RealmServiceClient.java @@ -0,0 +1,33 @@ +package dev.lions.user.manager.client.service; + +import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.util.List; + +/** + * REST Client pour le service de gestion des realms Keycloak + * Interface pour communiquer avec l'API backend + */ +@Path("/api/realms") +@RegisterRestClient(configKey = "lions-user-manager-api") +@RegisterClientHeaders(AuthHeaderFactory.class) +@RegisterProvider(RestClientExceptionMapper.class) +@Produces(MediaType.APPLICATION_JSON) +public interface RealmServiceClient { + + /** + * Récupère la liste de tous les realms disponibles dans Keycloak + * @return liste des noms de realms + */ + @GET + @Path("/list") + List getAllRealms(); +} + diff --git a/src/main/java/dev/lions/user/manager/client/service/RestClientExceptionMapper.java b/src/main/java/dev/lions/user/manager/client/service/RestClientExceptionMapper.java new file mode 100644 index 0000000..4e90b0d --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/service/RestClientExceptionMapper.java @@ -0,0 +1,112 @@ +package dev.lions.user.manager.client.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; + +/** + * Mapper d'exceptions pour les clients REST + * Convertit les réponses HTTP d'erreur en exceptions appropriées + */ +public class RestClientExceptionMapper implements ResponseExceptionMapper { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public RuntimeException toThrowable(Response response) { + int status = response.getStatus(); + String reasonPhrase = response.getStatusInfo().getReasonPhrase(); + + // Lire le corps de la réponse pour plus de détails + String errorMessage = reasonPhrase; + try { + if (response.hasEntity()) { + String body = response.readEntity(String.class); + if (body != null && !body.isEmpty()) { + // Essayer de parser le JSON pour extraire le message + try { + JsonNode jsonNode = objectMapper.readTree(body); + if (jsonNode.has("message")) { + errorMessage = jsonNode.get("message").asText(); + } else { + errorMessage = body; + } + } catch (Exception e) { + // Si ce n'est pas du JSON, utiliser le body tel quel + errorMessage = body; + } + } + } + } catch (Exception e) { + // Ignorer les erreurs de lecture du body + } + + return switch (status) { + case 400 -> new BadRequestException("Requête invalide: " + errorMessage); + case 401 -> new UnauthorizedException("Non autorisé: " + errorMessage); + case 403 -> new ForbiddenException("Accès interdit: " + errorMessage); + case 404 -> new NotFoundException(errorMessage); + case 409 -> new ConflictException("Conflit: " + errorMessage); + case 422 -> new UnprocessableEntityException("Données non valides: " + errorMessage); + case 500 -> new InternalServerErrorException("Erreur serveur interne: " + errorMessage); + case 502 -> new BadGatewayException("Erreur de passerelle: " + errorMessage); + case 503 -> new ServiceUnavailableException("Service indisponible: " + errorMessage); + case 504 -> new GatewayTimeoutException("Timeout de passerelle: " + errorMessage); + default -> new UnknownHttpStatusException("Erreur HTTP " + status + ": " + errorMessage); + }; + } + + @Override + public boolean handles(int status, MultivaluedMap headers) { + // Gérer tous les codes d'erreur HTTP (>= 400) + return status >= 400; + } + + // Classes d'exception personnalisées + public static class BadRequestException extends RuntimeException { + public BadRequestException(String message) { super(message); } + } + + public static class UnauthorizedException extends RuntimeException { + public UnauthorizedException(String message) { super(message); } + } + + public static class ForbiddenException extends RuntimeException { + public ForbiddenException(String message) { super(message); } + } + + public static class NotFoundException extends RuntimeException { + public NotFoundException(String message) { super(message); } + } + + public static class ConflictException extends RuntimeException { + public ConflictException(String message) { super(message); } + } + + public static class UnprocessableEntityException extends RuntimeException { + public UnprocessableEntityException(String message) { super(message); } + } + + public static class InternalServerErrorException extends RuntimeException { + public InternalServerErrorException(String message) { super(message); } + } + + public static class BadGatewayException extends RuntimeException { + public BadGatewayException(String message) { super(message); } + } + + public static class ServiceUnavailableException extends RuntimeException { + public ServiceUnavailableException(String message) { super(message); } + } + + public static class GatewayTimeoutException extends RuntimeException { + public GatewayTimeoutException(String message) { super(message); } + } + + public static class UnknownHttpStatusException extends RuntimeException { + public UnknownHttpStatusException(String message) { super(message); } + } +} + diff --git a/src/main/java/dev/lions/user/manager/client/service/RoleServiceClient.java b/src/main/java/dev/lions/user/manager/client/service/RoleServiceClient.java index 28ed731..26e4ac7 100644 --- a/src/main/java/dev/lions/user/manager/client/service/RoleServiceClient.java +++ b/src/main/java/dev/lions/user/manager/client/service/RoleServiceClient.java @@ -1,10 +1,12 @@ package dev.lions.user.manager.client.service; +import dev.lions.user.manager.client.filter.AuthHeaderFactory; import dev.lions.user.manager.dto.role.RoleAssignmentDTO; import dev.lions.user.manager.dto.role.RoleDTO; import dev.lions.user.manager.enums.role.TypeRole; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import java.util.List; @@ -14,6 +16,7 @@ import java.util.List; */ @Path("/api/roles") @RegisterRestClient(configKey = "lions-user-manager-api") +@RegisterClientHeaders(AuthHeaderFactory.class) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public interface RoleServiceClient { @@ -58,53 +61,76 @@ public interface RoleServiceClient { // ==================== Client Roles ==================== @POST - @Path("/client") + @Path("/client/{clientId}") RoleDTO createClientRole( + @PathParam("clientId") String clientId, RoleDTO role, - @QueryParam("realm") String realmName, - @QueryParam("clientName") String clientName + @QueryParam("realm") String realmName ); @GET - @Path("/client") + @Path("/client/{clientId}") List getAllClientRoles( - @QueryParam("realm") String realmName, - @QueryParam("clientName") String clientName + @PathParam("clientId") String clientId, + @QueryParam("realm") String realmName ); @GET - @Path("/client/{roleName}") + @Path("/client/{clientId}/{roleName}") RoleDTO getClientRoleByName( + @PathParam("clientId") String clientId, @PathParam("roleName") String roleName, - @QueryParam("realm") String realmName, - @QueryParam("clientName") String clientName + @QueryParam("realm") String realmName ); @DELETE - @Path("/client/{roleName}") + @Path("/client/{clientId}/{roleName}") void deleteClientRole( + @PathParam("clientId") String clientId, @PathParam("roleName") String roleName, - @QueryParam("realm") String realmName, - @QueryParam("clientName") String clientName + @QueryParam("realm") String realmName ); // ==================== Role Assignment ==================== @POST - @Path("/assign") - void assignRoleToUser(RoleAssignmentDTO assignment); + @Path("/assign/realm/{userId}") + void assignRealmRolesToUser( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + RoleAssignmentRequest request + ); @POST - @Path("/revoke") - void revokeRoleFromUser(RoleAssignmentDTO assignment); + @Path("/revoke/realm/{userId}") + void revokeRealmRolesFromUser( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + RoleAssignmentRequest request + ); @GET - @Path("/user/{userId}") - List getUserRoles( + @Path("/user/realm/{userId}") + List getUserRealmRoles( @PathParam("userId") String userId, @QueryParam("realm") String realmName ); + @GET + @Path("/user/client/{clientId}/{userId}") + List getUserClientRoles( + @PathParam("clientId") String clientId, + @PathParam("userId") String userId, + @QueryParam("realm") String realmName + ); + + /** + * DTO pour l'attribution/révocation de rôles + */ + class RoleAssignmentRequest { + public List roleNames; + } + // ==================== Composite Roles ==================== @GET @@ -118,18 +144,10 @@ public interface RoleServiceClient { @POST @Path("/composite/{roleName}/add") - void addCompositeRole( + void addCompositeRoles( @PathParam("roleName") String roleName, @QueryParam("realm") String realmName, - @QueryParam("compositeRoleName") String compositeRoleName - ); - - @DELETE - @Path("/composite/{roleName}/remove") - void removeCompositeRole( - @PathParam("roleName") String roleName, - @QueryParam("realm") String realmName, - @QueryParam("compositeRoleName") String compositeRoleName + RoleAssignmentRequest request ); } diff --git a/src/main/java/dev/lions/user/manager/client/service/SyncServiceClient.java b/src/main/java/dev/lions/user/manager/client/service/SyncServiceClient.java index c7919f5..0526b6c 100644 --- a/src/main/java/dev/lions/user/manager/client/service/SyncServiceClient.java +++ b/src/main/java/dev/lions/user/manager/client/service/SyncServiceClient.java @@ -1,53 +1,83 @@ package dev.lions.user.manager.client.service; -import dev.lions.user.manager.dto.sync.HealthStatusDTO; -import dev.lions.user.manager.dto.sync.SyncResultDTO; +import dev.lions.user.manager.client.filter.AuthHeaderFactory; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import java.util.List; +import java.util.Map; + /** * REST Client pour le service de synchronisation */ @Path("/api/sync") @RegisterRestClient(configKey = "lions-user-manager-api") +@RegisterClientHeaders(AuthHeaderFactory.class) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public interface SyncServiceClient { @GET @Path("/health") - HealthStatusDTO checkHealth(@QueryParam("realm") String realmName); + HealthCheckResponse checkHealth(); @GET - @Path("/health/keycloak") - HealthStatusDTO checkKeycloakHealth(); + @Path("/health/detailed") + Map getDetailedHealthStatus(); @POST - @Path("/users") - SyncResultDTO syncUsers(@QueryParam("realm") String realmName); + @Path("/users/{realmName}") + SyncUsersResponse syncUsers(@PathParam("realmName") String realmName); @POST - @Path("/roles") - SyncResultDTO syncRoles( - @QueryParam("realm") String realmName, - @QueryParam("clientName") String clientName + @Path("/roles/realm/{realmName}") + SyncRolesResponse syncRealmRoles(@PathParam("realmName") String realmName); + + @POST + @Path("/roles/client/{clientId}/{realmName}") + SyncRolesResponse syncClientRoles( + @PathParam("clientId") String clientId, + @PathParam("realmName") String realmName ); + @POST + @Path("/all/{realmName}") + Map syncAll(@PathParam("realmName") String realmName); + @GET - @Path("/exists/user/{username}") - Boolean userExists( - @PathParam("username") String username, + @Path("/check/realm/{realmName}") + ExistsCheckResponse checkRealmExists(@PathParam("realmName") String realmName); + + @GET + @Path("/check/user/{userId}") + ExistsCheckResponse checkUserExists( + @PathParam("userId") String userId, @QueryParam("realm") String realmName ); - @GET - @Path("/exists/role/{roleName}") - Boolean roleExists( - @PathParam("roleName") String roleName, - @QueryParam("realm") String realmName, - @QueryParam("typeRole") String typeRole, - @QueryParam("clientName") String clientName - ); + // ==================== DTOs de réponse ==================== + + class SyncUsersResponse { + public int count; + public List users; + } + + class SyncRolesResponse { + public int count; + public List roles; + } + + class HealthCheckResponse { + public boolean healthy; + public String message; + } + + class ExistsCheckResponse { + public boolean exists; + public String resourceType; + public String resourceId; + } } diff --git a/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java b/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java index ffa4dae..22f3c53 100644 --- a/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java +++ b/src/main/java/dev/lions/user/manager/client/service/UserServiceClient.java @@ -1,10 +1,14 @@ package dev.lions.user.manager.client.service; +import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import dev.lions.user.manager.client.service.RestClientExceptionMapper; import dev.lions.user.manager.dto.user.UserDTO; import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; import dev.lions.user.manager.dto.user.UserSearchResultDTO; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import java.util.List; @@ -15,6 +19,8 @@ import java.util.List; */ @Path("/api/users") @RegisterRestClient(configKey = "lions-user-manager-api") +@RegisterClientHeaders(AuthHeaderFactory.class) +@RegisterProvider(RestClientExceptionMapper.class) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public interface UserServiceClient { @@ -104,9 +110,17 @@ public interface UserServiceClient { void resetPassword( @PathParam("userId") String userId, @QueryParam("realm") String realmName, - @QueryParam("newPassword") String newPassword + PasswordResetRequest request ); + /** + * DTO pour la réinitialisation de mot de passe + */ + class PasswordResetRequest { + public String password; + public boolean temporary = true; + } + /** * Envoyer un email de vérification */ diff --git a/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java b/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java index e28b927..fe528d2 100644 --- a/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java +++ b/src/main/java/dev/lions/user/manager/client/view/AuditConsultationBean.java @@ -1,6 +1,7 @@ package dev.lions.user.manager.client.view; import dev.lions.user.manager.client.service.AuditServiceClient; +import dev.lions.user.manager.client.service.RealmServiceClient; import dev.lions.user.manager.dto.audit.AuditLogDTO; import dev.lions.user.manager.enums.audit.TypeActionAudit; import jakarta.annotation.PostConstruct; @@ -16,6 +17,7 @@ import java.io.Serializable; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.logging.Logger; @@ -38,6 +40,10 @@ public class AuditConsultationBean implements Serializable { @RestClient private AuditServiceClient auditServiceClient; + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + // Liste des logs private List auditLogs = new ArrayList<>(); private AuditLogDTO selectedLog; @@ -146,8 +152,10 @@ public class AuditConsultationBean implements Serializable { actionStatistics = auditServiceClient.getActionStatistics(dateDebutStr, dateFinStr); userActivityStatistics = auditServiceClient.getUserActivityStatistics(dateDebutStr, dateFinStr); - failureCount = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr); - successCount = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr); + AuditServiceClient.CountResponse failureResponse = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr); + failureCount = failureResponse != null ? failureResponse.count : 0L; + AuditServiceClient.CountResponse successResponse = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr); + successCount = successResponse != null ? successResponse.count : 0L; } catch (Exception e) { LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage()); } @@ -185,11 +193,44 @@ public class AuditConsultationBean implements Serializable { } /** - * Charger les realms disponibles + * Page précédente + */ + public void previousPage() { + if (currentPage > 0) { + currentPage--; + searchLogs(); + } + } + + /** + * Page suivante + */ + public void nextPage() { + currentPage++; + searchLogs(); + } + + /** + * Charger les realms disponibles depuis Keycloak */ private void loadRealms() { - // TODO: Implémenter la récupération des realms depuis Keycloak - availableRealms = List.of("master", "btpxpress", "unionflow"); + try { + LOGGER.info("Chargement des realms disponibles depuis Keycloak"); + List realms = realmServiceClient.getAllRealms(); + + if (realms == null || realms.isEmpty()) { + LOGGER.warning("Aucun realm trouvé dans Keycloak"); + availableRealms = Collections.emptyList(); + } else { + availableRealms = new ArrayList<>(realms); + LOGGER.info("Realms disponibles chargés depuis Keycloak: " + availableRealms.size()); + } + } 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: liste vide plutôt que des données fictives + availableRealms = Collections.emptyList(); + } } // Méthodes utilitaires diff --git a/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java b/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java new file mode 100644 index 0000000..d125907 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java @@ -0,0 +1,252 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.AuditServiceClient; +import dev.lions.user.manager.client.service.RoleServiceClient; +import dev.lions.user.manager.client.service.UserServiceClient; +import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import jakarta.annotation.PostConstruct; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.logging.Logger; + +/** + * Bean JSF pour le tableau de bord + * + * @author Lions User Manager + * @version 1.0.0 + */ +@Named("dashboardBean") +@ViewScoped +@Data +public class DashboardBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(DashboardBean.class.getName()); + + @Inject + @RestClient + private UserServiceClient userServiceClient; + + @Inject + @RestClient + private RoleServiceClient roleServiceClient; + + @Inject + @RestClient + private AuditServiceClient auditServiceClient; + + // Statistiques + private Long totalUsers = 0L; + private Long totalRoles = 0L; + private Long recentActions = 0L; + private Long activeSessions = 0L; + private Long onlineUsers = 0L; + + // Indicateur de chargement + private boolean loading = false; + + // Méthodes pour obtenir les valeurs formatées pour l'affichage + public String getTotalUsersDisplay() { + if (loading) return "..."; + return totalUsers != null ? String.valueOf(totalUsers) : "0"; + } + + public String getTotalRolesDisplay() { + if (loading) return "..."; + return totalRoles != null ? String.valueOf(totalRoles) : "0"; + } + + public String getRecentActionsDisplay() { + if (loading) return "..."; + return recentActions != null ? String.valueOf(recentActions) : "0"; + } + + public boolean isLoading() { + return loading; + } + + // Realm par défaut + private String realmName = "master"; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + + @PostConstruct + public void init() { + LOGGER.info("=== Initialisation du DashboardBean ==="); + LOGGER.info("Realm par défaut: " + realmName); + LOGGER.info("UserServiceClient injecté: " + (userServiceClient != null ? "OUI" : "NON")); + LOGGER.info("RoleServiceClient injecté: " + (roleServiceClient != null ? "OUI" : "NON")); + LOGGER.info("AuditServiceClient injecté: " + (auditServiceClient != null ? "OUI" : "NON")); + loadStatistics(); + } + + /** + * Charger toutes les statistiques + */ + public void loadStatistics() { + loading = true; + try { + loadTotalUsers(); + loadTotalRoles(); + loadRecentActions(); + // Les sessions actives nécessitent une API spécifique qui n'existe pas encore + // activeSessions = 0L; + // onlineUsers = 0L; + } catch (Exception e) { + LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage()); + } finally { + loading = false; + } + } + + /** + * Charger le nombre total d'utilisateurs + */ + private void loadTotalUsers() { + try { + LOGGER.info("Début chargement total utilisateurs pour realm: " + realmName); + UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() + .realmName(realmName) + .page(0) + .pageSize(1) // On n'a besoin que du count + .build(); + + LOGGER.info("Appel userServiceClient.searchUsers()..."); + UserSearchResultDTO result = userServiceClient.searchUsers(criteria); + LOGGER.info("Résultat reçu: " + (result != null ? "NON NULL" : "NULL")); + + if (result != null && result.getTotalCount() != null) { + totalUsers = result.getTotalCount(); + LOGGER.info("✅ Total utilisateurs chargé avec succès: " + totalUsers); + } else { + LOGGER.warning("⚠️ Résultat de recherche utilisateurs null ou totalCount null"); + if (result == null) { + LOGGER.warning(" - result est null"); + } else { + LOGGER.warning(" - result.getTotalCount() est null"); + } + totalUsers = 0L; + } + } catch (Exception e) { + LOGGER.severe("❌ ERREUR lors du chargement du nombre d'utilisateurs: " + e.getMessage()); + LOGGER.severe(" Type d'erreur: " + e.getClass().getName()); + e.printStackTrace(); + totalUsers = 0L; + addErrorMessage("Impossible de charger le nombre d'utilisateurs: " + e.getMessage()); + } + } + + /** + * Charger le nombre total de rôles Realm + */ + private void loadTotalRoles() { + try { + LOGGER.info("Début chargement total rôles pour realm: " + realmName); + LOGGER.info("Appel roleServiceClient.getAllRealmRoles()..."); + List roles = roleServiceClient.getAllRealmRoles(realmName); + LOGGER.info("Résultat reçu: " + (roles != null ? "NON NULL, taille: " + roles.size() : "NULL")); + + if (roles != null) { + totalRoles = (long) roles.size(); + LOGGER.info("✅ Total rôles chargé avec succès: " + totalRoles); + } else { + LOGGER.warning("⚠️ Liste de rôles null"); + totalRoles = 0L; + } + } catch (Exception e) { + LOGGER.severe("❌ ERREUR lors du chargement du nombre de rôles: " + e.getMessage()); + LOGGER.severe(" Type d'erreur: " + e.getClass().getName()); + e.printStackTrace(); + totalRoles = 0L; + addErrorMessage("Impossible de charger le nombre de rôles: " + e.getMessage()); + } + } + + /** + * 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 + */ + public void refreshStatistics() { + LOGGER.info("=== Rafraîchissement des statistiques ==="); + loadStatistics(); + addSuccessMessage("Statistiques rafraîchies avec succès"); + } + + // Méthodes utilitaires pour les messages + private void addSuccessMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message)); + } + + private void addErrorMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); + } +} + diff --git a/src/main/java/dev/lions/user/manager/client/view/DashboardView.java b/src/main/java/dev/lions/user/manager/client/view/DashboardView.java new file mode 100644 index 0000000..b6ed63d --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/DashboardView.java @@ -0,0 +1,140 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.api.AuditRestClient; +import dev.lions.user.manager.client.api.HealthRestClient; +import dev.lions.user.manager.client.api.UserRestClient; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import jakarta.annotation.PostConstruct; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.primefaces.model.charts.ChartData; +import org.primefaces.model.charts.axes.cartesian.CartesianScales; +import org.primefaces.model.charts.axes.cartesian.linear.CartesianLinearAxes; +import org.primefaces.model.charts.bar.BarChartDataSet; +import org.primefaces.model.charts.bar.BarChartModel; +import org.primefaces.model.charts.bar.BarChartOptions; +import org.primefaces.model.charts.optionconfig.title.Title; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Named +@ViewScoped +@Slf4j +public class DashboardView implements Serializable { + + @Inject + @RestClient + AuditRestClient auditRestClient; + + @Inject + @RestClient + HealthRestClient healthRestClient; + + @Inject + @RestClient + UserRestClient userRestClient; + + @ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "test-realm") + String defaultRealm; + + @Getter + private long totalSuccesses; + + @Getter + private long totalFailures; + + @Getter + private long activeUsers; + + @Getter + private boolean systemHealthy; + + @Getter + private BarChartModel barModel; + + @PostConstruct + public void init() { + loadStats(); + createBarModel(); + } + + public void loadStats() { + try { + totalSuccesses = auditRestClient.getSuccessCount(null, null); + totalFailures = auditRestClient.getFailureCount(null, null); + + // Assuming we display active users for default realm + // Ideally we would have an endpoint for global stats + activeUsers = 0; // Placeholder until we have count endpoint in UserRestClient or general stats + + try { + Map health = healthRestClient.getServiceStatus(); + systemHealthy = "UP".equals(health.get("status")); + } catch (Exception e) { + systemHealthy = false; + } + + } catch (Exception e) { + log.error("Error loading stats", e); + } + } + + public void createBarModel() { + barModel = new BarChartModel(); + ChartData data = new ChartData(); + + BarChartDataSet barDataSet = new BarChartDataSet(); + barDataSet.setLabel("Activités par type"); + + List values = new ArrayList<>(); + List labels = new ArrayList<>(); + List bgColor = new ArrayList<>(); + List borderColor = new ArrayList<>(); + + try { + Map stats = auditRestClient.getActionStatistics(null, null); + + for (Map.Entry entry : stats.entrySet()) { + labels.add(entry.getKey().name()); + values.add(entry.getValue()); + bgColor.add("rgba(75, 192, 192, 0.2)"); + borderColor.add("rgb(75, 192, 192)"); + } + } catch (Exception e) { + log.error("Error loading chart data", e); + } + + barDataSet.setData(values); + barDataSet.setBackgroundColor(bgColor); + barDataSet.setBorderColor(borderColor); + barDataSet.setBorderWidth(1); + + data.addChartDataSet(barDataSet); + data.setLabels(labels); + + barModel.setData(data); + + // Options + BarChartOptions options = new BarChartOptions(); + CartesianScales cScales = new CartesianScales(); + CartesianLinearAxes linearAxes = new CartesianLinearAxes(); + linearAxes.setOffset(true); + cScales.addYAxesData(linearAxes); + options.setScales(cScales); + + Title title = new Title(); + title.setDisplay(true); + title.setText("Audit Actions"); + options.setTitle(title); + + barModel.setOptions(options); + } +} diff --git a/src/main/java/dev/lions/user/manager/client/view/RealmAssignmentBean.java b/src/main/java/dev/lions/user/manager/client/view/RealmAssignmentBean.java new file mode 100644 index 0000000..1cc9780 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/RealmAssignmentBean.java @@ -0,0 +1,394 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.RealmAssignmentServiceClient; +import dev.lions.user.manager.client.service.RealmServiceClient; +import dev.lions.user.manager.client.service.UserServiceClient; +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import jakarta.annotation.PostConstruct; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Bean JSF pour la gestion des affectations de realms + * Permet d'assigner des realms aux utilisateurs pour le contrôle d'accès multi-tenant + * + * @author Lions User Manager + * @version 1.0.0 + */ +@Named("realmAssignmentBean") +@ViewScoped +@Data +public class RealmAssignmentBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(RealmAssignmentBean.class.getName()); + + @Inject + @RestClient + private RealmAssignmentServiceClient realmAssignmentServiceClient; + + @Inject + @RestClient + private UserServiceClient userServiceClient; + + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + + @Inject + private UserSessionBean userSessionBean; + + // Listes + private List assignments = new ArrayList<>(); + private List availableUsers = new ArrayList<>(); + private List availableRealms = new ArrayList<>(); + private RealmAssignmentDTO selectedAssignment; + + // Pour la création/édition + private RealmAssignmentDTO newAssignment = RealmAssignmentDTO.builder() + .active(true) + .temporaire(false) + .build(); + private String selectedUserId; + private String selectedRealmName; + + // Filtres + private String filterUserName; + private String filterRealmName; + + @PostConstruct + public void init() { + LOGGER.info("Initialisation de RealmAssignmentBean"); + + // Vérifier si l'utilisateur est admin + if (!userSessionBean.hasRole("admin")) { + addErrorMessage("Accès refusé: Cette fonctionnalité est réservée aux administrateurs"); + return; + } + + loadAssignments(); + loadAvailableUsers(); + loadAvailableRealms(); + } + + /** + * Charger toutes les affectations + */ + public void loadAssignments() { + try { + LOGGER.info("Chargement de toutes les affectations de realms"); + assignments = realmAssignmentServiceClient.getAllAssignments(); + LOGGER.info("Chargement réussi: " + assignments.size() + " affectation(s) trouvée(s)"); + + if (assignments.isEmpty()) { + addInfoMessage("Aucune affectation de realm configurée"); + } + } catch (Exception e) { + String errorMsg = "Erreur lors du chargement des affectations: " + e.getMessage(); + LOGGER.severe(errorMsg); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); + addErrorMessage(errorMsg); + assignments = new ArrayList<>(); + } + } + + /** + * Charger les utilisateurs disponibles + */ + public void loadAvailableUsers() { + try { + LOGGER.info("Chargement des utilisateurs disponibles"); + // Charger les utilisateurs du realm lions-user-manager (page 0, 100 utilisateurs max) + UserSearchResultDTO result = userServiceClient.getAllUsers("lions-user-manager", 0, 100); + availableUsers = result != null && result.getUsers() != null ? result.getUsers() : new ArrayList<>(); + LOGGER.info("Chargement réussi: " + availableUsers.size() + " utilisateur(s) disponible(s)"); + } catch (Exception e) { + String errorMsg = "Erreur lors du chargement des utilisateurs: " + e.getMessage(); + LOGGER.severe(errorMsg); + addErrorMessage(errorMsg); + availableUsers = new ArrayList<>(); + } + } + + /** + * Charger les realms disponibles depuis Keycloak + */ + public void loadAvailableRealms() { + try { + LOGGER.info("Chargement des realms disponibles depuis Keycloak"); + List realms = realmServiceClient.getAllRealms(); + + if (realms == null || realms.isEmpty()) { + LOGGER.warning("Aucun realm trouvé dans Keycloak"); + availableRealms = Collections.emptyList(); + addInfoMessage("Aucun realm disponible dans Keycloak"); + } else { + availableRealms = new ArrayList<>(realms); + LOGGER.info("Realms disponibles chargés depuis Keycloak: " + availableRealms.size()); + } + } catch (Exception e) { + String errorMsg = "Erreur lors du chargement des realms depuis Keycloak: " + e.getMessage(); + LOGGER.severe(errorMsg); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); + addErrorMessage(errorMsg); + // Fallback: liste vide plutôt que des données fictives + availableRealms = Collections.emptyList(); + } + } + + /** + * Assigner un realm à un utilisateur + */ + public void assignRealm() { + try { + if (selectedUserId == null || selectedUserId.isEmpty()) { + addErrorMessage("Veuillez sélectionner un utilisateur"); + return; + } + + if (selectedRealmName == null || selectedRealmName.isEmpty()) { + addErrorMessage("Veuillez sélectionner un realm"); + return; + } + + // Trouver l'utilisateur sélectionné + UserDTO selectedUser = availableUsers.stream() + .filter(u -> u.getId().equals(selectedUserId)) + .findFirst() + .orElse(null); + + if (selectedUser == null) { + addErrorMessage("Utilisateur introuvable"); + return; + } + + // Construire l'assignation + RealmAssignmentDTO assignment = RealmAssignmentDTO.builder() + .userId(selectedUserId) + .username(selectedUser.getUsername()) + .email(selectedUser.getEmail()) + .realmName(selectedRealmName) + .isSuperAdmin(false) + .assignedAt(LocalDateTime.now()) + .assignedBy(userSessionBean.getUsername()) + .raison(newAssignment.getRaison()) + .commentaires(newAssignment.getCommentaires()) + .temporaire(newAssignment.getTemporaire() != null && newAssignment.getTemporaire()) + .dateExpiration(newAssignment.getDateExpiration()) + .active(true) + .build(); + + LOGGER.info("Assignation du realm " + selectedRealmName + " à l'utilisateur " + selectedUser.getUsername()); + + RealmAssignmentDTO created = realmAssignmentServiceClient.assignRealmToUser(assignment); + + addSuccessMessage("Realm '" + selectedRealmName + "' assigné avec succès à " + selectedUser.getUsername()); + resetForm(); + loadAssignments(); + + } catch (Exception e) { + String errorMsg = "Erreur lors de l'assignation: " + e.getMessage(); + LOGGER.severe(errorMsg); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); + addErrorMessage(errorMsg); + } + } + + /** + * Révoquer l'accès d'un utilisateur à un realm + */ + public void revokeAssignment(RealmAssignmentDTO assignment) { + try { + if (assignment == null) { + addErrorMessage("Assignation invalide"); + return; + } + + LOGGER.info("Révocation du realm " + assignment.getRealmName() + " pour l'utilisateur " + assignment.getUsername()); + + realmAssignmentServiceClient.revokeRealmFromUser(assignment.getUserId(), assignment.getRealmName()); + + addSuccessMessage("Accès révoqué pour " + assignment.getUsername() + " au realm '" + assignment.getRealmName() + "'"); + loadAssignments(); + + } catch (Exception e) { + String errorMsg = "Erreur lors de la révocation: " + e.getMessage(); + LOGGER.severe(errorMsg); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); + addErrorMessage(errorMsg); + } + } + + /** + * Désactiver une assignation + */ + public void deactivateAssignment(RealmAssignmentDTO assignment) { + try { + if (assignment == null || assignment.getId() == null) { + addErrorMessage("Assignation invalide"); + return; + } + + LOGGER.info("Désactivation de l'assignation " + assignment.getId()); + + realmAssignmentServiceClient.deactivateAssignment(assignment.getId()); + + addSuccessMessage("Assignation désactivée"); + loadAssignments(); + + } catch (Exception e) { + String errorMsg = "Erreur lors de la désactivation: " + e.getMessage(); + LOGGER.severe(errorMsg); + addErrorMessage(errorMsg); + } + } + + /** + * Activer une assignation + */ + public void activateAssignment(RealmAssignmentDTO assignment) { + try { + if (assignment == null || assignment.getId() == null) { + addErrorMessage("Assignation invalide"); + return; + } + + LOGGER.info("Activation de l'assignation " + assignment.getId()); + + realmAssignmentServiceClient.activateAssignment(assignment.getId()); + + addSuccessMessage("Assignation activée"); + loadAssignments(); + + } catch (Exception e) { + String errorMsg = "Erreur lors de l'activation: " + e.getMessage(); + LOGGER.severe(errorMsg); + addErrorMessage(errorMsg); + } + } + + /** + * Définir un utilisateur comme super admin + */ + public void setSuperAdmin(String userId, boolean superAdmin) { + try { + if (userId == null || userId.isEmpty()) { + addErrorMessage("Utilisateur invalide"); + return; + } + + UserDTO user = availableUsers.stream() + .filter(u -> u.getId().equals(userId)) + .findFirst() + .orElse(null); + + String username = user != null ? user.getUsername() : userId; + + LOGGER.info("Définition de " + username + " comme super admin: " + superAdmin); + + realmAssignmentServiceClient.setSuperAdmin(userId, superAdmin); + + if (superAdmin) { + addSuccessMessage(username + " est maintenant super admin (peut gérer tous les realms)"); + } else { + addSuccessMessage("Privilèges super admin retirés pour " + username); + } + + loadAssignments(); + + } catch (Exception e) { + String errorMsg = "Erreur lors de la modification du statut super admin: " + e.getMessage(); + LOGGER.severe(errorMsg); + addErrorMessage(errorMsg); + } + } + + /** + * Réinitialiser le formulaire + */ + public void resetForm() { + newAssignment = RealmAssignmentDTO.builder() + .active(true) + .temporaire(false) + .build(); + selectedUserId = null; + selectedRealmName = null; + } + + /** + * Obtenir les assignations filtrées + */ + public List getFilteredAssignments() { + if (filterUserName == null && filterRealmName == null) { + return assignments; + } + + return assignments.stream() + .filter(a -> { + boolean matchUser = filterUserName == null || filterUserName.isEmpty() || + (a.getUsername() != null && a.getUsername().toLowerCase().contains(filterUserName.toLowerCase())); + + boolean matchRealm = filterRealmName == null || filterRealmName.isEmpty() || + (a.getRealmName() != null && a.getRealmName().toLowerCase().contains(filterRealmName.toLowerCase())); + + return matchUser && matchRealm; + }) + .collect(Collectors.toList()); + } + + /** + * Obtenir le nombre total d'assignations + */ + public int getTotalAssignments() { + return assignments != null ? assignments.size() : 0; + } + + /** + * Obtenir le nombre d'assignations actives + */ + public long getActiveAssignmentsCount() { + return assignments.stream() + .filter(RealmAssignmentDTO::isActive) + .count(); + } + + /** + * Obtenir le nombre de super admins + */ + public long getSuperAdminsCount() { + return assignments.stream() + .filter(RealmAssignmentDTO::isSuperAdmin) + .count(); + } + + // Méthodes utilitaires pour les messages + private void addSuccessMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message)); + } + + private void addErrorMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); + } + + private void addInfoMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message)); + } +} diff --git a/src/main/java/dev/lions/user/manager/client/view/RoleGestionBean.java b/src/main/java/dev/lions/user/manager/client/view/RoleGestionBean.java index 82b7a54..2f072c5 100644 --- a/src/main/java/dev/lions/user/manager/client/view/RoleGestionBean.java +++ b/src/main/java/dev/lions/user/manager/client/view/RoleGestionBean.java @@ -1,5 +1,6 @@ package dev.lions.user.manager.client.view; +import dev.lions.user.manager.client.service.RealmServiceClient; import dev.lions.user.manager.client.service.RoleServiceClient; import dev.lions.user.manager.dto.role.RoleAssignmentDTO; import dev.lions.user.manager.dto.role.RoleDTO; @@ -16,6 +17,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.logging.Logger; @@ -37,6 +39,13 @@ public class RoleGestionBean implements Serializable { @RestClient private RoleServiceClient roleServiceClient; + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + + @Inject + private UserSessionBean userSessionBean; + // Liste des rôles private List realmRoles = new ArrayList<>(); private List clientRoles = new ArrayList<>(); @@ -48,7 +57,8 @@ public class RoleGestionBean implements Serializable { private boolean editMode = false; // Filtres - private String realmName = "master"; + // Par défaut, utiliser le realm lions-user-manager où les rôles métier sont configurés + private String realmName = "lions-user-manager"; private String clientName; private TypeRole selectedTypeRole; private String roleSearchText; @@ -101,7 +111,7 @@ public class RoleGestionBean implements Serializable { } try { - clientRoles = roleServiceClient.getAllClientRoles(realmName, clientName); + clientRoles = roleServiceClient.getAllClientRoles(clientName, realmName); updateAllRoles(); LOGGER.info("Chargement de " + clientRoles.size() + " rôles Client"); } catch (Exception e) { @@ -144,7 +154,7 @@ public class RoleGestionBean implements Serializable { } try { - RoleDTO created = roleServiceClient.createClientRole(newRole, realmName, clientName); + RoleDTO created = roleServiceClient.createClientRole(clientName, newRole, realmName); addSuccessMessage("Rôle Client créé avec succès: " + created.getName()); resetForm(); loadClientRoles(); @@ -178,7 +188,7 @@ public class RoleGestionBean implements Serializable { } try { - roleServiceClient.deleteClientRole(roleName, realmName, clientName); + roleServiceClient.deleteClientRole(clientName, roleName, realmName); addSuccessMessage("Rôle Client supprimé avec succès"); loadClientRoles(); } catch (Exception e) { @@ -192,14 +202,9 @@ public class RoleGestionBean implements Serializable { */ public void assignRoleToUser(String userId, String roleName) { try { - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(userId) - .roleNames(List.of(roleName)) - .typeRole(TypeRole.REALM_ROLE) - .realmName(realmName) - .build(); - - roleServiceClient.assignRoleToUser(assignment); + RoleServiceClient.RoleAssignmentRequest request = new RoleServiceClient.RoleAssignmentRequest(); + request.roleNames = List.of(roleName); + roleServiceClient.assignRealmRolesToUser(userId, realmName, request); addSuccessMessage("Rôle attribué avec succès"); } catch (Exception e) { LOGGER.severe("Erreur lors de l'attribution: " + e.getMessage()); @@ -212,14 +217,9 @@ public class RoleGestionBean implements Serializable { */ public void revokeRoleFromUser(String userId, String roleName) { try { - RoleAssignmentDTO assignment = RoleAssignmentDTO.builder() - .userId(userId) - .roleNames(List.of(roleName)) - .typeRole(TypeRole.REALM_ROLE) - .realmName(realmName) - .build(); - - roleServiceClient.revokeRoleFromUser(assignment); + RoleServiceClient.RoleAssignmentRequest request = new RoleServiceClient.RoleAssignmentRequest(); + request.roleNames = List.of(roleName); + roleServiceClient.revokeRealmRolesFromUser(userId, realmName, request); addSuccessMessage("Rôle révoqué avec succès"); } catch (Exception e) { LOGGER.severe("Erreur lors de la révocation: " + e.getMessage()); @@ -283,18 +283,46 @@ public class RoleGestionBean implements Serializable { } /** - * Charger les realms disponibles + * Charger les realms disponibles depuis Keycloak en fonction des permissions de l'utilisateur */ private void loadRealms() { try { - // Pour l'instant, utiliser les realms de la configuration - // TODO: Implémenter la récupération des realms depuis Keycloak via un endpoint API - availableRealms = List.of("master", "lions-user-manager", "btpxpress", "test-realm"); - LOGGER.info("Realms disponibles chargés: " + availableRealms.size()); + // Récupérer tous les realms depuis Keycloak + List allRealms = realmServiceClient.getAllRealms(); + + if (allRealms == null || allRealms.isEmpty()) { + LOGGER.warning("Aucun realm trouvé dans Keycloak"); + availableRealms = Collections.emptyList(); + return; + } + + List 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); + } + } } catch (Exception e) { - LOGGER.severe("Erreur lors du chargement des realms: " + e.getMessage()); - // Fallback sur une liste par défaut - availableRealms = List.of("master"); + LOGGER.severe("Erreur lors du chargement des realms depuis Keycloak: " + e.getMessage()); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); + // Fallback: liste vide plutôt que des données fictives + availableRealms = Collections.emptyList(); } } diff --git a/src/main/java/dev/lions/user/manager/client/view/RoleView.java b/src/main/java/dev/lions/user/manager/client/view/RoleView.java new file mode 100644 index 0000000..98de8f8 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/RoleView.java @@ -0,0 +1,98 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.api.RoleRestClient; +import dev.lions.user.manager.dto.role.RoleDTO; +import jakarta.annotation.PostConstruct; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Named +@ViewScoped +@Slf4j +public class RoleView implements Serializable { + + @Inject + @RestClient + RoleRestClient roleRestClient; + + @ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "test-realm") + String defaultRealm; + + @Getter + @Setter + private List roles; + + @Getter + @Setter + private RoleDTO selectedRole; + + @Getter + @Setter + private String selectedRealm; + + @PostConstruct + public void init() { + this.selectedRealm = defaultRealm; + this.selectedRole = new RoleDTO(); + loadRoles(); + } + + public void loadRoles() { + try { + roles = roleRestClient.getAllRealmRoles(selectedRealm); + } catch (Exception e) { + log.error("Error loading roles", e); + roles = new ArrayList<>(); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", "Impossible de charger les rôles")); + } + } + + public void openNew() { + this.selectedRole = new RoleDTO(); + } + + public void saveRole() { + try { + if (this.selectedRole.getId() == null) { + // Create + roleRestClient.createRealmRole(selectedRealm, this.selectedRole); + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Rôle créé")); + } else { + // Update + roleRestClient.updateRealmRole(this.selectedRole.getName(), selectedRealm, this.selectedRole); + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Rôle mis à jour")); + } + loadRoles(); + } catch (Exception e) { + log.error("Error saving role", e); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage())); + } + } + + public void deleteRole() { + try { + roleRestClient.deleteRealmRole(this.selectedRole.getName(), selectedRealm); + this.selectedRole = null; + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Rôle supprimé")); + loadRoles(); + } catch (Exception e) { + log.error("Error deleting role", e); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage())); + } + } +} diff --git a/src/main/java/dev/lions/user/manager/client/view/SessionMonitorBean.java b/src/main/java/dev/lions/user/manager/client/view/SessionMonitorBean.java new file mode 100644 index 0000000..c234f2a --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/SessionMonitorBean.java @@ -0,0 +1,160 @@ +package dev.lions.user.manager.client.view; + +import jakarta.enterprise.context.SessionScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.io.Serializable; +import java.time.Instant; +import java.time.Duration; +import java.util.logging.Logger; + +/** + * Bean de monitoring de session utilisateur en temps réel + * Calcule le temps restant avant expiration du token JWT + * + * @author Lions User Manager Team + * @version 1.0 + */ +@Named("sessionMonitor") +@SessionScoped +public class SessionMonitorBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(SessionMonitorBean.class.getName()); + + // Temps d'inactivité maximum en secondes (30 minutes par défaut) + private static final long DEFAULT_INACTIVITY_TIMEOUT = 1800; + + @Inject + private JsonWebToken jwt; + + private Instant lastActivityTime; + private long inactivityTimeout = DEFAULT_INACTIVITY_TIMEOUT; + + public SessionMonitorBean() { + this.lastActivityTime = Instant.now(); + } + + /** + * Met à jour le timestamp de la dernière activité + */ + public void updateActivity() { + this.lastActivityTime = Instant.now(); + } + + /** + * Calcule le temps d'inactivité en secondes + */ + public long getInactivitySeconds() { + if (lastActivityTime == null) { + lastActivityTime = Instant.now(); + return 0; + } + return Duration.between(lastActivityTime, Instant.now()).getSeconds(); + } + + /** + * Calcule le temps restant avant expiration en minutes + */ + public long getRemainingMinutes() { + long inactivitySeconds = getInactivitySeconds(); + long remainingSeconds = inactivityTimeout - inactivitySeconds; + + if (remainingSeconds < 0) { + return 0; + } + + return remainingSeconds / 60; + } + + /** + * Calcule le temps restant avant expiration en secondes (pour le timer) + */ + public long getRemainingSeconds() { + long inactivitySeconds = getInactivitySeconds(); + long remainingSeconds = inactivityTimeout - inactivitySeconds; + + return Math.max(0, remainingSeconds); + } + + /** + * Formate le temps restant en format mm:ss + */ + public String getFormattedRemainingTime() { + long totalSeconds = getRemainingSeconds(); + long minutes = totalSeconds / 60; + long seconds = totalSeconds % 60; + return String.format("%02d:%02d", minutes, seconds); + } + + /** + * Retourne le pourcentage de temps écoulé (pour une barre de progression) + */ + public int getSessionProgressPercent() { + long inactivitySeconds = getInactivitySeconds(); + if (inactivityTimeout == 0) return 0; + + int percent = (int) ((inactivitySeconds * 100) / inactivityTimeout); + return Math.min(100, Math.max(0, percent)); + } + + /** + * Vérifie si la session est proche de l'expiration (moins de 5 minutes) + */ + public boolean isSessionExpiringSoon() { + return getRemainingMinutes() <= 5; + } + + /** + * Vérifie si la session est expirée + */ + public boolean isSessionExpired() { + return getRemainingSeconds() == 0; + } + + /** + * Retourne la classe CSS pour l'indicateur de temps (couleur) + */ + public String getTimeIndicatorClass() { + long minutes = getRemainingMinutes(); + if (minutes <= 3) { + return "text-red-600 font-bold"; // Rouge critique + } else if (minutes <= 5) { + return "text-orange-600 font-semibold"; // Orange warning + } else if (minutes <= 10) { + return "text-yellow-600"; // Jaune attention + } else { + return "text-green-600"; // Vert OK + } + } + + /** + * Retourne l'icône appropriée selon le temps restant + */ + public String getTimeIndicatorIcon() { + long minutes = getRemainingMinutes(); + if (minutes <= 3) { + return "pi pi-exclamation-triangle"; + } else if (minutes <= 5) { + return "pi pi-clock"; + } else { + return "pi pi-check-circle"; + } + } + + // Getters et Setters + + public long getInactivityTimeout() { + return inactivityTimeout; + } + + public void setInactivityTimeout(long inactivityTimeout) { + this.inactivityTimeout = inactivityTimeout; + } + + public Instant getLastActivityTime() { + return lastActivityTime; + } +} diff --git a/src/main/java/dev/lions/user/manager/client/view/SettingsBean.java b/src/main/java/dev/lions/user/manager/client/view/SettingsBean.java new file mode 100644 index 0000000..a7a314e --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/SettingsBean.java @@ -0,0 +1,63 @@ +package dev.lions.user.manager.client.view; + +import jakarta.annotation.PostConstruct; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; + +import java.io.Serializable; +import java.util.logging.Logger; + +/** + * Bean JSF pour la page de paramètres + * + * @author Lions User Manager + * @version 1.0.0 + */ +@Named("settingsBean") +@ViewScoped +@Data +public class SettingsBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(SettingsBean.class.getName()); + + @Inject + private UserSessionBean userSessionBean; + + @Inject + private GuestPreferences guestPreferences; + + @PostConstruct + public void init() { + LOGGER.info("Initialisation de SettingsBean"); + } + + /** + * Sauvegarder les préférences + */ + public void savePreferences() { + try { + // Les préférences sont déjà sauvegardées dans GuestPreferences (SessionScoped) + addSuccessMessage("Préférences sauvegardées avec succès"); + } catch (Exception e) { + LOGGER.severe("Erreur lors de la sauvegarde: " + e.getMessage()); + addErrorMessage("Erreur lors de la sauvegarde: " + e.getMessage()); + } + } + + // Méthodes utilitaires + private void addSuccessMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message)); + } + + private void addErrorMessage(String message) { + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); + } +} + diff --git a/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java b/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java index f87a235..5a7263a 100644 --- a/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java +++ b/src/main/java/dev/lions/user/manager/client/view/UserCreationBean.java @@ -1,5 +1,6 @@ package dev.lions.user.manager.client.view; +import dev.lions.user.manager.client.service.RealmServiceClient; import dev.lions.user.manager.client.service.UserServiceClient; import dev.lions.user.manager.dto.user.UserDTO; import dev.lions.user.manager.enums.user.StatutUser; @@ -14,6 +15,7 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.logging.Logger; @@ -35,8 +37,13 @@ public class UserCreationBean implements Serializable { @RestClient private UserServiceClient userServiceClient; + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + private UserDTO newUser = UserDTO.builder().build(); - private String realmName = "master"; + // Par défaut, utiliser le realm lions-user-manager où les utilisateurs sont configurés + private String realmName = "lions-user-manager"; private String password; private String passwordConfirm; @@ -78,7 +85,10 @@ public class UserCreationBean implements Serializable { UserDTO createdUser = userServiceClient.createUser(newUser, realmName); // Définir le mot de passe - userServiceClient.resetPassword(createdUser.getId(), realmName, password); + UserServiceClient.PasswordResetRequest passwordRequest = new UserServiceClient.PasswordResetRequest(); + passwordRequest.password = password; + passwordRequest.temporary = true; + userServiceClient.resetPassword(createdUser.getId(), realmName, passwordRequest); addSuccessMessage("Utilisateur créé avec succès: " + createdUser.getUsername()); resetForm(); @@ -111,11 +121,31 @@ public class UserCreationBean implements Serializable { } /** - * Charger les realms disponibles + * Charger les realms disponibles depuis Keycloak */ private void loadRealms() { - // TODO: Implémenter la récupération des realms depuis Keycloak - availableRealms = List.of("master", "btpxpress", "unionflow"); + try { + LOGGER.info("Chargement des realms disponibles depuis Keycloak"); + List realms = realmServiceClient.getAllRealms(); + + if (realms == null || realms.isEmpty()) { + LOGGER.warning("Aucun realm trouvé dans Keycloak"); + availableRealms = Collections.emptyList(); + } else { + availableRealms = new ArrayList<>(realms); + LOGGER.info("Realms disponibles chargés depuis Keycloak: " + availableRealms.size()); + + // Définir le premier realm comme realm par défaut si aucun n'est sélectionné + if (!availableRealms.isEmpty() && (realmName == null || realmName.isEmpty())) { + realmName = availableRealms.get(0); + } + } + } 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: liste vide plutôt que des données fictives + availableRealms = Collections.emptyList(); + } } // Méthodes utilitaires diff --git a/src/main/java/dev/lions/user/manager/client/view/UserListBean.java b/src/main/java/dev/lions/user/manager/client/view/UserListBean.java index 2770aaa..d6b34ce 100644 --- a/src/main/java/dev/lions/user/manager/client/view/UserListBean.java +++ b/src/main/java/dev/lions/user/manager/client/view/UserListBean.java @@ -1,5 +1,6 @@ package dev.lions.user.manager.client.view; +import dev.lions.user.manager.client.service.RealmServiceClient; import dev.lions.user.manager.client.service.UserServiceClient; import dev.lions.user.manager.dto.user.UserDTO; import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; @@ -8,14 +9,17 @@ import dev.lions.user.manager.enums.user.StatutUser; import jakarta.annotation.PostConstruct; import jakarta.faces.application.FacesMessage; import jakarta.faces.context.FacesContext; +import jakarta.faces.event.ActionEvent; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; import lombok.Data; import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.primefaces.event.data.PageEvent; import java.io.Serializable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.logging.Logger; @@ -42,6 +46,13 @@ public class UserListBean implements Serializable { @RestClient private UserServiceClient userServiceClient; + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + + @Inject + private UserSessionBean userSessionBean; + // Propriétés pour la liste private List users = new ArrayList<>(); private UserDTO selectedUser; @@ -50,7 +61,8 @@ public class UserListBean implements Serializable { // Propriétés pour la recherche private UserSearchCriteriaDTO searchCriteria = UserSearchCriteriaDTO.builder().build(); private String searchText; - private String realmName = "master"; + // Par défaut, utiliser le realm lions-user-manager où les utilisateurs sont configurés + private String realmName = "lions-user-manager"; private StatutUser selectedStatut; // Propriétés pour la pagination @@ -130,16 +142,68 @@ public class UserListBean implements Serializable { loadUsers(); } + /** + * Gérer les événements de pagination du datatable + */ + public void onPageChange(PageEvent event) { + try { + int page = event.getPage(); + + currentPage = page; + + LOGGER.info("Changement de page: page=" + currentPage + ", rows=" + pageSize); + + // Recharger les données avec la nouvelle page + loadUsers(); + } catch (Exception e) { + LOGGER.severe("Erreur lors du changement de page: " + e.getMessage()); + addErrorMessage("Erreur lors du changement de page: " + e.getMessage()); + } + } + + /** + * Action pour activer un utilisateur (utilisé par le composant composite) + */ + public void activateUserAction(ActionEvent event) { + String userId = (String) event.getComponent().getAttributes().get("userId"); + if (userId != null) { + activateUser(userId); + } + } + + /** + * Action pour désactiver un utilisateur (utilisé par le composant composite) + */ + public void deactivateUserAction(ActionEvent event) { + String userId = (String) event.getComponent().getAttributes().get("userId"); + if (userId != null) { + deactivateUser(userId); + } + } + + /** + * Action pour supprimer un utilisateur (utilisé par le composant composite) + */ + public void deleteUserAction(ActionEvent event) { + String userId = (String) event.getComponent().getAttributes().get("userId"); + if (userId != null) { + deleteUser(userId); + } + } + /** * Activer un utilisateur */ public void activateUser(String userId) { try { + LOGGER.info("Activation de l'utilisateur: " + userId + " dans le realm: " + realmName); userServiceClient.activateUser(userId, realmName); addSuccessMessage("Utilisateur activé avec succès"); + // Recharger les données loadUsers(); } catch (Exception e) { LOGGER.severe("Erreur lors de l'activation: " + e.getMessage()); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); addErrorMessage("Erreur lors de l'activation: " + e.getMessage()); } } @@ -149,11 +213,14 @@ public class UserListBean implements Serializable { */ public void deactivateUser(String userId) { try { + LOGGER.info("Désactivation de l'utilisateur: " + userId + " dans le realm: " + realmName); userServiceClient.deactivateUser(userId, realmName); addSuccessMessage("Utilisateur désactivé avec succès"); + // Recharger les données loadUsers(); } catch (Exception e) { LOGGER.severe("Erreur lors de la désactivation: " + e.getMessage()); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); addErrorMessage("Erreur lors de la désactivation: " + e.getMessage()); } } @@ -163,11 +230,14 @@ public class UserListBean implements Serializable { */ public void deleteUser(String userId) { try { + LOGGER.info("Suppression de l'utilisateur: " + userId + " dans le realm: " + realmName); userServiceClient.deleteUser(userId, realmName); addSuccessMessage("Utilisateur supprimé avec succès"); + // Recharger les données loadUsers(); } catch (Exception e) { LOGGER.severe("Erreur lors de la suppression: " + e.getMessage()); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); addErrorMessage("Erreur lors de la suppression: " + e.getMessage()); } } @@ -230,11 +300,69 @@ public class UserListBean implements Serializable { } /** - * Charger les realms disponibles + * Rafraîchir les données + */ + public void refreshData() { + loadUsers(); + addSuccessMessage("Données rafraîchies"); + } + + /** + * Exporter vers CSV (placeholder) + */ + public void exportToCSV() { + addSuccessMessage("Fonctionnalité d'export en cours de développement"); + } + + /** + * Importer des utilisateurs (placeholder) + */ + public void importUsers() { + addSuccessMessage("Fonctionnalité d'import en cours de développement"); + } + + /** + * Charger les realms disponibles depuis Keycloak en fonction des permissions de l'utilisateur */ private void loadRealms() { - // TODO: Implémenter la récupération des realms depuis Keycloak - availableRealms = List.of("master", "btpxpress", "unionflow"); + try { + // Récupérer tous les realms depuis Keycloak + List allRealms = realmServiceClient.getAllRealms(); + + if (allRealms == null || allRealms.isEmpty()) { + LOGGER.warning("Aucun realm trouvé dans Keycloak"); + availableRealms = Collections.emptyList(); + return; + } + + List 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); + } + } + } 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: liste vide plutôt que des données fictives + availableRealms = Collections.emptyList(); + } } /** diff --git a/src/main/java/dev/lions/user/manager/client/view/UserProfilBean.java b/src/main/java/dev/lions/user/manager/client/view/UserProfilBean.java index 91033c0..1c50aba 100644 --- a/src/main/java/dev/lions/user/manager/client/view/UserProfilBean.java +++ b/src/main/java/dev/lions/user/manager/client/view/UserProfilBean.java @@ -32,9 +32,13 @@ public class UserProfilBean implements Serializable { @RestClient private UserServiceClient userServiceClient; + @Inject + private RoleGestionBean roleGestionBean; + private UserDTO user; private String userId; - private String realmName = "master"; + // Par défaut, utiliser le realm lions-user-manager où les utilisateurs sont configurés + private String realmName = "lions-user-manager"; private boolean editMode = false; // Pour la réinitialisation de mot de passe @@ -43,14 +47,29 @@ public class UserProfilBean implements Serializable { @PostConstruct public void init() { - // Récupérer l'ID depuis les paramètres de requête - userId = FacesContext.getCurrentInstance().getExternalContext() - .getRequestParameterMap().get("userId"); + // Récupérer l'ID et le realm depuis les paramètres de requête + FacesContext facesContext = FacesContext.getCurrentInstance(); + java.util.Map params = facesContext.getExternalContext().getRequestParameterMap(); + + userId = params.get("userId"); + String realmParam = params.get("realm"); + + if (realmParam != null && !realmParam.isEmpty()) { + realmName = realmParam; + } + + LOGGER.info("Initialisation de UserProfilBean avec userId: " + userId + ", realm: " + realmName); if (userId != null && !userId.isEmpty()) { loadUser(); + // Charger les rôles disponibles + if (roleGestionBean != null) { + roleGestionBean.setRealmName(realmName); + roleGestionBean.loadRealmRoles(); + } } else { - LOGGER.warning("Aucun userId fourni dans les paramètres"); + LOGGER.warning("Aucun userId fourni dans les paramètres de requête"); + addErrorMessage("Aucun ID d'utilisateur fourni. Accédez à cette page depuis la liste des utilisateurs."); } } @@ -60,7 +79,13 @@ public class UserProfilBean implements Serializable { public void loadUser() { try { user = userServiceClient.getUserById(userId, realmName); - LOGGER.info("Utilisateur chargé: " + user.getUsername()); + if (user != null) { + LOGGER.info("Utilisateur chargé: " + user.getUsername()); + } + } catch (dev.lions.user.manager.client.service.RestClientExceptionMapper.NotFoundException e) { + LOGGER.warning("Utilisateur non trouvé: " + userId); + addErrorMessage("Utilisateur non trouvé dans le realm " + realmName); + user = null; } catch (Exception e) { LOGGER.severe("Erreur lors du chargement de l'utilisateur: " + e.getMessage()); addErrorMessage("Erreur lors du chargement de l'utilisateur: " + e.getMessage()); @@ -87,11 +112,15 @@ public class UserProfilBean implements Serializable { */ public void updateUser() { try { + LOGGER.info("Mise à jour de l'utilisateur: " + userId + " dans le realm: " + realmName); user = userServiceClient.updateUser(userId, user, realmName); editMode = false; addSuccessMessage("Utilisateur mis à jour avec succès"); + // Recharger les données pour s'assurer qu'elles sont à jour + loadUser(); } catch (Exception e) { LOGGER.severe("Erreur lors de la mise à jour: " + e.getMessage()); + LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); addErrorMessage("Erreur lors de la mise à jour: " + e.getMessage()); } } @@ -111,7 +140,10 @@ public class UserProfilBean implements Serializable { } try { - userServiceClient.resetPassword(userId, realmName, newPassword); + UserServiceClient.PasswordResetRequest request = new UserServiceClient.PasswordResetRequest(); + request.password = newPassword; + request.temporary = true; + userServiceClient.resetPassword(userId, realmName, request); newPassword = null; newPasswordConfirm = null; addSuccessMessage("Mot de passe réinitialisé avec succès"); diff --git a/src/main/java/dev/lions/user/manager/client/view/UserSessionBean.java b/src/main/java/dev/lions/user/manager/client/view/UserSessionBean.java new file mode 100644 index 0000000..7258b18 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/UserSessionBean.java @@ -0,0 +1,452 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.RealmAssignmentServiceClient; +import io.quarkus.oidc.IdToken; +import io.quarkus.oidc.OidcSession; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.SessionScoped; +import jakarta.faces.context.ExternalContext; +import jakarta.faces.context.FacesContext; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Data; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +/** + * Bean de session pour gérer les informations de l'utilisateur connecté + * + * @author Lions User Manager + * @version 1.0.0 + */ +@Named("userSessionBean") +@SessionScoped +@Data +public class UserSessionBean implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(UserSessionBean.class.getName()); + + @Inject + SecurityIdentity securityIdentity; + + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + OidcSession oidcSession; + + @Inject + @RestClient + RealmAssignmentServiceClient realmAssignmentServiceClient; + + // Informations utilisateur + private String username; + private String email; + private String firstName; + private String lastName; + private String fullName; + private String initials; + + @PostConstruct + public void init() { + loadUserInfo(); + } + + /** + * Charger les informations utilisateur depuis le token OIDC + */ + public void loadUserInfo() { + try { + if (idToken != null && securityIdentity != null && !securityIdentity.isAnonymous()) { + // Username + username = idToken.getClaim("preferred_username"); + if (username == null || username.trim().isEmpty()) { + username = securityIdentity.getPrincipal().getName(); + } + + // Email + email = idToken.getClaim("email"); + if (email == null || email.trim().isEmpty()) { + email = username + "@lions.dev"; + } + + // Prénom et nom + firstName = idToken.getClaim("given_name"); + lastName = idToken.getClaim("family_name"); + + // Nom complet + fullName = idToken.getClaim("name"); + if (fullName == null || fullName.trim().isEmpty()) { + if (firstName != null && lastName != null) { + fullName = firstName + " " + lastName; + } else if (firstName != null) { + fullName = firstName; + } else if (lastName != null) { + fullName = lastName; + } else { + fullName = username; + } + } + + // Initiales pour l'avatar + initials = generateInitials(fullName); + + LOGGER.info("Informations utilisateur chargées: " + fullName + " (" + email + ")"); + } else { + // Valeurs par défaut si non authentifié + username = "Utilisateur"; + email = "utilisateur@lions.dev"; + fullName = "Utilisateur"; + initials = "U"; + } + } catch (Exception e) { + LOGGER.severe("Erreur lors du chargement des informations utilisateur: " + e.getMessage()); + username = "Utilisateur"; + email = "utilisateur@lions.dev"; + fullName = "Utilisateur"; + initials = "U"; + } + } + + /** + * Générer les initiales depuis le nom complet + */ + private String generateInitials(String name) { + if (name == null || name.trim().isEmpty()) { + return "U"; + } + + String[] parts = name.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) { + String part = parts[0]; + if (part.length() >= 2) { + return part.substring(0, 2).toUpperCase(); + } else { + return part.substring(0, 1).toUpperCase(); + } + } + return "U"; + } + + // Rôles + private java.util.Set roles; + private String primaryRole; + + /** + * Obtenir le rôle principal de l'utilisateur + */ + public String getPrimaryRole() { + if (primaryRole == null) { + primaryRole = getMainRole(); + } + return primaryRole; + } + + /** + * Obtenir tous les rôles de l'utilisateur + */ + public java.util.Set getRoles() { + if (roles == null) { + roles = new java.util.HashSet<>(); + try { + if (securityIdentity != null && securityIdentity.getRoles() != null) { + roles.addAll(securityIdentity.getRoles()); + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération des rôles: " + e.getMessage()); + } + if (roles.isEmpty()) { + roles.add("Utilisateur"); + } + } + return roles; + } + + /** + * Obtenir le rôle principal de l'utilisateur (méthode interne) + */ + private String getMainRole() { + try { + if (securityIdentity != null && securityIdentity.getRoles() != null && !securityIdentity.getRoles().isEmpty()) { + // Prioriser certains rôles + java.util.Set roleSet = securityIdentity.getRoles(); + if (roleSet.contains("admin")) { + return "Administrateur"; + } else if (roleSet.contains("user_manager")) { + return "Gestionnaire"; + } else if (roleSet.contains("user_viewer")) { + return "Consultant"; + } else { + return roleSet.iterator().next(); + } + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération du rôle: " + e.getMessage()); + } + return "Utilisateur"; + } + + /** + * Vérifier si l'utilisateur est authentifié + */ + public boolean isAuthenticated() { + return securityIdentity != null && !securityIdentity.isAnonymous(); + } + + /** + * Vérifier si l'utilisateur a un rôle spécifique + */ + public boolean hasRole(String role) { + try { + if (securityIdentity != null && securityIdentity.getRoles() != null) { + return securityIdentity.getRoles().contains(role); + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la vérification du rôle: " + e.getMessage()); + } + return false; + } + + // ==================== Gestion des realms autorisés ==================== + + /** + * Vérifie si l'utilisateur est super admin (peut gérer tous les realms) + */ + public boolean isSuperAdmin() { + try { + if (getSubject() == null || "Non disponible".equals(getSubject())) { + return false; + } + + RealmAssignmentServiceClient.AuthorizedRealmsResponse response = + realmAssignmentServiceClient.getAuthorizedRealms(getSubject()); + + return response != null && response.isSuperAdmin; + } catch (Exception e) { + LOGGER.warning("Erreur lors de la vérification du statut super admin: " + e.getMessage()); + // En cas d'erreur réseau, vérifier le rôle local + return hasRole("admin"); + } + } + + /** + * Récupère la liste des realms que l'utilisateur peut administrer + * Retourne une liste vide si l'utilisateur est super admin (peut tout gérer) + */ + public List getAuthorizedRealms() { + try { + if (getSubject() == null || "Non disponible".equals(getSubject())) { + return Collections.emptyList(); + } + + RealmAssignmentServiceClient.AuthorizedRealmsResponse response = + realmAssignmentServiceClient.getAuthorizedRealms(getSubject()); + + if (response == null) { + return Collections.emptyList(); + } + + // Si super admin, retourner liste vide (convention: peut tout gérer) + if (response.isSuperAdmin) { + return Collections.emptyList(); + } + + return response.realms != null ? response.realms : Collections.emptyList(); + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération des realms autorisés: " + e.getMessage()); + // En cas d'erreur, si admin local, retourner liste vide (peut tout gérer) + if (hasRole("admin")) { + return Collections.emptyList(); + } + return Collections.emptyList(); + } + } + + /** + * Vérifie si l'utilisateur peut administrer un realm spécifique + */ + public boolean canManageRealm(String realmName) { + try { + if (realmName == null || realmName.isBlank()) { + return false; + } + + if (getSubject() == null || "Non disponible".equals(getSubject())) { + return false; + } + + // Super admin peut tout gérer + if (isSuperAdmin()) { + return true; + } + + RealmAssignmentServiceClient.CheckResponse response = + realmAssignmentServiceClient.canManageRealm(getSubject(), realmName); + + return response != null && response.canManage; + } catch (Exception e) { + LOGGER.warning("Erreur lors de la vérification d'accès au realm " + realmName + ": " + e.getMessage()); + // En cas d'erreur réseau, vérifier le rôle local + return hasRole("admin"); + } + } + + /** + * Obtenir l'issuer du token OIDC + */ + public String getIssuer() { + try { + if (idToken != null) { + return idToken.getIssuer(); + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération de l'issuer: " + e.getMessage()); + } + return "Non disponible"; + } + + /** + * Obtenir le subject du token OIDC + */ + public String getSubject() { + try { + if (idToken != null) { + return idToken.getSubject(); + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération du subject: " + e.getMessage()); + } + return "Non disponible"; + } + + /** + * Obtenir le session ID + */ + public String getSessionId() { + try { + if (idToken != null) { + Object sid = idToken.getClaim("sid"); + if (sid != null) { + return sid.toString(); + } + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération du session ID: " + e.getMessage()); + } + return "Non disponible"; + } + + /** + * Obtenir le temps d'expiration du token + */ + public java.util.Date getExpirationTime() { + try { + if (idToken != null && idToken.getExpirationTime() > 0) { + return new java.util.Date(idToken.getExpirationTime() * 1000L); + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération de l'expiration: " + e.getMessage()); + } + return null; + } + + /** + * Obtenir le temps d'émission du token + */ + public java.util.Date getIssuedAt() { + try { + if (idToken != null && idToken.getIssuedAtTime() > 0) { + return new java.util.Date(idToken.getIssuedAtTime() * 1000L); + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération de l'émission: " + e.getMessage()); + } + return null; + } + + /** + * Obtenir l'audience du token + */ + public String getAudience() { + try { + if (idToken != null && idToken.getAudience() != null && !idToken.getAudience().isEmpty()) { + return String.join(", ", idToken.getAudience()); + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération de l'audience: " + e.getMessage()); + } + return "Non disponible"; + } + + /** + * Obtenir l'authorized party (azp) + */ + public String getAuthorizedParty() { + try { + if (idToken != null) { + Object azp = idToken.getClaim("azp"); + if (azp != null) { + return azp.toString(); + } + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la récupération de l'authorized party: " + e.getMessage()); + } + return "Non disponible"; + } + + /** + * Vérifier si l'email est vérifié + */ + public boolean isEmailVerified() { + try { + if (idToken != null) { + Boolean emailVerified = idToken.getClaim("email_verified"); + return emailVerified != null && emailVerified; + } + } catch (Exception e) { + LOGGER.warning("Erreur lors de la vérification de l'email: " + e.getMessage()); + } + return false; + } + + /** + * Déconnexion OIDC + * Redirige vers l'endpoint de logout Quarkus qui gère la déconnexion Keycloak + */ + public String logout() { + try { + LOGGER.info("Déconnexion de l'utilisateur: " + fullName); + + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + + // Invalider la session HTTP locale + externalContext.invalidateSession(); + + // Rediriger vers l'endpoint de logout OIDC de Quarkus + // Quarkus gère automatiquement la redirection vers Keycloak pour la déconnexion complète + String logoutUrl = "/auth/logout"; + externalContext.redirect(logoutUrl); + facesContext.responseComplete(); + + return null; + } catch (Exception e) { + LOGGER.severe("Erreur lors de la déconnexion: " + e.getMessage()); + // En cas d'erreur, rediriger vers la page d'accueil + return "/?faces-redirect=true"; + } + } +} + diff --git a/src/main/java/dev/lions/user/manager/client/view/UserView.java b/src/main/java/dev/lions/user/manager/client/view/UserView.java new file mode 100644 index 0000000..61c834c --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/view/UserView.java @@ -0,0 +1,177 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.api.UserRestClient; +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import jakarta.annotation.PostConstruct; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.ExternalContext; +import jakarta.faces.context.FacesContext; +import jakarta.faces.view.ViewScoped; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.primefaces.model.FilterMeta; +import org.primefaces.model.LazyDataModel; +import org.primefaces.model.SortMeta; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +@Named +@ViewScoped +@Slf4j +public class UserView implements Serializable { + + @Inject + @RestClient + UserRestClient userRestClient; + + @ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "test-realm") + String defaultRealm; + + @Getter + @Setter + private LazyDataModel users; + + @Getter + @Setter + private UserDTO selectedUser; + + @Getter + @Setter + private String selectedRealm; + + @Getter + @Setter + private String searchTerm; + + @PostConstruct + public void init() { + this.selectedRealm = defaultRealm; + this.selectedUser = new UserDTO(); // Initialize to avoid NPE in dialogs before selection + + users = new LazyDataModel() { + @Override + public int count(Map filterBy) { + // Simplified count logic reusing search API + try { + return (int) userRestClient + .searchUsers(selectedRealm, searchTerm, null, null, null, null, null, 0, 1).getTotalCount() + .intValue(); + } catch (Exception e) { + log.error("Error counting users", e); + return 0; + } + } + + @Override + public List load(int first, int pageSize, Map sortBy, + Map filterBy) { + try { + int page = first / pageSize; + UserSearchResultDTO result = userRestClient.searchUsers(selectedRealm, searchTerm, null, null, null, + null, null, page, pageSize); + setRowCount(result.getTotalCount().intValue()); + return result.getUsers(); + } catch (Exception e) { + log.error("Error loading users", e); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur de chargement", e.getMessage())); + return List.of(); + } + } + + @Override + public UserDTO getRowData(String rowKey) { + // Not ideal for lazy model, but needed for selection sometimes if not using + // rowDataWrapper + // Assuming ID is rowKey + try { + return userRestClient.getUserById(rowKey, selectedRealm); + } catch (Exception e) { + return null; + } + } + + @Override + public String getRowKey(UserDTO user) { + return user.getId(); + } + }; + } + + public void openNew() { + this.selectedUser = new UserDTO(); + this.selectedUser.setEnabled(true); + } + + public void saveUser() { + try { + if (this.selectedUser.getId() == null) { + // Create + // Password handling: assume temporary password is set in UI + if (this.selectedUser.getTemporaryPassword() == null + || this.selectedUser.getTemporaryPassword().isBlank()) { + // Generate or require password logic here. For now, let's assume UI requires + // it. + } + userRestClient.createUser(selectedRealm, this.selectedUser); + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Utilisateur créé")); + } else { + // Update + userRestClient.updateUser(this.selectedUser.getId(), selectedRealm, this.selectedUser); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage("Succès", "Utilisateur mis à jour")); + } + // PrimeFaces.current().executeScript("PF('manageUserDialog').hide()"); // + // Handled in xhtml via oncomplete + } catch (Exception e) { + log.error("Error saving user", e); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage())); + } + } + + public void deleteUser() { + try { + userRestClient.deleteUser(this.selectedUser.getId(), selectedRealm, false); + this.selectedUser = null; + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage("Succès", "Utilisateur supprimé (soft delete)")); + } catch (Exception e) { + log.error("Error deleting user", e); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage())); + } + } + + public void downloadCSV() { + try { + String csvContent = userRestClient.exportUsersToCSV(selectedRealm); + + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + + externalContext.setResponseContentType("text/csv"); + externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"users_export.csv\""); + + OutputStream output = externalContext.getResponseOutputStream(); + output.write(csvContent.getBytes(StandardCharsets.UTF_8)); + + facesContext.responseComplete(); + } catch (IOException e) { + log.error("Error exporting CSV", e); + FacesContext.getCurrentInstance().addMessage(null, + new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur Export", e.getMessage())); + } + } +} diff --git a/src/main/resources/META-INF/faces-config.xml b/src/main/resources/META-INF/faces-config.xml index 2516c8e..c4f7f78 100644 --- a/src/main/resources/META-INF/faces-config.xml +++ b/src/main/resources/META-INF/faces-config.xml @@ -18,7 +18,9 @@ * - + Page d'accueil / Dashboard userManagerDashboardPage @@ -26,7 +28,16 @@ - + + Navigation directe vers dashboard + /pages/user-manager/dashboard + /pages/user-manager/dashboard.xhtml + + + + Page de liste des utilisateurs userListPage @@ -34,6 +45,13 @@ + + Navigation directe vers liste utilisateurs + /pages/user-manager/users/list + /pages/user-manager/users/list.xhtml + + + Page de création d'utilisateur userCreatePage @@ -41,6 +59,13 @@ + + Navigation directe vers création utilisateur + /pages/user-manager/users/create + /pages/user-manager/users/create.xhtml + + + Page de profil utilisateur userProfilePage @@ -48,6 +73,27 @@ + + Navigation directe vers profil utilisateur + /pages/user-manager/users/profile + /pages/user-manager/users/profile.xhtml + + + + + Page de visualisation d'un utilisateur spécifique + userViewPage + /pages/user-manager/users/view.xhtml + + + + + Navigation directe vers visualisation utilisateur + /pages/user-manager/users/view + /pages/user-manager/users/view.xhtml + + + Page d'édition utilisateur userEditPage @@ -55,7 +101,16 @@ - + + Navigation directe vers édition utilisateur + /pages/user-manager/users/edit + /pages/user-manager/users/edit.xhtml + + + + Page de liste des rôles roleListPage @@ -63,6 +118,13 @@ + + Navigation directe vers liste rôles + /pages/user-manager/roles/list + /pages/user-manager/roles/list.xhtml + + + Page d'attribution de rôles roleAssignPage @@ -70,7 +132,16 @@ - + + Navigation directe vers attribution rôles + /pages/user-manager/roles/assign + /pages/user-manager/roles/assign.xhtml + + + + Page de journal d'audit auditLogsPage @@ -78,7 +149,16 @@ - + + Navigation directe vers journal d'audit + /pages/user-manager/audit/logs + /pages/user-manager/audit/logs.xhtml + + + + Page de dashboard synchronisation syncDashboardPage @@ -86,6 +166,30 @@ + + Navigation directe vers dashboard synchronisation + /pages/user-manager/sync/dashboard + /pages/user-manager/sync/dashboard.xhtml + + + + + + Page de paramètres utilisateur + settingsPage + /pages/user-manager/settings.xhtml + + + + + Navigation directe vers paramètres + /pages/user-manager/settings + /pages/user-manager/settings.xhtml + + + diff --git a/src/main/resources/META-INF/resources/index.html b/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..c85452d --- /dev/null +++ b/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,982 @@ + + + + + + Lions User Manager - Plateforme de Gestion IAM Centralisée + + + + + + + + + + + + + + + + +
+
+ +
+ +
+ Votre session a expiré pour des raisons de sécurité. Veuillez vous reconnecter pour accéder à la plateforme. +
+
+ +
+
+ + Plateforme IAM Centralisée +
+ +

Gérez vos utilisateurs Keycloak en toute simplicité

+ +

+ Une interface moderne et intuitive pour administrer vos identités, rôles et permissions à travers tous vos royaumes Keycloak. Sécurisé, performant, professionnel. +

+ + +
+
+
+ + +
+
+
+
0
+
Utilisateurs gérés
+
+
+
0
+
Royaumes actifs
+
+
+
99.9%
+
Disponibilité
+
+
+
0
+
Support 24/7
+
+
+
+ + +
+
+ Fonctionnalités Métier +

Tout ce dont vous avez besoin pour gérer vos identités

+

Une suite complète d'outils pour simplifier l'administration de votre infrastructure IAM.

+
+ +
+ +
+
+ +
+

Gestion des utilisateurs

+

+ Créez, modifiez et supprimez des utilisateurs en quelques clics. Interface intuitive avec recherche avancée et filtrage en temps réel. +

+
    +
  • Import/Export CSV massif
  • +
  • Recherche multi-critères
  • +
  • Modification par lot
  • +
+
+ + +
+
+ +
+

Attribution des rôles

+

+ Gérez les permissions de manière granulaire avec un système de rôles flexible et sécurisé conforme aux standards RBAC. +

+
    +
  • Gestion RBAC complète
  • +
  • Hiérarchie de rôles
  • +
  • Permissions dynamiques
  • +
+
+ + +
+
+ +
+

Audit & Analytics

+

+ Suivez l'activité de vos utilisateurs avec des tableaux de bord interactifs et des rapports détaillés en temps réel. +

+
    +
  • Logs d'authentification
  • +
  • Rapports personnalisés
  • +
  • Alertes de sécurité
  • +
+
+ + +
+
+ +
+

Synchronisation

+

+ Intégration transparente avec vos systèmes existants via API RESTful sécurisée et webhooks en temps réel. +

+
    +
  • API REST complète
  • +
  • Webhooks événementiels
  • +
  • Connecteurs pré-configurés
  • +
+
+ + +
+
+ +
+

Sécurité avancée

+

+ Protection multi-niveaux avec chiffrement end-to-end, authentification multi-facteurs et audit de sécurité complet. +

+
    +
  • MFA/2FA obligatoire
  • +
  • Chiffrement AES-256
  • +
  • SOC 2 Type II conforme
  • +
+
+ + +
+
+ +
+

Multi-tenant

+

+ Gérez plusieurs organisations et royaumes depuis une seule interface avec isolation complète des données. +

+
    +
  • Isolation par royaume
  • +
  • Personnalisation par org
  • +
  • Délégation d'administration
  • +
+
+
+
+ + +
+
+

Prêt à transformer votre gestion IAM ?

+

+ Rejoignez des centaines d'entreprises qui font confiance à Lions User Manager pour sécuriser et simplifier leur infrastructure d'identité. +

+ + + Accéder à la plateforme maintenant + +
+
+ + +
+ +
+ + + + + diff --git a/src/main/resources/META-INF/resources/index.xhtml b/src/main/resources/META-INF/resources/index.xhtml deleted file mode 100644 index 03bdbf0..0000000 --- a/src/main/resources/META-INF/resources/index.xhtml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - Lions User Manager - Gestion des Utilisateurs Keycloak - - - - - - - - -
-
-
- -

Lions User Manager

-

Gestion centralisée des utilisateurs Keycloak

- -
- - - - - - - - - - - -
- -
-

Version 1.0.0

-

Module réutilisable pour l'écosystème LionsDev

-
-
-
-
-
- - - diff --git a/src/main/resources/META-INF/resources/pages/admin/realm-assignments.xhtml b/src/main/resources/META-INF/resources/pages/admin/realm-assignments.xhtml new file mode 100644 index 0000000..7bdfc77 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/admin/realm-assignments.xhtml @@ -0,0 +1,349 @@ + + + + Affectation des Realms - Lions User Manager + + + +
+ +
+
+
+
+ +
+

Affectation des Realms

+

Gérer les permissions d'administration par realm (contrôle multi-tenant)

+
+
+ +
+
+
+ + +
+
+
+
+
Total Affectations
+
#{realmAssignmentBean.totalAssignments}
+
+
+ +
+
+ Assignations configurées +
+
+ +
+
+
+
+
Affectations Actives
+
#{realmAssignmentBean.activeAssignmentsCount}
+
+
+ +
+
+ En cours de validité +
+
+ +
+
+
+
+
Super Admins
+
#{realmAssignmentBean.superAdminsCount}
+
+
+ +
+
+ Peuvent gérer tous les realms +
+
+ + +
+
+
+
Affectations Actuelles
+ +
+ + + + + + + + + +
+
+ +
+
+
#{assignment.username}
+ #{assignment.email} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + +
+
+ + + + + +
+ +
+ + + + + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+ +
+ + +
+ +
+
+
+ +
+
Information
+ + L'utilisateur pourra administrer uniquement le realm assigné. + Pour accorder l'accès à tous les realms, utilisez le statut Super Admin. + +
+
+
+
+ +
+
+ + +
+
+
+
+
+ + + + + + +
+ +
diff --git a/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml index 3ab6763..70ac1e9 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/audit/logs.xhtml @@ -4,176 +4,420 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" 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"> Journal d'Audit - Lions User Manager - - - - - - - -
- - - - - - -
-
-
-
- - -
-
- - - - - - -
-
- - - - - - -
-
- - - - - - -
-
- - - - - - -
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
-
-
- - -
- -
Logs d'Audit
-
- - - - - - - - -

Aucun log d'audit trouvé

-
-
- - -
- - Affichage de #{auditConsultationBean.currentPage * auditConsultationBean.pageSize + 1} - à #{auditConsultationBean.currentPage * auditConsultationBean.pageSize + auditConsultationBean.auditLogs.size()} - sur #{auditConsultationBean.totalRecords} - -
- - +
+ +
+
+
+
+ +
+

Journal d'Audit

+

Consultation des logs d'audit et statistiques système

+
+
+ + +
- +
+ + +
+
Statistiques d'Audit
+
+ + +
+
+
+
+
Total Actions
+
#{auditConsultationBean.totalRecords}
+
+
+ +
+
+
+ + Actions enregistrées +
+
+
+ + +
+
+
+
+
Actions Réussies
+
#{auditConsultationBean.successCount}
+
+
+ +
+
+
+ + + Succès + + Opérations validées +
+
+
+ + +
+
+
+
+
Actions Échouées
+
#{auditConsultationBean.failureCount}
+
+
+ +
+
+
+ + + Échecs + + Opérations en erreur +
+
+
+ + +
+
+
+
+
Taux de Réussite
+
+ #{auditConsultationBean.totalRecords > 0 ? (auditConsultationBean.successCount * 100 / auditConsultationBean.totalRecords) : 0}% +
+
+
+ +
+
+
+ + Performance globale +
+
+
+ + +
+
+
+ +
Filtres de Recherche
+
+ + +
+
+ + +
+ +
+ + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+
+ + +
+
+
+
+ +
Logs d'Audit
+
+ +
+ + + + + + + + + + + +
+ + #{log.typeAction} +
+
+ + + +
+
+ + #{log.acteurUsername != null and log.acteurUsername.length() > 1 ? log.acteurUsername.substring(0,2).toUpperCase() : 'XX'} + +
+ #{log.acteurUsername} +
+
+ + + +
+ + #{log.ressourceType} +
+
+ + + +
+ + #{log.dateAction} +
+
+ + + + + #{not empty log.details ? log.details : '-'} + + + + + + + #{not empty log.adresseIp ? log.adresseIp : '-'} + + + + + + + + + +
+
+
+
+ + + + +
+ +
+
+ Statut + +
+
+ + +
+ +

#{auditConsultationBean.selectedLog.typeAction}

+
+ + +
+ +
+ +

#{auditConsultationBean.selectedLog.acteurUsername}

+
+
+ + +
+ +
+ +

#{auditConsultationBean.selectedLog.ressourceType}

+
+ ID: #{auditConsultationBean.selectedLog.ressourceId} +
+ + +
+ +
+ +

#{auditConsultationBean.selectedLog.dateAction}

+
+
+ + + +
+ +

#{auditConsultationBean.selectedLog.details}

+
+
+ + + +
+ +

#{auditConsultationBean.selectedLog.adresseIp}

+
+
+ + + +
+ +

#{auditConsultationBean.selectedLog.userAgent}

+
+
+ + + +
+ +

#{auditConsultationBean.selectedLog.messageErreur}

+
+
+
+ +
+ +
+
+
- diff --git a/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml index c8a9e9d..3af3573 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/dashboard.xhtml @@ -7,154 +7,327 @@ template="/templates/main-template.xhtml"> Tableau de Bord - Lions User Manager - + -
- -
- - - - - -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+ +
+
+
+
+ +
+

Tableau de Bord

+

Vue d'ensemble de la gestion des utilisateurs Keycloak

+
+
+ +
+
-
- - - - - - - -
-
- - +
+
Statistiques Principales
+
+ + +
+
+ +
+
+
Utilisateurs Actifs
+
#{dashboardBean.totalUsersDisplay}
+
+
+ +
+
+
+ + Total utilisateurs +
+
+
+
+ + +
+
+ +
+
+
Rôles Realm
+
#{dashboardBean.totalRolesDisplay}
+
+
+ +
+
+
+ + Rôles configurés +
+
+
+
+ + +
+
+ +
+
+
Actions Récentes
+
#{dashboardBean.recentActionsDisplay}
+
+
+ +
+
+
+ + Dernières 24h +
+
+
+
+ + +
+
+
+
+
Realm Actif
+
lions-user-manager
+
+
+ +
+
+
+ + Realm Keycloak +
+
+
+ + +
+
+
+ +
Actions Rapides
+
+ +
+
+ - -
-
- - +
+ - -
-
- - +
+ - -
-
- - +
+ - +
+
+ +
+
+ +
+
Conseil
+ Utilisez les raccourcis ci-dessus pour accéder rapidement aux fonctionnalités principales +
+
- - - - - - - - - -
-
- Version - 1.0.0 +
+ + +
+
+
+ +
Informations Système
-
- Realm Keycloak - lions-user-manager -
-
- Statut - -
-
- Application - Lions User Manager -
-
- Environnement - Développement -
-
- Base de données - Keycloak Admin API -
-
- Framework - Quarkus, PrimeFaces Freya + +
+ +
+
+
+ + Version +
+ 1.0.0 +
+
+ + +
+
+
+ + Realm Keycloak +
+ lions-user-manager +
+
+ + +
+
+
+ + Statut +
+ +
+
+ + +
+
+
+ + Framework +
+ Quarkus 3.15.1 +
+
+ + +
+
+
+ + Interface +
+ PrimeFaces Freya +
+
+ + +
+
+
+ + Environnement +
+ +
+
- - -
+
+ + +
+
+
+
+ +
Activités Récentes
+
+ +
+ +
+ +
+
+ +
0
+
Utilisateurs créés
+ Aujourd'hui +
+
+ + +
+
+ +
0
+
Rôles modifiés
+ Cette semaine +
+
+ + +
+
+ +
-
+
Sessions actives
+ En temps réel +
+
+ + +
+
+ +
0
+
Actions critiques
+ 24 dernières heures +
+
+
+
+
+
+
- + diff --git a/src/main/resources/META-INF/resources/pages/user-manager/roles.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/roles.xhtml new file mode 100644 index 0000000..8a3fa51 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/user-manager/roles.xhtml @@ -0,0 +1,90 @@ + + + Gestion Rôles + + + +
+ + + + + + + + + +
+ Rôles +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + +
+ Nom + +
+
+ Description + +
+
+
+ + + + + +
+ + + + + +
+
+ +
\ No newline at end of file diff --git a/src/main/resources/META-INF/resources/pages/user-manager/roles/assign.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/roles/assign.xhtml index 47cb98d..9cebae9 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/roles/assign.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/roles/assign.xhtml @@ -4,29 +4,301 @@ 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"> + + + + + Attribution de Rôles - Lions User Manager - - - - - - +
+ +
+
+
+
+ +
+

Attribution de Rôles

+

Gérer les rôles de l'utilisateur

+
+
+ + + Retour à la liste + +
+
+
- -
- - - - - - + +
+
+

+ + Informations de l'Utilisateur +

+ + +
+
+
+ +
+ +
+

#{userProfilBean.user.username}

+

#{userProfilBean.user.email}

+
+
+ +
+
+
+
+ +

#{userProfilBean.user.prenom}

+
+
+ +
+
+ +

#{userProfilBean.user.nom}

+
+
+ +
+
+ +

#{userProfilBean.user.email}

+
+
+ +
+
+ +
+ + + + + +
+
+
+
+
+
+
+ + +
+ +

Utilisateur non trouvé

+

+ + +

+ Pour assigner des rôles, accédez à cette page depuis la liste des utilisateurs + + + Aller à la liste des utilisateurs + +
+
+
+
+ + + +
+
+

+ + Rôles Actuels +

+ + + +
+ +
+
+
+ +
+
#{role}
+ Rôle Realm +
+
+ + + +
+
+
+ + +
+ +

Aucun rôle assigné

+ Assignez des rôles depuis la liste disponible +
+
+ +
+
+ + Total: #{userProfilBean.user.realmRoles != null ? userProfilBean.user.realmRoles.size() : 0} rôle(s) +
+ +
+
+
+
+ +
+
+

+ + Rôles Disponibles +

+ + + + + + + +
+ + + +
+
+
+
+ + #{role.name} +
+

+ + +

+
+ +
+
+
+
+ + +
+ +

Aucun rôle disponible

+ Créez des rôles depuis la page de gestion des rôles +
+
+ +
+
+ +
+
Astuce
+ Cliquez sur pour assigner un rôle à l'utilisateur +
+
+
+
+
+
+ + +
+
+

+ + Actions +

+ + +
+ + + + Voir le Profil + + + + + + Modifier l'Utilisateur + + + + + Liste des Utilisateurs + + + + + Gérer les Rôles + +
+
+
+
+
+ + + + + + - diff --git a/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml index 064281b..de53f34 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/roles/list.xhtml @@ -4,159 +4,515 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" - xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" + xmlns:fr="http://primefaces.org/freya" template="/templates/main-template.xhtml"> Gestion des Rôles - Lions User Manager - - - - - - - -
- - - - - - - - - - - - +
+ +
+
+
+
+ +
+

Gestion des Rôles

+

Gestion des rôles Realm et Client Keycloak

+
+
+
+ + +
+
+
+
+ + +
+
+

+ + Filtres +

+ + +
+ +
+
+ + + + + + +
+
+ + +
+
+ + + + + + +
+
+ + +
+
+ + + + + +
+
+
+
+
+
+ + +
+ +
+
+
+
+
+
Rôles Realm
+
#{roleGestionBean.realmRoles.size()}
+
+
+ +
+
+
+ + Rôles du realm +
+
+
+ +
+
+
+
+
Rôles Client
+
#{roleGestionBean.clientRoles.size()}
+
+
+ +
+
+
+ + Rôles spécifiques client +
+
+
+ +
+
+
+
+
Total Rôles
+
#{roleGestionBean.allRoles.size()}
+
+
+ +
+
+
+ + Tous les rôles configurés +
+
+
+ +
+
+
+
+
Realm Actif
+
#{roleGestionBean.realmName}
+
+
+ +
+
+
+ + Realm actuellement sélectionné +
+
+
- - +
- -
- - - - - - - - + +
+
+ +
+

+ + Rôles Realm +

+ +
- - - - - - +
+ +
+
+
+
+

+ + #{role.name} +

+

+ + +

+
+
+ + + +
+
- - - - - - - +
+ + + REALM + + + + COMPOSITE + +
+ +
+ + ID: #{role.id != null ? role.id : 'N/A'} +
+
+
+
+ + +
+
+ +

Aucun rôle Realm trouvé

+ Sélectionnez un realm ou créez un nouveau rôle +
+
+
+
+
+
+ + +
+
+ +
+

+ + Rôles Client +

+ +
+ +
+ +
+
+
+
+

+ + #{role.name} +

+

+ + +

+
+
+ + + +
+
+ +
+ + + CLIENT + + + + COMPOSITE + + + #{role.clientName} + +
+ +
+ + ID: #{role.id != null ? role.id : 'N/A'} +
+
+
+
+ + +
+
+ +

Aucun rôle Client trouvé

+ Sélectionnez un client ou créez un nouveau rôle +
+
+
+
+
+
- -
- - -
- -
- - - - -
-
- -
-

Aucun rôle Realm trouvé

-
-
-
-
-
-
- - -
- - -
- -
- - - - -
-
- -
-

Aucun rôle Client trouvé

-
-
-
-
-
-
- - - + + - - - - - - - - - +
+
+
+ + + + + + Lettres, chiffres, underscores et tirets uniquement +
+ +
+ + + +
+
+
+ + + +
+ + + + +
- - + + - - - - - - - - - +
+
+
+ + + + + +
+ +
+ + + + + +
+ +
+ + + +
+
+
+ + + +
+ + + + +
+ + + + + + - diff --git a/src/main/resources/META-INF/resources/pages/user-manager/settings.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/settings.xhtml new file mode 100644 index 0000000..109f651 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/user-manager/settings.xhtml @@ -0,0 +1,131 @@ + + + + + Paramètres - Lions User Manager + + + + + + + + + +
+ +
+
+
Informations du compte
+ + + + + + + + + + + + + + + +
+
+ + +
+
+
Préférences
+ +
+
+ Thème des composants + + + + +
+
+ Mode sombre + + + + + +
+
+ Style d'input + + + + + +
+
+
+
+
+ + +
+
+
Actions
+
+ + + + + + + + + +
+
+
+
+
+ +
+ diff --git a/src/main/resources/META-INF/resources/pages/user-manager/users.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/users.xhtml new file mode 100644 index 0000000..ac2f22a --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/user-manager/users.xhtml @@ -0,0 +1,129 @@ + + + Gestion Utilisateurs + + + +
+ + + + + + + + + + + +
+ Utilisateurs + + + + + + +
+
+ + + + + + + + + + + + + + + + #{user.enabled ? 'ACTIF' : 'INACTIF'} + + + + + + + + + + + + +
+
+
+ + + + + +
+ Username + +
+
+ Email + +
+
+ Prénom + +
+
+ Nom + +
+
+ Actif + +
+ + +
+ Mot de passe (Temporaire) + +
+
+
+
+ + + + + +
+ + + + + +
+
+ +
\ No newline at end of file diff --git a/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml index ae193cd..2789d83 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/users/create.xhtml @@ -4,32 +4,462 @@ 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"> Nouvel Utilisateur - Lions User Manager - - - - - - +
+ +
+
+
+
+ +
+

Nouvel Utilisateur

+

Créer un nouvel utilisateur dans le realm Keycloak

+
+
+
+ + + + Retour + +
+
+
+
- -
- - - - - - - - - + + + +
+ +
+ + +
+
+ +
+
+
+ +
Informations de Base
+
+ +
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+
+
+
+ + +
+
+
+ +
Sécurité
+
+ +
+ +
+
+ + + + + + + Au moins 8 caractères avec lettres et chiffres + +
+
+ + +
+
+ + + + + + Doit correspondre au mot de passe + +
+
+
+ + +
+
+ +
+
Recommandations de sécurité
+ + Utilisez un mot de passe fort contenant des majuscules, minuscules, chiffres et caractères spéciaux. + L'utilisateur pourra le modifier lors de sa première connexion. + +
+
+
+
+
+ + +
+
+
+ +
Configuration
+
+ +
+ +
+ + + +
+ + +
+ +
+
+ +
+ + + +
+ + +
+ + + +
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
Aperçu
+
+ + +
+
+ +
+
+ + +
+ +
+ Nom d'utilisateur +
+ +
+
+ + +
+ Nom complet +
+ +
+
+ + +
+ Email +
+ +
+
+ + +
+ Statut +
+ + +
+
+ + +
+ Realm +
+ + #{userCreationBean.realmName} +
+
+
+
+
+ + +
+
+
+
+ + + + + + + + + + +
+ + +
+ + * Champs obligatoires +
+
+
+
+
+ + + + + + + + + +
+ +
+
+ +
Informations Requises
+
+
+
    +
  • + Nom d'utilisateur : Identifiant unique de 3 à 50 caractères. + Ex: jdupont, marie.martin +
  • +
  • + Email : Adresse email valide et unique. + Ex: utilisateur@example.com +
  • +
  • + Prénom et Nom : Identification complète de l'utilisateur. +
  • +
  • + Mot de passe : Au moins 8 caractères requis. +
  • +
+
+
+ + +
+
+ +
Recommandations de Sécurité
+
+
+
    +
  • Utilisez un mot de passe fort avec majuscules, minuscules, chiffres et symboles
  • +
  • Évitez les mots de passe trop simples ou courants
  • +
  • Le mot de passe sera hashé et sécurisé par Keycloak
  • +
  • L'utilisateur pourra modifier son mot de passe après connexion
  • +
+
+
+ + +
+
+ +
Options de Configuration
+
+
+
    +
  • + Compte activé : Si coché, l'utilisateur peut se connecter immédiatement. +
  • +
  • + Email vérifié : Si coché, l'email est considéré comme vérifié (pas de vérification requise). +
  • +
  • + Realm : lions-user-manager est le realm par défaut pour la gestion des utilisateurs. +
  • +
+
+
+
+ + +
+ +
+
+
- diff --git a/src/main/resources/META-INF/resources/pages/user-manager/users/edit.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/users/edit.xhtml index 2f75390..3713185 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/users/edit.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/users/edit.xhtml @@ -4,30 +4,322 @@ 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"> + + + + + Modifier Utilisateur - Lions User Manager - - - - - - +
+ +
+
+
+
+ +
+

Modifier Utilisateur

+

Modifier les informations d'un utilisateur existant dans Keycloak

+
+
+
+ + + + + Voir le profil + + + + Retour + +
+
+
+
- -
- - - - - - - + + + +
+ +
+ + + +
+
+ +
+
+
+ +
Informations de Base
+
+ +
+ +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ +
+
+
+
+ + +
+
+
+ +
Configuration
+
+ +
+ +
+ +
+ + +
+ +
+
+ +
+ + + +
+ + +
+ + + +
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
Aperçu
+
+ + +
+
+ +
+
+ + +
+ +
+ Nom d'utilisateur +
+ +
+
+ + +
+ Nom complet +
+ +
+
+ + +
+ Email +
+ +
+
+ + +
+ Statut +
+ + +
+
+ + +
+ Realm +
+ + #{userProfilBean.realmName} +
+
+
+
+
+ + +
+
+
+
+ + + + + + + + +
+ + +
+ + * Champs obligatoires +
+
+
+
+
+ + + +
+
+
+ +

Utilisateur non trouvé

+

L'utilisateur demandé n'existe pas ou n'a pas pu être chargé.

+ + + Retour à la liste + +
+
+
+
+
+ + + + + + - diff --git a/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml index 1aa2987..a6012e3 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/users/list.xhtml @@ -4,117 +4,485 @@ 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"> Liste des Utilisateurs - Lions User Manager - - - - - - - -
- - - - - - + +
+ +
+
+
+
+ +
+

Gestion des Utilisateurs

+

Gestion centralisée des utilisateurs Keycloak - Recherche, création, modification et suppression

+
+
+
+ + +
+
- - - +
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ +
+
Statistiques des Utilisateurs
+
- - - -
- - - - - - - -
+ +
+
+
+
+
Total Utilisateurs
+
#{userListBean.totalRecords}
+
+
+ +
+
+
+ + Utilisateurs dans le realm +
+
+
- -
- - - - - - - - - - - - + +
+
+
+
+
Utilisateurs Actifs
+
#{userListBean.activeUsersCount}
+
+
+ +
+
+
+ + + #{userListBean.activeUsersPercentage}% + + Taux d'activation +
+ +
+
+ + +
+
+
+
+
Utilisateurs Désactivés
+
#{userListBean.disabledUsersCount}
+
+
+ +
+
+
+ + + #{userListBean.disabledUsersPercentage}% + + Taux de désactivation +
+ +
+
+ + +
+
+
+
+
Realm Actuel
+
#{userListBean.realmName}
+
+
+ +
+
+
+ + Realm Keycloak +
+
+
+ + +
+
+
+ +
Recherche et Filtres
+
+ +
+
+
+ + + + +
+
+ +
+
+ + + + + +
+
+ +
+
+ + + + + + +
+
+ +
+ +
+
+
+
+ + +
+
+
+
+ +
Liste des Utilisateurs
+
+ +
+ + + + + + + + + +
+
+ + #{user.prenom != null ? user.prenom.substring(0,1).toUpperCase() : 'U'}#{user.nom != null ? user.nom.substring(0,1).toUpperCase() : 'U'} + +
+
+ #{user.username} + #{user.prenom} #{user.nom} +
+
+
+ + + +
+ + #{user.email} + + + +
+
+ + + + + + + + +
+ + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + +
+
+
+ +
Actions Rapides
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + + + +
+

+ Importez des utilisateurs depuis un fichier CSV ou JSON. +

+ +
+ + +
+
+
+
+ + + + + + diff --git a/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml index ae6c082..a6540d6 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/users/profile.xhtml @@ -4,104 +4,418 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" template="/templates/main-template.xhtml"> - - Profil Utilisateur - Lions User Manager + + Mon Profil - Lions User Manager - - - - - - - -
- - - - - - - - - - - - - - -
-
-
-
-
- -
- - - - - + +
+
+
+

+ + Mon Profil +

+ + + Retour au tableau de bord + +
+
- -
+ +
- - - - - - - - - - - - - - - - - - - - -
+
+ +
+
+ +
+ #{userSessionBean.initials} +
- -
-
Actions Rapides
-
- - - - - - - - - - - - - - - - - - - - + +

#{userSessionBean.fullName}

+ + +

+ + #{userSessionBean.email} +

+ + +
+ + + Connecté + +
+ + +
+ + #{userSessionBean.primaryRole} + +
+
+
+ + +
+
+ +
+

+ + Informations Personnelles +

+ +
+ +

#{userSessionBean.username}

+
+ +
+ +

#{userSessionBean.fullName}

+
+ +
+ +
+

#{userSessionBean.email}

+ +
+
+ +
+ +

#{userSessionBean.firstName}

+
+ +
+ +

#{userSessionBean.lastName}

+
+
+ + +
+

+ + Rôles et Permissions +

+ +
+ +
+ + + +
+
+ +
+ +
+ +
+
+ +
+ +

+ + + + + +

+
+ +
+ +
+ +
+
+
+
+
+ + +
+
+

+ + Informations de Session OIDC +

+ +
+ +
+

Informations du Token

+ +
+ +

+ #{userSessionBean.issuer} +

+
+ +
+ +

+ #{userSessionBean.subject} +

+
+ +
+ +

+ account +

+
+ +
+ +
+ +
+
+
+ + +
+

Détails de la Session

+ +
+ +
+ +

+ + + +

+
+
+ +
+ +
+ +

+ + + +

+
+
+ +
+ +
+ +

+ lions-user-manager +

+
+
+ +
+ +
+ +

+ Session active +

+
+
+
+
+
+
+ + +
+
+

+ + Statistiques d'Activité +

+ +
+
+
+
+ Connexions + +
+

--

+ Total des connexions +
+
+ +
+
+
+ Dernière connexion + +
+

Aujourd'hui

+ Session en cours +
+
+ +
+
+
+ Actions + +
+

--

+ Actions effectuées +
+
+ +
+
+
+ Sessions + +
+

1

+ Session active +
+
+
+
+
+ + +
+
+

+ + Actions Rapides +

+ + +
+ +
+
+

+ + Gestion du Profil +

+
+ + + + + + + + + + + +
+
+
+ + +
+
+

+ + Sessions et Sécurité +

+
+ + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + + + - diff --git a/src/main/resources/META-INF/resources/pages/user-manager/users/view.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/users/view.xhtml new file mode 100644 index 0000000..1ca4300 --- /dev/null +++ b/src/main/resources/META-INF/resources/pages/user-manager/users/view.xhtml @@ -0,0 +1,232 @@ + + + + + + + + + + Profil Utilisateur - Lions User Manager + + +
+ +
+
+
+
+ +
+

Profil de l'Utilisateur

+

Détails et informations de l'utilisateur

+
+
+ + + Retour à la liste + +
+
+
+ + +
+
+ +
+ +
+
+ +
+ #{userProfilBean.user.prenom != null ? userProfilBean.user.prenom.substring(0,1).toUpperCase() : 'U'}#{userProfilBean.user.nom != null ? userProfilBean.user.nom.substring(0,1).toUpperCase() : 'U'} +
+ + +

#{userProfilBean.user.prenom} #{userProfilBean.user.nom}

+ + +

+ + #{userProfilBean.user.email} +

+ + +
+ +
+ + +
+ + +
+
+
+ + +
+
+ +
+

+ + Informations Personnelles +

+ +
+ +

#{userProfilBean.user.username}

+
+ +
+ +

#{userProfilBean.user.prenom} #{userProfilBean.user.nom}

+
+ +
+ +
+

#{userProfilBean.user.email}

+ +
+
+ +
+ +

#{userProfilBean.user.prenom}

+
+ +
+ +

#{userProfilBean.user.nom}

+
+ +
+ +

#{userProfilBean.user.telephone}

+
+
+ + +
+

+ + Rôles et Permissions +

+ +
+ +
+ + + + +
+
+ +
+ +
+ +
+
+ +
+ +

#{userProfilBean.realmName}

+
+
+
+
+
+
+ + +
+ +

Utilisateur non trouvé

+

L'utilisateur demandé n'existe pas ou n'a pas pu être chargé.

+ + + Retour à la liste + +
+
+
+
+ + +
+
+

+ + Actions +

+ + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+ +
+ diff --git a/src/main/resources/META-INF/resources/resources/components/user-action-dropdown.xhtml b/src/main/resources/META-INF/resources/resources/components/user-action-dropdown.xhtml new file mode 100644 index 0000000..1ae248a --- /dev/null +++ b/src/main/resources/META-INF/resources/resources/components/user-action-dropdown.xhtml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/META-INF/resources/resources/css/custom-topbar.css b/src/main/resources/META-INF/resources/resources/css/custom-topbar.css new file mode 100644 index 0000000..aa01c86 --- /dev/null +++ b/src/main/resources/META-INF/resources/resources/css/custom-topbar.css @@ -0,0 +1,625 @@ +/* ============================================================================ + Lions User Manager - Enhanced Custom Topbar Styles + + Auteur: Lions User Manager + Version: 2.0.0 + Description: Styles améliorés pour la topbar avec intégration intelligente + des patterns Freya layout pour un rendu parfait + + Intégrations: + - Freya Layout Variables & Patterns + - Support Dark/Light Theme + - Animations fluides (fadeInDown, modal-in) + - PrimeFlex utility classes + - Responsive design + ============================================================================ */ + +/* ---------------------------------------------------------------------------- + BASE TOPBAR LAYOUT OVERRIDES + Améliore la structure de base de la topbar Freya + ---------------------------------------------------------------------------- */ + +.layout-topbar { + position: fixed; + top: 0; + z-index: 999; + width: 100%; + height: 62px; + transition: width 0.2s, box-shadow 0.3s ease; +} + +.layout-topbar .layout-topbar-wrapper { + height: 100%; + display: flex; + align-items: center; +} + +.layout-topbar .layout-topbar-wrapper .layout-topbar-right { + height: 100%; + flex-grow: 1; + padding: 0 16px 0 12px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.layout-topbar .layout-topbar-wrapper .layout-topbar-right .layout-topbar-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-grow: 1; + list-style-type: none; + margin: 0; + padding: 0; + height: 100%; +} + +.layout-topbar .layout-topbar-wrapper .layout-topbar-right .layout-topbar-actions > li { + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +/* ---------------------------------------------------------------------------- + USER PROFILE LINK - Enhanced with Freya patterns + ---------------------------------------------------------------------------- */ + +.layout-topbar .user-profile-link { + display: flex !important; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + border-radius: 6px; + transition: all 0.2s cubic-bezier(0.05, 0.74, 0.2, 0.99); + text-decoration: none; + cursor: pointer; + position: relative; + overflow: hidden; +} + +.layout-topbar .user-profile-link::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)); + opacity: 0; + transition: opacity 0.2s ease; +} + +.layout-topbar .user-profile-link:hover::before { + opacity: 1; +} + +.layout-topbar .user-profile-link:hover { + background-color: rgba(255, 255, 255, 0.12); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +/* User Avatar - Integration with Freya avatar patterns */ +.layout-topbar .user-profile-link .user-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 600; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.layout-topbar .user-profile-link:hover .user-avatar { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +/* User Info Container */ +.layout-topbar .user-info { + display: flex; + flex-direction: column; + align-items: flex-start; + line-height: 1.2; + min-width: 0; +} + +/* User Name - Enhanced typography */ +.layout-topbar .user-name { + font-weight: 600; + font-size: 0.875rem; + color: var(--text-color); + margin-bottom: 0.125rem; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +/* User Email */ +.layout-topbar .user-email { + font-size: 0.75rem; + color: var(--text-color-secondary); + opacity: 0.85; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; +} + +/* User Role Badge */ +.layout-topbar .user-role { + font-size: 0.7rem; + color: var(--primary-color); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + background: rgba(var(--primary-color-rgb, 79, 142, 236), 0.1); + padding: 0.125rem 0.375rem; + border-radius: 4px; + white-space: nowrap; +} + +.layout-topbar .user-separator { + color: var(--text-color-secondary); + opacity: 0.5; + font-weight: 300; + margin: 0 0.25rem; +} + +/* Online Status Indicator */ +.layout-topbar .user-status { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + position: relative; +} + +.layout-topbar .user-status.online { + background-color: #4CAF50; + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); + animation: pulse-online 2s ease-in-out infinite; +} + +@keyframes pulse-online { + 0%, 100% { + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); + } + 50% { + box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.2); + } +} + +/* ---------------------------------------------------------------------------- + USER DROPDOWN MENU - Enhanced with Freya dropdown patterns + ---------------------------------------------------------------------------- */ + +.layout-topbar .user-dropdown-menu { + display: none; + position: absolute; + top: 62px; + right: 0; + min-width: 280px; + max-width: 320px; + padding: 0; + margin: 0; + list-style-type: none; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08); + border: 1px solid var(--surface-border); + background: var(--surface-card); + overflow: hidden; + z-index: 1000; + animation-duration: 0.2s; + animation-timing-function: cubic-bezier(0.05, 0.74, 0.2, 0.99); + animation-fill-mode: forwards; +} + +/* Show dropdown when parent is active */ +.layout-topbar .user-profile.active-topmenuitem > .user-dropdown-menu { + display: block; + animation-name: fadeInDown; +} + +/* Dropdown Header - Integration with Freya gradient patterns */ +.user-dropdown-header { + padding: 1.25rem 1rem; + background: linear-gradient(135deg, var(--primary-color), var(--primary-600, #387FE9)); + color: white; + display: flex; + align-items: center; + gap: 0.75rem; + position: relative; + overflow: hidden; +} + +.user-dropdown-header::before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + animation: shimmer 3s ease-in-out infinite; +} + +@keyframes shimmer { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(-20%, -20%); } +} + +/* Dropdown Avatar */ +.user-dropdown-avatar { + position: relative; + flex-shrink: 0; +} + +.user-dropdown-avatar > div { + width: 48px; + height: 48px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.3); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: 700; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.user-status-indicator { + position: absolute; + bottom: 2px; + right: 2px; + width: 10px; + height: 10px; + border-radius: 50%; + border: 2px solid white; +} + +.user-status-indicator.online { + background-color: #4CAF50; + animation: pulse-indicator 2s ease-in-out infinite; +} + +@keyframes pulse-indicator { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +/* Dropdown User Info */ +.user-dropdown-info { + flex: 1; + min-width: 0; +} + +.user-dropdown-name { + font-weight: 600; + font-size: 1rem; + margin-bottom: 0.25rem; + color: white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-dropdown-email { + font-size: 0.875rem; + opacity: 0.95; + margin-bottom: 0.25rem; + color: rgba(255, 255, 255, 0.95); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-dropdown-role { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + background: rgba(255, 255, 255, 0.25); + padding: 0.25rem 0.5rem; + border-radius: 12px; + display: inline-block; + color: white; + backdrop-filter: blur(10px); +} + +/* Dividers */ +.user-dropdown-divider { + height: 1px; + background-color: var(--surface-border); + margin: 0; +} + +/* Menu Sections */ +.user-dropdown-section { + padding: 0.75rem 0; +} + +.section-title { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-color-secondary); + padding: 0 1rem 0.5rem 1rem; + margin-bottom: 0.25rem; +} + +.section-items { + display: flex; + flex-direction: column; +} + +/* Dropdown Items - Enhanced with Freya interaction patterns */ +.dropdown-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + color: var(--text-color); + text-decoration: none; + transition: all 0.2s cubic-bezier(0.05, 0.74, 0.2, 0.99); + border: none; + background: none; + width: 100%; + text-align: left; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + position: relative; + overflow: hidden; +} + +.dropdown-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 3px; + height: 100%; + background: var(--primary-color); + transform: scaleX(0); + transition: transform 0.2s ease; +} + +.dropdown-item:hover::before { + transform: scaleX(1); +} + +.dropdown-item:hover { + background-color: var(--surface-hover); + color: var(--primary-color); + padding-left: 1.25rem; +} + +.dropdown-item:active { + background-color: var(--surface-ground); + transform: scale(0.98); +} + +.dropdown-item i { + width: 1.25rem; + text-align: center; + color: var(--text-color-secondary); + transition: all 0.2s ease; + font-size: 1rem; +} + +.dropdown-item:hover i { + color: var(--primary-color); + transform: scale(1.1); +} + +.dropdown-item span { + flex: 1; +} + +.item-arrow { + margin-left: auto; + opacity: 0; + transition: opacity 0.2s ease, transform 0.2s ease; + font-size: 0.75rem; +} + +.dropdown-item:hover .item-arrow { + opacity: 1; + transform: translateX(4px); +} + +/* Logout Item - Enhanced danger state */ +.logout-item { + color: var(--red-500) !important; + margin-top: 0.25rem; +} + +.logout-item:hover { + background-color: var(--red-50) !important; + color: var(--red-600) !important; +} + +.logout-item i { + color: var(--red-500) !important; +} + +.logout-item:hover i { + color: var(--red-600) !important; + transform: scale(1.1) rotate(-5deg); +} + +/* ---------------------------------------------------------------------------- + ANIMATIONS - Integration with Freya animation patterns + ---------------------------------------------------------------------------- */ + +@keyframes dropdownFadeIn { + 0% { + opacity: 0; + transform: translateY(-10px) scale(0.95); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.user-dropdown-menu { + animation: dropdownFadeIn 0.3s ease-out; + transform-origin: top right; +} + +/* ---------------------------------------------------------------------------- + DARK MODE SUPPORT - Integration with Freya dark theme + ---------------------------------------------------------------------------- */ + +.layout-wrapper.layout-topbar-dark .layout-topbar { + background-color: #293241; + box-shadow: 0 10px 40px 0 rgba(0, 0, 0, 0.2); +} + +.layout-wrapper.layout-topbar-dark .user-profile-link:hover { + background-color: rgba(255, 255, 255, 0.08); +} + +.layout-wrapper.layout-topbar-dark .user-dropdown-menu { + background: var(--surface-900, #1E1E1E); + border-color: var(--surface-700, #383838); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.3); +} + +.layout-wrapper.layout-topbar-dark .user-dropdown-divider { + background-color: var(--surface-700, #383838); +} + +.layout-wrapper.layout-topbar-dark .section-title { + color: var(--text-color-secondary); + opacity: 0.8; +} + +.layout-wrapper.layout-topbar-dark .dropdown-item { + color: var(--text-color); +} + +.layout-wrapper.layout-topbar-dark .dropdown-item:hover { + background-color: var(--surface-800, #2A2A2A); +} + +.layout-wrapper.layout-topbar-dark .logout-item:hover { + background-color: rgba(211, 47, 47, 0.1) !important; +} + +/* ---------------------------------------------------------------------------- + RESPONSIVE DESIGN - Integration with Freya responsive patterns + ---------------------------------------------------------------------------- */ + +@media (max-width: 991px) { + .layout-topbar .user-dropdown-menu { + left: 10px; + right: 10px; + position: fixed; + top: 62px; + max-width: none; + } +} + +@media (max-width: 768px) { + .layout-topbar .user-dropdown-menu { + min-width: 260px; + max-width: 280px; + } + + .user-dropdown-header { + padding: 1rem 0.75rem; + } + + .dropdown-item { + padding: 0.625rem 0.75rem; + font-size: 0.8125rem; + } + + .section-title { + padding: 0 0.75rem 0.5rem 0.75rem; + } + + /* Hide user info on mobile */ + .layout-topbar .user-info { + display: none; + } + + .layout-topbar .user-profile-link { + padding: 0.5rem; + } +} + +@media (max-width: 576px) { + .layout-topbar .user-dropdown-menu { + left: 8px; + right: 8px; + border-radius: 12px; + } +} + +/* ---------------------------------------------------------------------------- + ACCESSIBILITY ENHANCEMENTS + ---------------------------------------------------------------------------- */ + +.dropdown-item:focus, +.user-profile-link:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +@media (prefers-reduced-motion: reduce) { + .layout-topbar .user-profile-link, + .dropdown-item, + .user-dropdown-menu, + .user-avatar, + .item-arrow { + animation: none; + transition: none; + } +} + +/* ---------------------------------------------------------------------------- + UTILITY CLASSES - PrimeFlex integration + ---------------------------------------------------------------------------- */ + +.layout-topbar .flex { + display: flex !important; +} + +.layout-topbar .align-items-center { + align-items: center !important; +} + +.layout-topbar .justify-content-center { + justify-content: center !important; +} + +.layout-topbar .gap-2 { + gap: 0.5rem !important; +} + +.layout-topbar .text-white { + color: white !important; +} + +.layout-topbar .border-circle { + border-radius: 50% !important; +} + +.layout-topbar .bg-primary { + background-color: var(--primary-color) !important; +} diff --git a/src/main/resources/META-INF/resources/resources/css/topbar-elite.css b/src/main/resources/META-INF/resources/resources/css/topbar-elite.css new file mode 100644 index 0000000..f257f28 --- /dev/null +++ b/src/main/resources/META-INF/resources/resources/css/topbar-elite.css @@ -0,0 +1,795 @@ +/* + * ╔════════════════════════════════════════════════════════════╗ + * ║ Lions Platform Elite Topbar Styles (Freya Design System) ║ + * ║ Modern, Professional, Responsive ║ + * ╚════════════════════════════════════════════════════════════╝ + */ + +/* ═══════════════════════════════════════════════════════════ */ +/* BASE TOPBAR */ +/* ═══════════════════════════════════════════════════════════ */ + +.unionflow-elite, +.lions-elite { + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-600) 100%); + box-shadow: 0 2px 12px rgba(0,0,0,0.08); + position: relative; + z-index: 1000; +} + +.unionflow-elite::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, + transparent 0%, + var(--primary-300) 50%, + transparent 100%); + opacity: 0.5; +} + +/* App Version Badge */ +.app-version { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.625rem; + background: rgba(255,255,255,0.15); + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + color: rgba(255,255,255,0.9); + margin-left: 0.75rem; + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.2); +} + +/* ═══════════════════════════════════════════════════════════ */ +/* SEARCH */ +/* ═══════════════════════════════════════════════════════════ */ + +.search-item .topbar-icon { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.search-item:hover .topbar-icon { + transform: scale(1.1); + color: var(--primary-100); +} + +.search-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + min-width: 400px; + background: white; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0,0,0,0.12); + padding: 1rem; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid var(--surface-border); +} + +.search-item:hover .search-dropdown { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.search-wrapper-elite { + display: flex; + align-items: center; + gap: 0.75rem; + background: var(--surface-50); + border-radius: 8px; + padding: 0.5rem 1rem; + border: 1px solid var(--surface-border); + transition: all 0.3s ease; +} + +.search-wrapper-elite:focus-within { + background: white; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1); +} + +.search-wrapper-elite .pi-search { + color: var(--text-color-secondary); + font-size: 1rem; +} + +.search-wrapper-elite .search-input { + flex: 1; + border: none; + background: transparent; + padding: 0.5rem 0; + font-size: 0.875rem; +} + +.search-wrapper-elite .search-input:focus { + outline: none; + box-shadow: none; +} + +/* ═══════════════════════════════════════════════════════════ */ +/* NOTIFICATIONS */ +/* ═══════════════════════════════════════════════════════════ */ + +.notifications-item { + position: relative; +} + +.badge-count { + position: absolute; + top: -4px; + right: -4px; + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + color: white; + font-size: 0.625rem; + font-weight: 700; + padding: 0.125rem 0.375rem; + border-radius: 10px; + min-width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4); + animation: pulse-badge 2s infinite; +} + +@keyframes pulse-badge { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } +} + +.notifications-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + min-width: 360px; + max-width: 400px; + background: white; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0,0,0,0.12); + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid var(--surface-border); + overflow: hidden; +} + +.notifications-item:hover .notifications-dropdown { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.notif-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + background: var(--surface-50); + border-bottom: 1px solid var(--surface-border); +} + +.count-label { + font-size: 0.75rem; + color: var(--text-color-secondary); + background: var(--primary-color); + color: white; + padding: 0.25rem 0.625rem; + border-radius: 12px; + font-weight: 600; +} + +.notif-item { + display: flex; + align-items: flex-start; + gap: 0.875rem; + padding: 0.875rem 1.25rem; + transition: all 0.2s ease; + cursor: pointer; +} + +.notif-item:hover { + background: var(--surface-50); +} + +.notif-item i { + font-size: 1.25rem; + margin-top: 0.25rem; +} + +.notif-title { + font-weight: 600; + color: var(--text-color); + font-size: 0.875rem; + margin-bottom: 0.25rem; +} + +.notif-time { + font-size: 0.75rem; + color: var(--text-color-secondary); +} + +.notif-footer { + padding: 0.75rem 1.25rem; + text-align: center; + border-top: 1px solid var(--surface-border); + background: var(--surface-50); +} + +.notif-footer a { + font-size: 0.875rem; + font-weight: 600; + text-decoration: none; + transition: color 0.2s ease; +} + +.notif-footer a:hover { + color: var(--primary-600); +} + +/* ═══════════════════════════════════════════════════════════ */ +/* USER PROFILE */ +/* ═══════════════════════════════════════════════════════════ */ + +.elite-user .profile-trigger { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.875rem; + border-radius: 10px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: rgba(255,255,255,0.1); + backdrop-filter: blur(10px); +} + +.elite-user .profile-trigger:hover { + background: rgba(255,255,255,0.2); + transform: translateY(-1px); +} + +.avatar-container { + position: relative; +} + +.avatar { + width: 38px; + height: 38px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + font-weight: 700; + color: white; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + border: 2px solid rgba(255,255,255,0.3); +} + +.bg-gradient-primary { + background: linear-gradient(135deg, var(--primary-400) 0%, var(--primary-600) 100%); +} + +.status-dot { + position: absolute; + bottom: 0; + right: 0; + width: 10px; + height: 10px; + border-radius: 50%; + border: 2px solid white; +} + +.status-dot.online { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); + animation: pulse-dot 2s infinite; +} + +@keyframes pulse-dot { + 0%, 100% { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); } + 50% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.4); } +} + +.user-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.user-header { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.user-name { + font-size: 0.875rem; + font-weight: 600; + color: white; + line-height: 1.2; +} + +.role-badge { + font-size: 0.625rem; + padding: 0.125rem 0.5rem; + background: rgba(255,255,255,0.25); + border-radius: 8px; + font-weight: 600; + color: white; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.session-timer { + display: flex; + align-items: center; + gap: 0.375rem; +} + +.icon-sm { + font-size: 0.7rem; +} + +.timer-text { + font-size: 0.75rem; + font-weight: 600; + font-family: 'Courier New', monospace; +} + +.arrow { + font-size: 0.75rem; + color: rgba(255,255,255,0.8); + transition: transform 0.3s ease; +} + +.elite-user:hover .arrow { + transform: rotate(180deg); +} + +/* ═══════════════════════════════════════════════════════════ */ +/* USER DROPDOWN MENU */ +/* ═══════════════════════════════════════════════════════════ */ + +.elite-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + min-width: 340px; + background: white; + border-radius: 16px; + box-shadow: 0 12px 48px rgba(0,0,0,0.15); + opacity: 0; + visibility: hidden; + transform: translateY(-10px) scale(0.95); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid var(--surface-border); + overflow: hidden; +} + +.elite-user:hover .elite-dropdown { + opacity: 1; + visibility: visible; + transform: translateY(0) scale(1); +} + +/* Dropdown Header */ +.dropdown-header { + padding: 1.25rem; + background: linear-gradient(135deg, var(--primary-50) 0%, var(--surface-50) 100%); + border-bottom: 1px solid var(--surface-border); +} + +.header-content { + display: flex; + gap: 1rem; +} + +.header-avatar { + position: relative; +} + +.avatar-lg { + width: 56px; + height: 56px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + font-weight: 700; + color: white; + box-shadow: 0 6px 16px rgba(0,0,0,0.15); +} + +.status-indicator { + position: absolute; + bottom: 2px; + right: 2px; + width: 14px; + height: 14px; + border-radius: 50%; + border: 3px solid white; + display: flex; + align-items: center; + justify-content: center; +} + +.status-indicator.online { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); +} + +.status-indicator i { + font-size: 6px; + color: white; +} + +.header-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; + justify-content: center; +} + +.header-info .name { + font-size: 1rem; + font-weight: 700; + color: var(--text-color); + line-height: 1.3; +} + +.header-info .email { + font-size: 0.75rem; + color: var(--text-color-secondary); + line-height: 1.3; +} + +.role-tag { + display: inline-flex; + align-items: center; + font-size: 0.625rem; + padding: 0.25rem 0.625rem; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-600) 100%); + color: white; + border-radius: 8px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + align-self: flex-start; + margin-top: 0.25rem; +} + +/* Session Card */ +.session-card { + padding: 1rem 1.25rem; + background: var(--surface-50); + border-bottom: 1px solid var(--surface-border); +} + +.card-content { + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +.info-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.info-row .label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: var(--text-color-secondary); + font-weight: 500; +} + +.info-row .label i { + font-size: 0.875rem; +} + +.info-row .value { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-color); +} + +/* Progress Bar */ +.progress-container { + margin-top: 0.5rem; +} + +.progress-bar { + height: 6px; + background: var(--surface-200); + border-radius: 10px; + overflow: hidden; + position: relative; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--green-400) 0%, var(--green-500) 100%); + border-radius: 10px; + transition: width 1s ease, background 0.3s ease; +} + +.progress-fill[style*="width: 0%"], +.progress-fill[style*="width: 1%"], +.progress-fill[style*="width: 2%"], +.progress-fill[style*="width: 3%"], +.progress-fill[style*="width: 4%"], +.progress-fill[style*="width: 5%"] { + background: linear-gradient(90deg, var(--red-400) 0%, var(--red-500) 100%); +} + +.progress-label { + font-size: 0.625rem; + color: var(--text-color-secondary); + margin-top: 0.375rem; + text-align: right; + font-weight: 500; +} + +/* Menu Sections */ +.divider { + height: 1px; + background: var(--surface-border); + margin: 0; +} + +.menu-section { + padding: 0.75rem 0; +} + +.menu-section.compact { + padding: 0.5rem 0; +} + +.section-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + font-weight: 700; + color: var(--text-color-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 0.5rem 1.25rem 0.75rem; +} + +.section-items { + display: flex; + flex-direction: column; +} + +.menu-item { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 0.75rem 1.25rem; + color: var(--text-color); + text-decoration: none; + transition: all 0.2s ease; + font-size: 0.875rem; + cursor: pointer; + border: none; + background: transparent; + width: 100%; + text-align: left; +} + +.menu-item:hover { + background: var(--surface-100); +} + +.menu-item i:first-child { + font-size: 1rem; + color: var(--text-color-secondary); + transition: all 0.2s ease; +} + +.menu-item:hover i:first-child { + color: var(--primary-color); + transform: translateX(2px); +} + +.menu-item span { + flex: 1; + font-weight: 500; +} + +.arrow-right { + font-size: 0.75rem; + color: var(--text-color-secondary); + margin-left: auto; + transition: transform 0.2s ease; +} + +.menu-item:hover .arrow-right { + transform: translateX(3px); +} + +.item-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 0.375rem; + background: var(--primary-color); + color: white; + font-size: 0.625rem; + font-weight: 700; + border-radius: 10px; + margin-left: auto; +} + +.value-badge { + font-size: 0.75rem; + color: var(--text-color-secondary); + background: var(--surface-100); + padding: 0.25rem 0.625rem; + border-radius: 8px; + font-weight: 600; + margin-left: auto; +} + +/* Logout Section */ +.logout-divider { + background: linear-gradient(90deg, + transparent 0%, + var(--red-200) 50%, + transparent 100%); + height: 2px; +} + +.logout-section { + padding: 0.75rem 0; + background: linear-gradient(to bottom, white 0%, var(--red-50) 100%); +} + +.logout-btn { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 0.875rem 1.25rem; + color: var(--red-600); + font-weight: 600; + font-size: 0.875rem; + text-decoration: none; + transition: all 0.3s ease; + cursor: pointer; + border: none; + background: transparent; + width: 100%; + text-align: left; +} + +.logout-btn:hover { + background: var(--red-100); + color: var(--red-700); +} + +.logout-btn i:first-child { + font-size: 1rem; + transition: transform 0.3s ease; +} + +.logout-btn:hover i:first-child { + transform: scale(1.1) rotate(-10deg); +} + +.logout-btn .pi-lock { + font-size: 0.875rem; +} + +/* ═══════════════════════════════════════════════════════════ */ +/* LOGOUT DIALOG */ +/* ═══════════════════════════════════════════════════════════ */ + +.elite-dialog .dialog-content { + text-align: center; + padding: 1.5rem 1rem; +} + +.icon-wrapper { + display: inline-flex; + align-items: center; + justify-content: center; + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, var(--red-50) 0%, var(--red-100) 100%); + margin-bottom: 1.5rem; +} + +.icon-lg { + font-size: 2.5rem; + color: var(--red-500); +} + +.dialog-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--text-color); + margin-bottom: 1.5rem; + line-height: 1.4; +} + +.info-box { + background: var(--surface-50); + border-radius: 12px; + padding: 1rem; + margin-bottom: 1rem; + border: 1px solid var(--surface-border); +} + +.info-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem; + color: var(--text-color); + font-size: 0.875rem; +} + +.info-item i { + color: var(--primary-color); + font-size: 1rem; +} + +.warning-text { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-size: 0.875rem; + color: var(--text-color-secondary); + margin: 0; +} + +.warning-text i { + color: var(--blue-500); +} + +.dialog-footer { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + padding-top: 1rem; +} + +/* ═══════════════════════════════════════════════════════════ */ +/* UTILITY CLASSES */ +/* ═══════════════════════════════════════════════════════════ */ + +.text-green-600 { color: #059669 !important; } +.text-yellow-600 { color: #d97706 !important; } +.text-orange-600 { color: #ea580c !important; } +.text-red-600 { color: #dc2626 !important; } + +/* ═══════════════════════════════════════════════════════════ */ +/* RESPONSIVE */ +/* ═══════════════════════════════════════════════════════════ */ + +@media (max-width: 768px) { + .app-version { display: none; } + .user-info { display: none; } + .elite-dropdown { min-width: 300px; } + .search-dropdown { min-width: 280px; } +} diff --git a/src/main/resources/META-INF/resources/template.xhtml b/src/main/resources/META-INF/resources/template.xhtml new file mode 100644 index 0000000..fa68ea5 --- /dev/null +++ b/src/main/resources/META-INF/resources/template.xhtml @@ -0,0 +1,98 @@ + + + + + + + + + + + <ui:insert name="title">Lions User Manager</ui:insert> + + + + + + +
+
+ + 🦁 Lions User Manager + + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + +
+ + diff --git a/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml b/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml index 45d377b..2ca51de 100644 --- a/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/layout/menu.xhtml @@ -49,6 +49,11 @@ + + + + +
diff --git a/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml b/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml index 8010114..0dc3b04 100644 --- a/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/layout/topbar.xhtml @@ -1,70 +1,298 @@ - + -
+ + +
+
- + - + + v2.0
- + + - + +
- + + +
+
+ +
+ +

Êtes-vous sûr de vouloir vous déconnecter ?

+ +
+
+ + #{userSessionBean.fullName} +
+
+ + Session: #{sessionMonitor.formattedRemainingTime} +
+
+ +

+ + Vous devrez vous reconnecter pour accéder à l'application. +

+
+ + + + +
+ + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/templates/components/role-management/role-assignment.xhtml b/src/main/resources/META-INF/resources/templates/components/role-management/role-assignment.xhtml index ba52180..ad3f0f8 100644 --- a/src/main/resources/META-INF/resources/templates/components/role-management/role-assignment.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/role-management/role-assignment.xhtml @@ -162,13 +162,13 @@

Rechercher un rôle

- - + update="@parent" />
diff --git a/src/main/resources/META-INF/resources/templates/components/role-management/role-card.xhtml b/src/main/resources/META-INF/resources/templates/components/role-management/role-card.xhtml index cd9884e..885e2ce 100644 --- a/src/main/resources/META-INF/resources/templates/components/role-management/role-card.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/role-management/role-card.xhtml @@ -3,7 +3,8 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" - xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"> + xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" + xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions"> - - - - + + + + + + + + diff --git a/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml b/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml index 5a7f194..5a9cbba 100644 --- a/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/shared/buttons/button-user-action.xhtml @@ -15,8 +15,11 @@ Paramètres: - value: String (requis) - Texte du bouton - icon: String (optionnel) - Classe d'icône PrimeIcons - - action: String (optionnel) - Action à exécuter - - outcome: String (optionnel) - Page de redirection + - hasAction: Boolean (défaut: false) - Indique si une action est fournie + - action: MethodExpression (optionnel) - Action à exécuter (requis si hasAction=true) + - hasOutcome: Boolean (défaut: false) - Indique si un outcome est fourni + - outcome: String (optionnel) - Page de redirection (requis si hasOutcome=true) + - onclick: String (optionnel) - Code JavaScript à exécuter au clic - severity: String (défaut: "primary") - Severity: "primary", "success", "warning", "danger", "info", "secondary" - size: String (défaut: "normal") - Taille: "small", "normal", "large" - disabled: Boolean (défaut: false) - Désactiver le bouton @@ -45,8 +48,8 @@ - - + + @@ -85,14 +88,15 @@ - + process="#{process}" + onclick="#{not empty onclick ? onclick : ''}" /> + process="#{process}" + onclick="#{not empty onclick ? onclick : ''}" /> + + + + xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" + xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions">
@@ -16,7 +17,19 @@
-
#{value}
+
+ + + + + 0 + 0 + #{value} + + + 0 + +
diff --git a/src/main/resources/META-INF/resources/templates/components/shared/dashboard/kpi-group.xhtml b/src/main/resources/META-INF/resources/templates/components/shared/dashboard/kpi-group.xhtml index b95c26f..73187d7 100644 --- a/src/main/resources/META-INF/resources/templates/components/shared/dashboard/kpi-group.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/shared/dashboard/kpi-group.xhtml @@ -34,7 +34,7 @@ - + Autres KPI à ajouter ici --> diff --git a/src/main/resources/META-INF/resources/templates/components/shared/tables/user-data-table.xhtml b/src/main/resources/META-INF/resources/templates/components/shared/tables/user-data-table.xhtml index 13d3f7c..c55a153 100644 --- a/src/main/resources/META-INF/resources/templates/components/shared/tables/user-data-table.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/shared/tables/user-data-table.xhtml @@ -3,7 +3,8 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" - xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"> + xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" + xmlns:lum="http://xmlns.jcp.org/jsf/composite/components"> + + + - + - -
- - #{user.username} + +
+
+
+ + #{user.prenom != null ? user.prenom.substring(0,1) : 'U'}#{user.nom != null ? user.nom.substring(0,1) : ''} + +
+
+ #{user.username}
- -
- #{user.prenom} #{user.nom} + +
+ #{user.prenom} #{user.nom} - #{user.fonction} + #{user.fonction}
- -
- - #{user.email} + +
+ + #{user.email} - +
@@ -107,45 +139,50 @@ - -
+ +
- - - - - -
- -
- - - - - - - - + +
+ + + + + + + + + + + + + Aucun rôle + +
- - - - - - + +
+ +
diff --git a/src/main/resources/META-INF/resources/templates/components/user-management/user-actions.xhtml b/src/main/resources/META-INF/resources/templates/components/user-management/user-actions.xhtml index 4c3065e..06a4d14 100644 --- a/src/main/resources/META-INF/resources/templates/components/user-management/user-actions.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/user-management/user-actions.xhtml @@ -78,9 +78,11 @@ - + styleClass="p-button-text p-button-sm p-button-rounded p-button-plain" + type="button" + title="Actions" + style="width: 2rem; height: 2rem; padding: 0; margin: 0;"> +
-

#{user.firstName} #{user.lastName}

+

#{user.prenom} #{user.nom}

@#{user.username}
diff --git a/src/main/resources/META-INF/resources/templates/components/user-management/user-form-content.xhtml b/src/main/resources/META-INF/resources/templates/components/user-management/user-form-content.xhtml new file mode 100644 index 0000000..d407e84 --- /dev/null +++ b/src/main/resources/META-INF/resources/templates/components/user-management/user-form-content.xhtml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Mot de passe

+ + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + +
+
+
+ +
+ diff --git a/src/main/resources/META-INF/resources/templates/components/user-management/user-form.xhtml b/src/main/resources/META-INF/resources/templates/components/user-management/user-form.xhtml index 729ad44..7de20d0 100644 --- a/src/main/resources/META-INF/resources/templates/components/user-management/user-form.xhtml +++ b/src/main/resources/META-INF/resources/templates/components/user-management/user-form.xhtml @@ -23,6 +23,7 @@ - submitOutcome: String (optionnel) - Page de redirection après soumission - update: String (optionnel) - Composants à mettre à jour après soumission - hasSubmitAction: Boolean (optionnel) - Indicateur si submitAction est fourni (pour éviter l'évaluation) + - useParentForm: Boolean (défaut: false) - Utiliser le formulaire parent au lieu de créer un nouveau formulaire Exemples d'utilisation: @@ -53,215 +54,42 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Mot de passe

- - - - - - - - - -
- - - -
- - - - - - - - - - - - - - - - - -
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/META-INF/resources/templates/main-template.xhtml b/src/main/resources/META-INF/resources/templates/main-template.xhtml index cc7419a..0e79e59 100644 --- a/src/main/resources/META-INF/resources/templates/main-template.xhtml +++ b/src/main/resources/META-INF/resources/templates/main-template.xhtml @@ -4,6 +4,7 @@ xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:p="http://primefaces.org/ui" + xmlns:fr="http://primefaces.org/freya" lang="fr"> @@ -47,6 +48,7 @@ + diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 64d6b26..46bbfd9 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,53 +1,69 @@ # ============================================================================ -# Configuration Développement - Lions User Manager Client +# Lions User Manager Client - Configuration Développement # ============================================================================ -# NOTE: La configuration OIDC principale est dans application.properties -# avec le préfixe %dev. Ce fichier contient UNIQUEMENT les surcharges -# spécifiques au développement qui ne sont pas déjà dans application.properties +# Ce fichier contient TOUTES les propriétés spécifiques au développement +# Il surcharge et complète application.properties # ============================================================================ # ============================================ -# Logging - Surcharges DEV +# HTTP Configuration DEV +# ============================================ +quarkus.http.port=8082 + +# Port de débogage distinct (évite les conflits) +quarkus.debug.port=5006 + +# Session cookies non sécurisés en dev (HTTP autorisé) +quarkus.http.session-cookie-secure=false + +# ============================================ +# Logging DEV (plus verbeux) # ============================================ -# Logging plus détaillé en dev quarkus.log.console.level=DEBUG quarkus.log.category."dev.lions.user.manager".level=TRACE -# Debug OIDC pour voir quelle valeur est chargée quarkus.log.category."io.quarkus.oidc".level=DEBUG -quarkus.log.category."io.quarkus.oidc.runtime".level=TRACE +quarkus.log.category."io.quarkus.oidc.runtime".level=DEBUG # ============================================ -# MyFaces - Surcharges DEV +# MyFaces DEV # ============================================ quarkus.myfaces.project-stage=Development quarkus.myfaces.check-id-production-mode=false # ============================================ -# Backend - Surcharges DEV +# Backend REST Client DEV # ============================================ -# Backend local (le serveur tourne sur le port 8081) lions.user.manager.backend.url=http://localhost:8081 +quarkus.rest-client."lions-user-manager-api".url=http://localhost:8081 +# Timeout augmenté pour éviter les erreurs lors des appels Keycloak lents +quarkus.rest-client."lions-user-manager-api".read-timeout=90000 # ============================================ -# CORS - Surcharges DEV +# CORS DEV (permissif) # ============================================ -# CORS permissif en dev (surcharge de application.properties) -quarkus.http.cors.origins=* +quarkus.http.cors.origins=http://localhost:8080,http://localhost:8081,http://localhost:8082 # ============================================ -# OIDC - Surcharges DEV (si nécessaire) +# OIDC Configuration DEV - Keycloak Local # ============================================ -# NOTE: La configuration OIDC principale est dans application.properties -# avec le préfixe %dev. (lignes 73-81) -# Ne définir ici QUE les propriétés qui ne sont pas déjà dans application.properties -# -# State Secret pour PKCE (OBLIGATOIRE quand pkce-required=true) -# Ce secret est utilisé pour encrypter le PKCE code verifier dans le state cookie -# Minimum 16 caractères requis, recommandé 32 caractères -# IMPORTANT: Ne PAS définir pkce-secret quand state-secret est défini (conflit Quarkus) +# Serveur Keycloak local +quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager +quarkus.oidc.client-id=lions-user-manager-client +quarkus.oidc.credentials.secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO +quarkus.oidc.token.issuer=http://localhost:8180/realms/lions-user-manager + +# Désactiver la vérification TLS en dev (Keycloak local sans certificat) +quarkus.oidc.tls.verification=none + +# Cookie same-site permissif en dev +quarkus.oidc.authentication.cookie-same-site=lax + +# PKCE requis en dev +quarkus.oidc.authentication.pkce-required=true + +# Secrets pour PKCE et state management quarkus.oidc.authentication.state-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO -# Surcharge de encryption-secret (64 caractères pour garantir) -# Cette propriété est aussi définie dans application.properties avec %dev., -# mais on la redéfinit ici pour garantir qu'elle soit chargée quarkus.oidc.token-state-manager.encryption-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO +# Chemins publics élargis pour faciliter le développement +quarkus.http.auth.permission.public.paths=/,/index.xhtml,/index,/pages/public/*,/auth/*,/q/*,/q/oidc/*,/favicon.ico,/resources/*,/META-INF/resources/*,/images/*,/jakarta.faces.resource/*,/javax.faces.resource/* diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 36e7511..cb66536 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -1,18 +1,61 @@ # ============================================================================ -# Configuration Production - Lions User Manager Client +# Lions User Manager Client - Configuration Production # ============================================================================ -# NOTE: La configuration OIDC principale est dans application.properties -# avec le préfixe %prod. (lignes 86-94) -# -# Ce fichier peut être utilisé pour des surcharges spécifiques à la production -# qui ne sont pas déjà définies dans application.properties avec %prod. -# -# Exemple d'utilisation : -# - Surcharges de logging spécifiques à la production -# - Configurations spécifiques à un environnement de production particulier -# - Variables d'environnement qui doivent être surchargées +# Ce fichier contient TOUTES les propriétés spécifiques à la production +# Il surcharge et complète application.properties # ============================================================================ -# Exemple (décommenter si nécessaire) : -# quarkus.log.console.level=WARN -# quarkus.log.category."dev.lions.user.manager".level=INFO +# ============================================ +# HTTP Configuration PROD +# ============================================ +quarkus.http.port=8080 + +# Session cookies sécurisés en prod (HTTPS uniquement) +quarkus.http.session-cookie-secure=true + +# ============================================ +# Logging PROD (moins verbeux) +# ============================================ +quarkus.log.console.level=WARN +quarkus.log.category."dev.lions.user.manager".level=INFO +quarkus.log.category."io.quarkus.oidc".level=INFO + +# ============================================ +# MyFaces PROD +# ============================================ +quarkus.myfaces.project-stage=Production +quarkus.myfaces.check-id-production-mode=true + +# ============================================ +# Backend REST Client PROD +# ============================================ +# L'URL du backend doit être fournie via variable d'environnement +lions.user.manager.backend.url=${LIONS_USER_MANAGER_BACKEND_URL:https://api.lions.dev/user-manager} +quarkus.rest-client."lions-user-manager-api".url=${LIONS_USER_MANAGER_BACKEND_URL:https://api.lions.dev/user-manager} + +# ============================================ +# CORS PROD (restrictif) +# ============================================ +# Les origines autorisées doivent être fournies via variable d'environnement +quarkus.http.cors.origins=${CORS_ORIGINS:https://lions.dev,https://app.lions.dev} + +# ============================================ +# OIDC Configuration PROD - Keycloak Production +# ============================================ +# Serveur Keycloak production (via variable d'environnement) +quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master} +quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:lions-user-manager-client} +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} +quarkus.oidc.token.issuer=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master} + +# Vérification TLS requise en prod +quarkus.oidc.tls.verification=required + +# Cookie same-site strict en prod +quarkus.oidc.authentication.cookie-same-site=strict + +# PKCE optionnel en prod (géré par Keycloak) +quarkus.oidc.authentication.pkce-required=false + +# Secret de chiffrement via variable d'environnement (OBLIGATOIRE) +quarkus.oidc.token-state-manager.encryption-secret=${OIDC_ENCRYPTION_SECRET} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7b0e3c3..1a303b4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,21 @@ -# Configuration Lions User Manager Client +# ============================================================================ +# Lions User Manager Client - Configuration Commune +# ============================================================================ +# Ce fichier contient UNIQUEMENT la configuration commune à tous les environnements +# Les configurations spécifiques sont dans: +# - application-dev.properties (développement) +# - application-prod.properties (production) +# ============================================================================ + +# ============================================ +# Application Info +# ============================================ quarkus.application.name=lions-user-manager-client quarkus.application.version=1.0.0 -# Configuration HTTP -quarkus.http.port=8080 +# ============================================ +# Configuration HTTP (commune) +# ============================================ quarkus.http.host=0.0.0.0 quarkus.http.root-path=/ quarkus.http.so-reuse-port=true @@ -12,29 +24,34 @@ quarkus.http.so-reuse-port=true quarkus.http.session-timeout=60m quarkus.http.session-cookie-same-site=lax quarkus.http.session-cookie-http-only=true -quarkus.http.session-cookie-secure=false -# Configuration logging +# ============================================ +# Logging (configuration de base) +# ============================================ quarkus.log.console.enable=true quarkus.log.console.level=INFO quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n quarkus.log.category."dev.lions.user.manager".level=DEBUG -# MyFaces Configuration -quarkus.myfaces.project-stage=Development +# ============================================ +# MyFaces Configuration (commune) +# ============================================ quarkus.myfaces.state-saving-method=server -quarkus.myfaces.number-of-views-in-session=50 -quarkus.myfaces.number-of-sequential-views-in-session=10 +quarkus.myfaces.number-of-views-in-session=100 +quarkus.myfaces.number-of-sequential-views-in-session=20 quarkus.myfaces.serialize-state-in-session=false -quarkus.myfaces.client-view-state-timeout=3600000 -quarkus.myfaces.view-expired-exception-handler-redirect-page=/ -quarkus.myfaces.check-id-production-mode=false +quarkus.myfaces.client-view-state-timeout=7200000 +# Redirection vers la page d'accueil publique (HTML statique) en cas de vue expirée +quarkus.myfaces.view-expired-exception-handler-redirect-page=/index.html?expired=true quarkus.myfaces.strict-xhtml-links=false quarkus.myfaces.refresh-transient-build-on-pss=true quarkus.myfaces.resource-max-time-expires=604800000 quarkus.myfaces.resource-buffer-size=2048 +quarkus.myfaces.automatic-extensionless-mapping=true +# ============================================ # PrimeFaces Configuration +# ============================================ primefaces.THEME=freya primefaces.FONT_AWESOME=true primefaces.CLIENT_SIDE_VALIDATION=true @@ -44,85 +61,57 @@ primefaces.UPLOADER=commons primefaces.AUTO_UPDATE=false primefaces.CACHE_PROVIDER=org.primefaces.cache.DefaultCacheProvider -# Configuration Backend Lions User Manager -lions.user.manager.backend.url=${LIONS_USER_MANAGER_BACKEND_URL:http://localhost:8081} - -# Configuration REST Client -quarkus.rest-client."lions-user-manager-api".url=${lions.user.manager.backend.url} +# ============================================ +# Configuration REST Client (commune) +# ============================================ quarkus.rest-client."lions-user-manager-api".scope=jakarta.inject.Singleton quarkus.rest-client."lions-user-manager-api".connect-timeout=5000 quarkus.rest-client."lions-user-manager-api".read-timeout=30000 +quarkus.rest-client."lions-user-manager-api".bearer-token-propagation=true # ============================================ -# OIDC Configuration - Base (All Environments) +# Configuration OIDC - Base (commune) # ============================================ quarkus.oidc.enabled=true quarkus.oidc.application-type=web-app quarkus.oidc.authentication.redirect-path=/auth/callback quarkus.oidc.authentication.restore-path-after-redirect=true quarkus.oidc.authentication.scopes=openid,profile,email,roles -quarkus.oidc.authentication.cookie-same-site=lax quarkus.oidc.authentication.java-script-auto-redirect=false quarkus.oidc.discovery-enabled=true quarkus.oidc.verify-access-token=true quarkus.security.auth.enabled=true -# ============================================ -# OIDC Configuration - DEV Profile -# ============================================ -%dev.quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager -%dev.quarkus.oidc.client-id=lions-user-manager-client -%dev.quarkus.oidc.credentials.secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO -%dev.quarkus.oidc.token.issuer=http://localhost:8180/realms/lions-user-manager -%dev.quarkus.oidc.tls.verification=none -%dev.quarkus.oidc.authentication.pkce-required=true -# State Secret pour PKCE (OBLIGATOIRE quand pkce-required=true) -# Ce secret est utilisé pour encrypter le PKCE code verifier dans le state cookie -# Minimum 16 caractères requis, recommandé 32 caractères -# IMPORTANT: Ne PAS définir pkce-secret quand state-secret est défini (conflit Quarkus) -%dev.quarkus.oidc.authentication.state-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO -# Secret de chiffrement pour le token state manager (minimum 16 caractères requis) -# Cette clé est utilisée pour chiffrer les cookies d'état OIDC -# Valeur: 64 caractères (secret Keycloak dupliqué pour garantir la longueur) -%dev.quarkus.oidc.token-state-manager.encryption-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO +# Extraction des rôles depuis le token +quarkus.oidc.roles.role-claim-path=realm_access/roles +quarkus.oidc.roles.source=accesstoken # ============================================ -# OIDC Configuration - PROD Profile -# ============================================ -%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master} -%prod.quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:lions-user-manager-client} -%prod.quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} -%prod.quarkus.oidc.token.issuer=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master} -%prod.quarkus.oidc.tls.verification=required -%prod.quarkus.oidc.authentication.cookie-same-site=strict -%prod.quarkus.oidc.authentication.pkce-required=false -# Secret production via variable d'environnement (32 caractères requis) -%prod.quarkus.oidc.token-state-manager.encryption-secret=${OIDC_ENCRYPTION_SECRET} - # Chemins publics (non protégés par OIDC) -# Note: Les pages JSF sont servies via /pages/*.xhtml -quarkus.http.auth.permission.public.paths=/,/index.xhtml,/index,/pages/public/*,/auth/*,/q/*,/q/oidc/*,/favicon.ico,/resources/*,/META-INF/resources/*,/images/*,/jakarta.faces.resource/*,/javax.faces.resource/* +# ============================================ +quarkus.http.auth.permission.public.paths=/,/index.html,/index.xhtml,/index,/pages/public/*,/auth/*,/q/*,/q/oidc/*,/favicon.ico,/resources/*,/META-INF/resources/*,/images/*,/jakarta.faces.resource/*,/javax.faces.resource/* quarkus.http.auth.permission.public.policy=permit -# Chemins protégés (requièrent authentification) - Désactivé en dev +# Chemins protégés (requièrent authentification) quarkus.http.auth.permission.authenticated.paths=/pages/user-manager/* quarkus.http.auth.permission.authenticated.policy=authenticated -# DEV: Chemins publics élargis pour faciliter le développement -%dev.quarkus.http.auth.permission.public.paths=/,/index.xhtml,/index,/pages/public/*,/auth/*,/q/*,/q/oidc/*,/favicon.ico,/resources/*,/META-INF/resources/*,/images/*,/jakarta.faces.resource/*,/javax.faces.resource/* - -# CORS (si nécessaire pour développement) +# ============================================ +# CORS (configuration de base) +# ============================================ quarkus.http.cors=true -quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:8080,http://localhost:8081} quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS quarkus.http.cors.headers=Accept,Authorization,Content-Type,X-Requested-With +# ============================================ # Health Checks +# ============================================ quarkus.smallrye-health.root-path=/health quarkus.smallrye-health.liveness-path=/health/live quarkus.smallrye-health.readiness-path=/health/ready -# Metrics (optionnel) +# ============================================ +# Metrics +# ============================================ quarkus.micrometer.export.prometheus.enabled=true quarkus.micrometer.export.prometheus.path=/metrics - diff --git a/src/test/java/dev/lions/user/manager/client/filter/AuthHeaderFactoryTest.java b/src/test/java/dev/lions/user/manager/client/filter/AuthHeaderFactoryTest.java new file mode 100644 index 0000000..8a86d7c --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/filter/AuthHeaderFactoryTest.java @@ -0,0 +1,93 @@ +package dev.lions.user.manager.client.filter; + +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour AuthHeaderFactory + */ +@ExtendWith(MockitoExtension.class) +class AuthHeaderFactoryTest { + + @Mock + private JsonWebToken jwt; + + @InjectMocks + private AuthHeaderFactory authHeaderFactory; + + @BeforeEach + void setUp() { + // Setup + } + + @Test + void testUpdate_WithToken() { + when(jwt.getRawToken()).thenReturn("test-token-123"); + + MultivaluedMap incomingHeaders = new MultivaluedHashMap<>(); + MultivaluedMap clientOutgoingHeaders = new MultivaluedHashMap<>(); + + MultivaluedMap result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders); + + assertEquals("Bearer test-token-123", result.getFirst("Authorization")); + } + + @Test + void testUpdate_WithoutToken() { + when(jwt.getRawToken()).thenReturn(null); + + MultivaluedMap incomingHeaders = new MultivaluedHashMap<>(); + MultivaluedMap clientOutgoingHeaders = new MultivaluedHashMap<>(); + + MultivaluedMap result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders); + + assertNull(result.getFirst("Authorization")); + } + + @Test + void testUpdate_WithEmptyToken() { + when(jwt.getRawToken()).thenReturn(""); + + MultivaluedMap incomingHeaders = new MultivaluedHashMap<>(); + MultivaluedMap clientOutgoingHeaders = new MultivaluedHashMap<>(); + + MultivaluedMap result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders); + + assertNull(result.getFirst("Authorization")); + } + + @Test + void testUpdate_WithNullJwt() { + AuthHeaderFactory factory = new AuthHeaderFactory(); + + MultivaluedMap incomingHeaders = new MultivaluedHashMap<>(); + MultivaluedMap clientOutgoingHeaders = new MultivaluedHashMap<>(); + + MultivaluedMap result = factory.update(incomingHeaders, clientOutgoingHeaders); + + assertNotNull(result); + } + + @Test + void testUpdate_ExceptionHandling() { + when(jwt.getRawToken()).thenThrow(new RuntimeException("Error")); + + MultivaluedMap incomingHeaders = new MultivaluedHashMap<>(); + MultivaluedMap clientOutgoingHeaders = new MultivaluedHashMap<>(); + + MultivaluedMap result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders); + + assertNotNull(result); + assertNull(result.getFirst("Authorization")); + } +} diff --git a/src/test/java/dev/lions/user/manager/client/service/RestClientExceptionMapperTest.java b/src/test/java/dev/lions/user/manager/client/service/RestClientExceptionMapperTest.java new file mode 100644 index 0000000..3495129 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/service/RestClientExceptionMapperTest.java @@ -0,0 +1,126 @@ +package dev.lions.user.manager.client.service; + +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import jakarta.ws.rs.core.Response.StatusType; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour RestClientExceptionMapper + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RestClientExceptionMapperTest { + + private RestClientExceptionMapper exceptionMapper; + + @Mock + private Response response; + + @Mock + private StatusType statusType; + + @BeforeEach + void setUp() { + exceptionMapper = new RestClientExceptionMapper(); + // Mock getStatusInfo() to return a StatusType + when(response.getStatusInfo()).thenReturn(statusType); + when(statusType.getReasonPhrase()).thenReturn("Reason Phrase"); + } + + @Test + void testHandleBadRequest() { + when(response.getStatus()).thenReturn(400); + when(response.readEntity(String.class)).thenReturn("Bad Request"); + + RuntimeException exception = exceptionMapper.toThrowable(response); + + assertNotNull(exception); + assertTrue(exception instanceof RestClientExceptionMapper.BadRequestException); + } + + @Test + void testHandleUnauthorized() { + when(response.getStatus()).thenReturn(401); + when(response.readEntity(String.class)).thenReturn("Unauthorized"); + + RuntimeException exception = exceptionMapper.toThrowable(response); + + assertNotNull(exception); + assertTrue(exception instanceof RestClientExceptionMapper.UnauthorizedException); + } + + @Test + void testHandleForbidden() { + when(response.getStatus()).thenReturn(403); + when(response.readEntity(String.class)).thenReturn("Forbidden"); + + RuntimeException exception = exceptionMapper.toThrowable(response); + + assertNotNull(exception); + assertTrue(exception instanceof RestClientExceptionMapper.ForbiddenException); + } + + @Test + void testHandleNotFound() { + when(response.getStatus()).thenReturn(404); + when(response.readEntity(String.class)).thenReturn("Not Found"); + + RuntimeException exception = exceptionMapper.toThrowable(response); + + assertNotNull(exception); + assertTrue(exception instanceof RestClientExceptionMapper.NotFoundException); + } + + @Test + void testHandleInternalServerError() { + when(response.getStatus()).thenReturn(500); + when(response.readEntity(String.class)).thenReturn("Internal Server Error"); + + RuntimeException exception = exceptionMapper.toThrowable(response); + + assertNotNull(exception); + assertTrue(exception instanceof RestClientExceptionMapper.InternalServerErrorException); + } + + @Test + void testHandleUnknownStatus() { + when(response.getStatus()).thenReturn(418); + when(response.readEntity(String.class)).thenReturn("I'm a teapot"); + + RuntimeException exception = exceptionMapper.toThrowable(response); + + assertNotNull(exception); + assertTrue(exception instanceof RestClientExceptionMapper.UnknownHttpStatusException); + } + + @Test + void testHandlesMethod() { + MultivaluedMap headers = new jakarta.ws.rs.core.MultivaluedHashMap<>(); + // La méthode handles vérifie le status code + assertTrue(exceptionMapper.handles(400, headers)); + assertTrue(exceptionMapper.handles(500, headers)); + assertFalse(exceptionMapper.handles(200, headers)); + } + + @Test + void testGetPriority() { + int priority = exceptionMapper.getPriority(); + assertTrue(priority >= 0); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/AuditConsultationBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/AuditConsultationBeanTest.java new file mode 100644 index 0000000..8d28191 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/AuditConsultationBeanTest.java @@ -0,0 +1,274 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.AuditServiceClient; +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AuditConsultationBeanTest { + + @Mock + AuditServiceClient auditServiceClient; + + @Mock + FacesContext facesContext; + + @InjectMocks + AuditConsultationBean auditConsultationBean; + + MockedStatic facesContextMock; + + @BeforeEach + void setUp() { + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + } + + @AfterEach + void tearDown() { + facesContextMock.close(); + } + + @Test + void testInit() { + Map actionStats = Map.of(TypeActionAudit.USER_CREATE, 10L); + Map userStats = Map.of("admin", 5L); + + when(auditServiceClient.getActionStatistics(isNull(), isNull())).thenReturn(actionStats); + when(auditServiceClient.getUserActivityStatistics(isNull(), isNull())).thenReturn(userStats); + AuditServiceClient.CountResponse failureResponse = new AuditServiceClient.CountResponse(); + failureResponse.count = 2L; + AuditServiceClient.CountResponse successResponse = new AuditServiceClient.CountResponse(); + successResponse.count = 8L; + when(auditServiceClient.getFailureCount(isNull(), isNull())).thenReturn(failureResponse); + when(auditServiceClient.getSuccessCount(isNull(), isNull())).thenReturn(successResponse); + + auditConsultationBean.init(); + + assertNotNull(auditConsultationBean.getAuditLogs()); + assertNotNull(auditConsultationBean.getActionStatistics()); + assertNotNull(auditConsultationBean.getUserActivityStatistics()); + } + + @Test + void testSearchLogs() { + List logs = Collections.singletonList( + AuditLogDTO.builder() + .acteurUsername("admin") + .typeAction(TypeActionAudit.USER_CREATE) + .build()); + + when(auditServiceClient.searchLogs( + nullable(String.class), nullable(String.class), nullable(String.class), + nullable(TypeActionAudit.class), nullable(String.class), nullable(Boolean.class), + anyInt(), anyInt())) + .thenReturn(logs); + + auditConsultationBean.setActeurUsername("admin"); + auditConsultationBean.searchLogs(); + + assertFalse(auditConsultationBean.getAuditLogs().isEmpty()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testSearchLogsError() { + when(auditServiceClient.searchLogs( + anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(), + anyInt(), anyInt())) + .thenThrow(new RuntimeException("Error")); + + auditConsultationBean.searchLogs(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLoadLogsByActeur() { + List logs = Collections.singletonList( + AuditLogDTO.builder().acteurUsername("admin").build()); + + when(auditServiceClient.getLogsByActeur("admin", 100)).thenReturn(logs); + + auditConsultationBean.loadLogsByActeur("admin"); + + assertFalse(auditConsultationBean.getAuditLogs().isEmpty()); + assertEquals(1, auditConsultationBean.getTotalRecords()); + } + + @Test + void testLoadLogsByActeurError() { + when(auditServiceClient.getLogsByActeur("admin", 100)) + .thenThrow(new RuntimeException("Error")); + + auditConsultationBean.loadLogsByActeur("admin"); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLoadLogsByRealm() { + List logs = Collections.singletonList( + AuditLogDTO.builder().build()); + + when(auditServiceClient.getLogsByRealm( + anyString(), nullable(String.class), nullable(String.class), anyInt(), anyInt())) + .thenReturn(logs); + + auditConsultationBean.loadLogsByRealm("master"); + + assertFalse(auditConsultationBean.getAuditLogs().isEmpty()); + } + + @Test + void testLoadLogsByRealmError() { + when(auditServiceClient.getLogsByRealm( + anyString(), anyString(), anyString(), anyInt(), anyInt())) + .thenThrow(new RuntimeException("Error")); + + auditConsultationBean.loadLogsByRealm("master"); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLoadStatistics() { + Map actionStats = Map.of(TypeActionAudit.USER_CREATE, 10L); + Map userStats = Map.of("admin", 5L); + + when(auditServiceClient.getActionStatistics(anyString(), anyString())).thenReturn(actionStats); + when(auditServiceClient.getUserActivityStatistics(anyString(), anyString())).thenReturn(userStats); + AuditServiceClient.CountResponse failureResponse2 = new AuditServiceClient.CountResponse(); + failureResponse2.count = 2L; + AuditServiceClient.CountResponse successResponse2 = new AuditServiceClient.CountResponse(); + successResponse2.count = 8L; + when(auditServiceClient.getFailureCount(anyString(), anyString())).thenReturn(failureResponse2); + when(auditServiceClient.getSuccessCount(anyString(), anyString())).thenReturn(successResponse2); + + auditConsultationBean.setDateDebut(LocalDateTime.now().minusDays(7)); + auditConsultationBean.setDateFin(LocalDateTime.now()); + auditConsultationBean.loadStatistics(); + + assertNotNull(auditConsultationBean.getActionStatistics()); + assertNotNull(auditConsultationBean.getUserActivityStatistics()); + assertEquals(2L, auditConsultationBean.getFailureCount()); + assertEquals(8L, auditConsultationBean.getSuccessCount()); + } + + @Test + void testLoadStatisticsError() { + when(auditServiceClient.getActionStatistics(anyString(), anyString())) + .thenThrow(new RuntimeException("Error")); + + auditConsultationBean.loadStatistics(); + + // L'erreur est loggée mais ne doit pas planter + assertNotNull(auditConsultationBean); + } + + @Test + void testExportToCSV() { + when(auditServiceClient.exportLogsToCSV(anyString(), anyString())) + .thenReturn("csv,data\nline1,value1"); + + auditConsultationBean.setDateDebut(LocalDateTime.now().minusDays(7)); + auditConsultationBean.setDateFin(LocalDateTime.now()); + auditConsultationBean.exportToCSV(); + + verify(auditServiceClient).exportLogsToCSV(anyString(), anyString()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testExportToCSVError() { + when(auditServiceClient.exportLogsToCSV(anyString(), anyString())) + .thenThrow(new RuntimeException("Error")); + + auditConsultationBean.exportToCSV(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testResetFilters() { + auditConsultationBean.setActeurUsername("admin"); + auditConsultationBean.setDateDebut(LocalDateTime.now()); + auditConsultationBean.setDateFin(LocalDateTime.now()); + auditConsultationBean.setSelectedTypeAction(TypeActionAudit.USER_CREATE); + auditConsultationBean.setRessourceType("USER"); + auditConsultationBean.setSucces(true); + auditConsultationBean.setCurrentPage(2); + auditConsultationBean.getAuditLogs().add(AuditLogDTO.builder().build()); + + auditConsultationBean.resetFilters(); + + assertNull(auditConsultationBean.getActeurUsername()); + assertNull(auditConsultationBean.getDateDebut()); + assertNull(auditConsultationBean.getDateFin()); + assertNull(auditConsultationBean.getSelectedTypeAction()); + assertNull(auditConsultationBean.getRessourceType()); + assertNull(auditConsultationBean.getSucces()); + assertEquals(0, auditConsultationBean.getCurrentPage()); + assertTrue(auditConsultationBean.getAuditLogs().isEmpty()); + } + + @Test + void testPreviousPage() { + auditConsultationBean.setCurrentPage(2); + List logs = Collections.singletonList(AuditLogDTO.builder().build()); + when(auditServiceClient.searchLogs( + anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(), + anyInt(), anyInt())) + .thenReturn(logs); + + auditConsultationBean.previousPage(); + + assertEquals(1, auditConsultationBean.getCurrentPage()); + } + + @Test + void testPreviousPageAtFirstPage() { + auditConsultationBean.setCurrentPage(0); + + auditConsultationBean.previousPage(); + + assertEquals(0, auditConsultationBean.getCurrentPage()); + verify(auditServiceClient, never()).searchLogs( + anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(), + anyInt(), anyInt()); + } + + @Test + void testNextPage() { + List logs = Collections.singletonList(AuditLogDTO.builder().build()); + when(auditServiceClient.searchLogs( + anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(), + anyInt(), anyInt())) + .thenReturn(logs); + + auditConsultationBean.setCurrentPage(0); + auditConsultationBean.nextPage(); + + assertEquals(1, auditConsultationBean.getCurrentPage()); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/DashboardBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/DashboardBeanTest.java new file mode 100644 index 0000000..2c1b27a --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/DashboardBeanTest.java @@ -0,0 +1,117 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.AuditServiceClient; +import dev.lions.user.manager.client.service.RoleServiceClient; +import dev.lions.user.manager.client.service.UserServiceClient; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DashboardBeanTest { + + @Mock + UserServiceClient userServiceClient; + + @Mock + RoleServiceClient roleServiceClient; + + @Mock + AuditServiceClient auditServiceClient; + + @Mock + FacesContext facesContext; + + @InjectMocks + DashboardBean dashboardBean; + + MockedStatic facesContextMock; + + @BeforeEach + void setUp() { + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + } + + @AfterEach + void tearDown() { + facesContextMock.close(); + } + + @Test + void testInit() { + // Mock User Client + UserSearchResultDTO userResult = new UserSearchResultDTO(); + userResult.setTotalCount(100L); + when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(userResult); + + // Mock Role Client + RoleDTO role = RoleDTO.builder().name("role").build(); + when(roleServiceClient.getAllRealmRoles(anyString())).thenReturn(Collections.singletonList(role)); + + // Mock Audit Client + AuditServiceClient.CountResponse successResponse = new AuditServiceClient.CountResponse(); + successResponse.count = 50L; + AuditServiceClient.CountResponse failureResponse = new AuditServiceClient.CountResponse(); + failureResponse.count = 5L; + when(auditServiceClient.getSuccessCount(anyString(), anyString())).thenReturn(successResponse); + when(auditServiceClient.getFailureCount(anyString(), anyString())).thenReturn(failureResponse); + + dashboardBean.init(); + + assertEquals(100L, dashboardBean.getTotalUsers()); + assertEquals(1L, dashboardBean.getTotalRoles()); + assertEquals(55L, dashboardBean.getRecentActions()); + assertEquals("100", dashboardBean.getTotalUsersDisplay()); + } + + @Test + void testLoadStatisticsError() { + when(userServiceClient.searchUsers(any())).thenThrow(new RuntimeException("Error")); + + dashboardBean.loadStatistics(); + + assertEquals(0L, dashboardBean.getTotalUsers()); + verify(facesContext).addMessage(any(), any(FacesMessage.class)); + } + + @Test + void testRefreshStatistics() { + // Mock User Client + UserSearchResultDTO userResult = new UserSearchResultDTO(); + userResult.setTotalCount(10L); + when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(userResult); + + // Mock Role Client + when(roleServiceClient.getAllRealmRoles(anyString())).thenReturn(Collections.emptyList()); + + // Mock Audit Client + AuditServiceClient.CountResponse successResponse = new AuditServiceClient.CountResponse(); + successResponse.count = 0L; + AuditServiceClient.CountResponse failureResponse = new AuditServiceClient.CountResponse(); + failureResponse.count = 0L; + when(auditServiceClient.getSuccessCount(anyString(), anyString())).thenReturn(successResponse); + when(auditServiceClient.getFailureCount(anyString(), anyString())).thenReturn(failureResponse); + + dashboardBean.refreshStatistics(); + + verify(facesContext, atLeastOnce()).addMessage(any(), any(FacesMessage.class)); + } +} diff --git a/src/test/java/dev/lions/user/manager/client/view/GuestPreferencesTest.java b/src/test/java/dev/lions/user/manager/client/view/GuestPreferencesTest.java new file mode 100644 index 0000000..a497017 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/GuestPreferencesTest.java @@ -0,0 +1,152 @@ +package dev.lions.user.manager.client.view; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitaires pour GuestPreferences + */ +class GuestPreferencesTest { + + private GuestPreferences guestPreferences; + + @BeforeEach + void setUp() { + guestPreferences = new GuestPreferences(); + } + + @Test + void testDefaultValues() { + assertEquals("blue-light", guestPreferences.getTheme()); + assertEquals("light", guestPreferences.getLayout()); + assertEquals("blue-light", guestPreferences.getComponentTheme()); + assertEquals("light", guestPreferences.getDarkMode()); + assertEquals("layout-sidebar", guestPreferences.getMenuMode()); + assertEquals("light", guestPreferences.getTopbarTheme()); + assertEquals("light", guestPreferences.getMenuTheme()); + assertEquals("outlined", guestPreferences.getInputStyle()); + assertFalse(guestPreferences.isLightLogo()); + } + + @Test + void testThemeSetterAndGetter() { + guestPreferences.setTheme("green-light"); + assertEquals("green-light", guestPreferences.getTheme()); + } + + @Test + void testLayoutSetterAndGetter() { + guestPreferences.setLayout("dark"); + assertEquals("dark", guestPreferences.getLayout()); + } + + @Test + void testComponentThemeSetterAndGetter() { + guestPreferences.setComponentTheme("purple-light"); + assertEquals("purple-light", guestPreferences.getComponentTheme()); + } + + @Test + void testDarkModeSetterAndGetter() { + guestPreferences.setDarkMode("dark"); + assertEquals("dark", guestPreferences.getDarkMode()); + assertTrue(guestPreferences.isLightLogo()); + } + + @Test + void testDarkModeLight() { + guestPreferences.setDarkMode("light"); + assertEquals("light", guestPreferences.getDarkMode()); + assertFalse(guestPreferences.isLightLogo()); + } + + @Test + void testMenuModeSetterAndGetter() { + guestPreferences.setMenuMode("layout-horizontal"); + assertEquals("layout-horizontal", guestPreferences.getMenuMode()); + } + + @Test + void testTopbarThemeSetterAndGetter() { + guestPreferences.setTopbarTheme("dark"); + assertEquals("dark", guestPreferences.getTopbarTheme()); + } + + @Test + void testMenuThemeSetterAndGetter() { + guestPreferences.setMenuTheme("dark"); + assertEquals("dark", guestPreferences.getMenuTheme()); + } + + @Test + void testInputStyleSetterAndGetter() { + guestPreferences.setInputStyle("filled"); + assertEquals("filled", guestPreferences.getInputStyle()); + } + + @Test + void testLightLogoSetterAndGetter() { + guestPreferences.setLightLogo(true); + assertTrue(guestPreferences.isLightLogo()); + + guestPreferences.setLightLogo(false); + assertFalse(guestPreferences.isLightLogo()); + } + + @Test + void testGetInputStyleClass() { + guestPreferences.setInputStyle("outlined"); + assertEquals("p-input-outlined", guestPreferences.getInputStyleClass()); + + guestPreferences.setInputStyle("filled"); + assertEquals("p-input-filled", guestPreferences.getInputStyleClass()); + } + + @Test + void testGetLayoutClass() { + guestPreferences.setLayout("light"); + guestPreferences.setTheme("blue-light"); + assertEquals("layout-light layout-theme-blue-light", guestPreferences.getLayoutClass()); + + guestPreferences.setLayout("dark"); + guestPreferences.setTheme("green-light"); + assertEquals("layout-dark layout-theme-green-light", guestPreferences.getLayoutClass()); + } + + @Test + void testGetComponentThemes() { + var themes = guestPreferences.getComponentThemes(); + assertNotNull(themes); + assertFalse(themes.isEmpty()); + assertEquals(8, themes.size()); + + // Vérifier le premier thème + var firstTheme = themes.get(0); + assertEquals("blue-light", firstTheme.getFile()); + assertEquals("Blue", firstTheme.getName()); + assertEquals("#007ad9", firstTheme.getColor()); + + // Vérifier le dernier thème + var lastTheme = themes.get(themes.size() - 1); + assertEquals("cyan-light", lastTheme.getFile()); + assertEquals("Cyan", lastTheme.getName()); + assertEquals("#17a2b8", lastTheme.getColor()); + } + + @Test + void testOnMenuTypeChange() { + // Cette méthode ne fait rien, on vérifie juste qu'elle ne lance pas d'exception + assertDoesNotThrow(() -> guestPreferences.onMenuTypeChange()); + } + + @Test + void testComponentThemeClass() { + var theme = new GuestPreferences.ComponentTheme("test-file", "Test Name", "#FF0000"); + assertEquals("test-file", theme.getFile()); + assertEquals("Test Name", theme.getName()); + assertEquals("#FF0000", theme.getColor()); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/RealmAssignmentBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/RealmAssignmentBeanTest.java new file mode 100644 index 0000000..abd3665 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/RealmAssignmentBeanTest.java @@ -0,0 +1,343 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.RealmAssignmentServiceClient; +import dev.lions.user.manager.client.service.RealmServiceClient; +import dev.lions.user.manager.client.service.UserServiceClient; +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Tests unitaires pour RealmAssignmentBean + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RealmAssignmentBeanTest { + + @Mock + @RestClient + private RealmAssignmentServiceClient realmAssignmentServiceClient; + + @Mock + @RestClient + private UserServiceClient userServiceClient; + + @Mock + @RestClient + private RealmServiceClient realmServiceClient; + + @Mock + private UserSessionBean userSessionBean; + + @Mock + private FacesContext facesContext; + + @InjectMocks + private RealmAssignmentBean realmAssignmentBean; + + MockedStatic facesContextMock; + + @BeforeEach + void setUp() { + // Mock FacesContext + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + doNothing().when(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @AfterEach + void tearDown() { + if (facesContextMock != null) { + facesContextMock.close(); + } + } + + @Test + void testInit_WithAdminRole() { + when(userSessionBean.hasRole("admin")).thenReturn(true); + when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList()); + when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt())) + .thenReturn(UserSearchResultDTO.builder().users(Collections.emptyList()).build()); + when(realmServiceClient.getAllRealms()).thenReturn(Collections.emptyList()); + + realmAssignmentBean.init(); + + verify(realmAssignmentServiceClient).getAllAssignments(); + verify(userServiceClient).getAllUsers(anyString(), anyInt(), anyInt()); + verify(realmServiceClient).getAllRealms(); + } + + @Test + void testInit_WithoutAdminRole() { + when(userSessionBean.hasRole("admin")).thenReturn(false); + + realmAssignmentBean.init(); + + verify(realmAssignmentServiceClient, never()).getAllAssignments(); + } + + @Test + void testLoadAssignments_Success() { + List assignments = new ArrayList<>(); + assignments.add(RealmAssignmentDTO.builder().id("1").build()); + when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(assignments); + + realmAssignmentBean.loadAssignments(); + + assertEquals(1, realmAssignmentBean.getAssignments().size()); + verify(realmAssignmentServiceClient).getAllAssignments(); + } + + @Test + void testLoadAssignments_Error() { + when(realmAssignmentServiceClient.getAllAssignments()).thenThrow(new RuntimeException("Error")); + + realmAssignmentBean.loadAssignments(); + + assertTrue(realmAssignmentBean.getAssignments().isEmpty()); + } + + @Test + void testLoadAvailableUsers_Success() { + List users = new ArrayList<>(); + users.add(UserDTO.builder().id("1").username("user1").build()); + UserSearchResultDTO result = UserSearchResultDTO.builder().users(users).build(); + when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt())).thenReturn(result); + + realmAssignmentBean.loadAvailableUsers(); + + assertEquals(1, realmAssignmentBean.getAvailableUsers().size()); + } + + @Test + void testLoadAvailableUsers_NullResult() { + when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt())).thenReturn(null); + + realmAssignmentBean.loadAvailableUsers(); + + assertTrue(realmAssignmentBean.getAvailableUsers().isEmpty()); + } + + @Test + void testLoadAvailableRealms_Success() { + List realms = List.of("realm1", "realm2"); + when(realmServiceClient.getAllRealms()).thenReturn(realms); + + realmAssignmentBean.loadAvailableRealms(); + + assertEquals(2, realmAssignmentBean.getAvailableRealms().size()); + } + + @Test + void testLoadAvailableRealms_Empty() { + when(realmServiceClient.getAllRealms()).thenReturn(Collections.emptyList()); + + realmAssignmentBean.loadAvailableRealms(); + + assertTrue(realmAssignmentBean.getAvailableRealms().isEmpty()); + } + + @Test + void testAssignRealm_Success() { + when(userSessionBean.hasRole("admin")).thenReturn(true); + when(userSessionBean.getUsername()).thenReturn("admin"); + + List users = new ArrayList<>(); + UserDTO user = UserDTO.builder() + .id("user1") + .username("testuser") + .email("test@example.com") + .build(); + users.add(user); + UserSearchResultDTO result = UserSearchResultDTO.builder().users(users).build(); + when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt())).thenReturn(result); + + realmAssignmentBean.setAvailableUsers(users); + realmAssignmentBean.setSelectedUserId("user1"); + realmAssignmentBean.setSelectedRealmName("realm1"); + + RealmAssignmentDTO created = RealmAssignmentDTO.builder().id("1").build(); + when(realmAssignmentServiceClient.assignRealmToUser(any(RealmAssignmentDTO.class))) + .thenReturn(created); + + realmAssignmentBean.assignRealm(); + + verify(realmAssignmentServiceClient).assignRealmToUser(any(RealmAssignmentDTO.class)); + } + + @Test + void testAssignRealm_NoUserId() { + realmAssignmentBean.setSelectedUserId(null); + realmAssignmentBean.setSelectedRealmName("realm1"); + + realmAssignmentBean.assignRealm(); + + verify(realmAssignmentServiceClient, never()).assignRealmToUser(any()); + } + + @Test + void testAssignRealm_NoRealmName() { + realmAssignmentBean.setSelectedUserId("user1"); + realmAssignmentBean.setSelectedRealmName(null); + + realmAssignmentBean.assignRealm(); + + verify(realmAssignmentServiceClient, never()).assignRealmToUser(any()); + } + + @Test + void testRevokeAssignment_Success() { + RealmAssignmentDTO assignment = RealmAssignmentDTO.builder() + .id("1") + .userId("user1") + .username("testuser") + .realmName("realm1") + .build(); + when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList()); + + realmAssignmentBean.revokeAssignment(assignment); + + verify(realmAssignmentServiceClient).revokeRealmFromUser("user1", "realm1"); + } + + @Test + void testRevokeAssignment_Null() { + realmAssignmentBean.revokeAssignment(null); + + verify(realmAssignmentServiceClient, never()).revokeRealmFromUser(anyString(), anyString()); + } + + @Test + void testDeactivateAssignment_Success() { + RealmAssignmentDTO assignment = RealmAssignmentDTO.builder() + .id("1") + .build(); + when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList()); + + realmAssignmentBean.deactivateAssignment(assignment); + + verify(realmAssignmentServiceClient).deactivateAssignment("1"); + } + + @Test + void testActivateAssignment_Success() { + RealmAssignmentDTO assignment = RealmAssignmentDTO.builder() + .id("1") + .build(); + when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList()); + + realmAssignmentBean.activateAssignment(assignment); + + verify(realmAssignmentServiceClient).activateAssignment("1"); + } + + @Test + void testSetSuperAdmin_Success() { + List users = new ArrayList<>(); + UserDTO user = UserDTO.builder() + .id("user1") + .username("testuser") + .build(); + users.add(user); + realmAssignmentBean.setAvailableUsers(users); + when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList()); + + realmAssignmentBean.setSuperAdmin("user1", true); + + verify(realmAssignmentServiceClient).setSuperAdmin("user1", true); + } + + @Test + void testResetForm() { + realmAssignmentBean.setSelectedUserId("user1"); + realmAssignmentBean.setSelectedRealmName("realm1"); + + realmAssignmentBean.resetForm(); + + assertNull(realmAssignmentBean.getSelectedUserId()); + assertNull(realmAssignmentBean.getSelectedRealmName()); + } + + @Test + void testGetFilteredAssignments_NoFilters() { + List assignments = new ArrayList<>(); + assignments.add(RealmAssignmentDTO.builder().username("user1").realmName("realm1").build()); + realmAssignmentBean.setAssignments(assignments); + realmAssignmentBean.setFilterUserName(null); + realmAssignmentBean.setFilterRealmName(null); + + List filtered = realmAssignmentBean.getFilteredAssignments(); + + assertEquals(1, filtered.size()); + } + + @Test + void testGetFilteredAssignments_WithFilters() { + List assignments = new ArrayList<>(); + assignments.add(RealmAssignmentDTO.builder().username("user1").realmName("realm1").build()); + assignments.add(RealmAssignmentDTO.builder().username("user2").realmName("realm2").build()); + realmAssignmentBean.setAssignments(assignments); + realmAssignmentBean.setFilterUserName("user1"); + realmAssignmentBean.setFilterRealmName("realm1"); + + List filtered = realmAssignmentBean.getFilteredAssignments(); + + assertEquals(1, filtered.size()); + assertEquals("user1", filtered.get(0).getUsername()); + } + + @Test + void testGetTotalAssignments() { + List assignments = new ArrayList<>(); + assignments.add(RealmAssignmentDTO.builder().build()); + assignments.add(RealmAssignmentDTO.builder().build()); + realmAssignmentBean.setAssignments(assignments); + + assertEquals(2, realmAssignmentBean.getTotalAssignments()); + } + + @Test + void testGetActiveAssignmentsCount() { + List assignments = new ArrayList<>(); + assignments.add(RealmAssignmentDTO.builder().active(true).build()); + assignments.add(RealmAssignmentDTO.builder().active(false).build()); + assignments.add(RealmAssignmentDTO.builder().active(true).build()); + realmAssignmentBean.setAssignments(assignments); + + assertEquals(2, realmAssignmentBean.getActiveAssignmentsCount()); + } + + @Test + void testGetSuperAdminsCount() { + List assignments = new ArrayList<>(); + assignments.add(RealmAssignmentDTO.builder().isSuperAdmin(true).build()); + assignments.add(RealmAssignmentDTO.builder().isSuperAdmin(false).build()); + assignments.add(RealmAssignmentDTO.builder().isSuperAdmin(true).build()); + realmAssignmentBean.setAssignments(assignments); + + assertEquals(2, realmAssignmentBean.getSuperAdminsCount()); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/RoleGestionBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/RoleGestionBeanTest.java new file mode 100644 index 0000000..acd48d6 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/RoleGestionBeanTest.java @@ -0,0 +1,368 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.RoleServiceClient; +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.dto.user.UserDTO; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.ExternalContext; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RoleGestionBeanTest { + + @Mock + RoleServiceClient roleServiceClient; + + @Mock + FacesContext facesContext; + + @Mock + ExternalContext externalContext; + + @InjectMocks + RoleGestionBean roleGestionBean; + + MockedStatic facesContextMock; + + private static final String REALM_NAME = "lions-user-manager"; + private static final String CLIENT_NAME = "test-client"; + + @BeforeEach + void setUp() { + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + when(facesContext.getExternalContext()).thenReturn(externalContext); + } + + @AfterEach + void tearDown() { + facesContextMock.close(); + } + + @Test + void testInit() { + roleGestionBean.init(); + + assertNotNull(roleGestionBean.getRealmRoles()); + assertNotNull(roleGestionBean.getClientRoles()); + assertNotNull(roleGestionBean.getAllRoles()); + assertEquals(REALM_NAME, roleGestionBean.getRealmName()); + } + + @Test + void testLoadRealmRoles() { + List roles = Collections.singletonList( + RoleDTO.builder().id("1").name("admin").build()); + when(roleServiceClient.getAllRealmRoles(REALM_NAME)).thenReturn(roles); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.loadRealmRoles(); + + assertFalse(roleGestionBean.getRealmRoles().isEmpty()); + assertEquals(1, roleGestionBean.getRealmRoles().size()); + } + + @Test + void testLoadRealmRolesEmptyRealm() { + roleGestionBean.setRealmName(""); + roleGestionBean.loadRealmRoles(); + + verify(roleServiceClient, never()).getAllRealmRoles(anyString()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLoadRealmRolesError() { + when(roleServiceClient.getAllRealmRoles(REALM_NAME)) + .thenThrow(new RuntimeException("Error")); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.loadRealmRoles(); + + assertTrue(roleGestionBean.getRealmRoles().isEmpty()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLoadClientRoles() { + List roles = Collections.singletonList( + RoleDTO.builder().id("1").name("client-role").build()); + when(roleServiceClient.getAllClientRoles(CLIENT_NAME, REALM_NAME)).thenReturn(roles); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.setClientName(CLIENT_NAME); + roleGestionBean.loadClientRoles(); + + assertFalse(roleGestionBean.getClientRoles().isEmpty()); + assertEquals(1, roleGestionBean.getClientRoles().size()); + } + + @Test + void testLoadClientRolesEmptyClient() { + roleGestionBean.setClientName(""); + roleGestionBean.loadClientRoles(); + + verify(roleServiceClient, never()).getAllClientRoles(anyString(), anyString()); + } + + @Test + void testCreateRealmRole() { + RoleDTO newRole = RoleDTO.builder().name("new-role").description("New role").build(); + RoleDTO created = RoleDTO.builder().id("1").name("new-role").description("New role").build(); + + when(roleServiceClient.createRealmRole(any(RoleDTO.class), eq(REALM_NAME))) + .thenReturn(created); + when(roleServiceClient.getAllRealmRoles(REALM_NAME)) + .thenReturn(Collections.singletonList(created)); + + roleGestionBean.setNewRole(newRole); + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.createRealmRole(); + + verify(roleServiceClient).createRealmRole(any(RoleDTO.class), eq(REALM_NAME)); + verify(roleServiceClient).getAllRealmRoles(REALM_NAME); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testCreateRealmRoleError() { + RoleDTO newRole = RoleDTO.builder().name("new-role").build(); + when(roleServiceClient.createRealmRole(any(RoleDTO.class), eq(REALM_NAME))) + .thenThrow(new RuntimeException("Error")); + + roleGestionBean.setNewRole(newRole); + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.createRealmRole(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testCreateClientRole() { + RoleDTO newRole = RoleDTO.builder().name("client-role").build(); + RoleDTO created = RoleDTO.builder().id("1").name("client-role").build(); + + when(roleServiceClient.createClientRole(eq(CLIENT_NAME), any(RoleDTO.class), eq(REALM_NAME))) + .thenReturn(created); + when(roleServiceClient.getAllClientRoles(CLIENT_NAME, REALM_NAME)) + .thenReturn(Collections.singletonList(created)); + + roleGestionBean.setNewRole(newRole); + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.setClientName(CLIENT_NAME); + roleGestionBean.createClientRole(); + + verify(roleServiceClient).createClientRole(eq(CLIENT_NAME), any(RoleDTO.class), eq(REALM_NAME)); + verify(roleServiceClient).getAllClientRoles(CLIENT_NAME, REALM_NAME); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testCreateClientRoleEmptyClient() { + roleGestionBean.setClientName(""); + roleGestionBean.createClientRole(); + + verify(roleServiceClient, never()).createClientRole(anyString(), any(), anyString()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testDeleteRealmRole() { + // Mock pour le rechargement après suppression (retourne une liste vide) + when(roleServiceClient.getAllRealmRoles(REALM_NAME)) + .thenReturn(Collections.emptyList()); + doNothing().when(roleServiceClient).deleteRealmRole("admin", REALM_NAME); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.deleteRealmRole("admin"); + + verify(roleServiceClient).deleteRealmRole("admin", REALM_NAME); + verify(roleServiceClient, atLeastOnce()).getAllRealmRoles(REALM_NAME); + // addMessage est appelé au moins une fois (pour le succès ou l'erreur) + verify(facesContext, atLeastOnce()).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testDeleteRealmRoleError() { + doThrow(new RuntimeException("Error")) + .when(roleServiceClient).deleteRealmRole("admin", REALM_NAME); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.deleteRealmRole("admin"); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testDeleteClientRole() { + when(roleServiceClient.getAllClientRoles(CLIENT_NAME, REALM_NAME)) + .thenReturn(Collections.emptyList()); + doNothing().when(roleServiceClient).deleteClientRole(CLIENT_NAME, "client-role", REALM_NAME); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.setClientName(CLIENT_NAME); + roleGestionBean.deleteClientRole("client-role"); + + verify(roleServiceClient).deleteClientRole(CLIENT_NAME, "client-role", REALM_NAME); + verify(roleServiceClient).getAllClientRoles(CLIENT_NAME, REALM_NAME); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testDeleteClientRoleEmptyClient() { + roleGestionBean.setClientName(""); + roleGestionBean.deleteClientRole("client-role"); + + verify(roleServiceClient, never()).deleteClientRole(anyString(), anyString(), anyString()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testAssignRoleToUser() { + doNothing().when(roleServiceClient).assignRealmRolesToUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.assignRoleToUser("user-1", "admin"); + + verify(roleServiceClient).assignRealmRolesToUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testAssignRoleToUserError() { + doThrow(new RuntimeException("Error")) + .when(roleServiceClient).assignRealmRolesToUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.assignRoleToUser("user-1", "admin"); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testRevokeRoleFromUser() { + doNothing().when(roleServiceClient).revokeRealmRolesFromUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.revokeRoleFromUser("user-1", "admin"); + + verify(roleServiceClient).revokeRealmRolesFromUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testRevokeRoleFromUserError() { + doThrow(new RuntimeException("Error")) + .when(roleServiceClient).revokeRealmRolesFromUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.revokeRoleFromUser("user-1", "admin"); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testAssignRoleFromParams() { + Map params = new HashMap<>(); + params.put("userId", "user-1"); + params.put("roleName", "admin"); + when(externalContext.getRequestParameterMap()).thenReturn(params); + doNothing().when(roleServiceClient).assignRealmRolesToUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.assignRoleFromParams(); + + verify(roleServiceClient).assignRealmRolesToUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + } + + @Test + void testAssignRoleFromParamsMissing() { + Map params = new HashMap<>(); + when(externalContext.getRequestParameterMap()).thenReturn(params); + + roleGestionBean.assignRoleFromParams(); + + verify(roleServiceClient, never()).assignRealmRolesToUser(anyString(), anyString(), any()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testRevokeRoleFromParams() { + Map params = new HashMap<>(); + params.put("userId", "user-1"); + params.put("roleName", "admin"); + when(externalContext.getRequestParameterMap()).thenReturn(params); + doNothing().when(roleServiceClient).revokeRealmRolesFromUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + + roleGestionBean.setRealmName(REALM_NAME); + roleGestionBean.revokeRoleFromParams(); + + verify(roleServiceClient).revokeRealmRolesFromUser(eq("user-1"), eq(REALM_NAME), any(RoleServiceClient.RoleAssignmentRequest.class)); + } + + @Test + void testGetUserRolesDTOs() { + UserDTO user = UserDTO.builder() + .id("user-1") + .realmRoles(List.of("admin", "user_manager")) + .build(); + RoleDTO role1 = RoleDTO.builder().id("1").name("admin").build(); + RoleDTO role2 = RoleDTO.builder().id("2").name("user_manager").build(); + RoleDTO role3 = RoleDTO.builder().id("3").name("other").build(); + + roleGestionBean.setAllRoles(List.of(role1, role2, role3)); + + List result = roleGestionBean.getUserRolesDTOs(user); + + assertEquals(2, result.size()); + assertTrue(result.stream().anyMatch(r -> r.getName().equals("admin"))); + assertTrue(result.stream().anyMatch(r -> r.getName().equals("user_manager"))); + } + + @Test + void testGetUserRolesDTOsNullUser() { + List result = roleGestionBean.getUserRolesDTOs(null); + assertTrue(result.isEmpty()); + } + + @Test + void testGetUserRolesDTOsEmptyRoles() { + UserDTO user = UserDTO.builder().id("user-1").realmRoles(Collections.emptyList()).build(); + List result = roleGestionBean.getUserRolesDTOs(user); + assertTrue(result.isEmpty()); + } + + @Test + void testResetForm() { + RoleDTO role = RoleDTO.builder().name("test").build(); + roleGestionBean.setNewRole(role); + roleGestionBean.setEditMode(true); + + roleGestionBean.resetForm(); + + assertNotNull(roleGestionBean.getNewRole()); + assertFalse(roleGestionBean.isEditMode()); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/SettingsBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/SettingsBeanTest.java new file mode 100644 index 0000000..c5c6f6b --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/SettingsBeanTest.java @@ -0,0 +1,77 @@ +package dev.lions.user.manager.client.view; + +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SettingsBeanTest { + + @Mock + UserSessionBean userSessionBean; + + @Mock + GuestPreferences guestPreferences; + + @Mock + FacesContext facesContext; + + @InjectMocks + SettingsBean settingsBean; + + MockedStatic facesContextMock; + + @BeforeEach + void setUp() { + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + } + + @AfterEach + void tearDown() { + if (facesContextMock != null) { + facesContextMock.close(); + } + } + + @Test + void testInit() { + settingsBean.init(); + // Vérifier que l'initialisation se fait sans erreur + assertNotNull(settingsBean); + } + + @Test + void testSavePreferences() { + settingsBean.savePreferences(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testSavePreferencesError() { + // Simuler une erreur lors de l'accès à FacesContext + facesContextMock.when(FacesContext::getCurrentInstance).thenThrow(new RuntimeException("Error")); + + // Le bean devrait gérer l'erreur gracieusement + assertDoesNotThrow(() -> { + try { + settingsBean.savePreferences(); + } catch (Exception e) { + // L'erreur est attendue dans ce cas + } + }); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/UserCreationBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/UserCreationBeanTest.java new file mode 100644 index 0000000..f9e2a80 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/UserCreationBeanTest.java @@ -0,0 +1,171 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.UserServiceClient; +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.enums.user.StatutUser; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserCreationBeanTest { + + @Mock + UserServiceClient userServiceClient; + + @Mock + FacesContext facesContext; + + @InjectMocks + UserCreationBean userCreationBean; + + MockedStatic facesContextMock; + + private static final String REALM_NAME = "lions-user-manager"; + + @BeforeEach + void setUp() { + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + } + + @AfterEach + void tearDown() { + if (facesContextMock != null) { + facesContextMock.close(); + } + } + + @Test + void testInit() { + userCreationBean.init(); + + assertNotNull(userCreationBean.getNewUser()); + assertTrue(Boolean.TRUE.equals(userCreationBean.getNewUser().getEnabled())); + assertFalse(Boolean.TRUE.equals(userCreationBean.getNewUser().getEmailVerified())); + assertEquals(StatutUser.ACTIF, userCreationBean.getNewUser().getStatut()); + assertEquals(REALM_NAME, userCreationBean.getRealmName()); + } + + @Test + void testCreateUser() { + UserDTO newUser = UserDTO.builder() + .username("newuser") + .email("newuser@example.com") + .prenom("John") + .nom("Doe") + .build(); + UserDTO createdUser = UserDTO.builder() + .id("user-123") + .username("newuser") + .email("newuser@example.com") + .build(); + + when(userServiceClient.createUser(any(UserDTO.class), eq(REALM_NAME))) + .thenReturn(createdUser); + doNothing().when(userServiceClient).resetPassword(eq("user-123"), eq(REALM_NAME), any(UserServiceClient.PasswordResetRequest.class)); + + userCreationBean.setNewUser(newUser); + userCreationBean.setPassword("password123"); + userCreationBean.setPasswordConfirm("password123"); + userCreationBean.setRealmName(REALM_NAME); + + String result = userCreationBean.createUser(); + + assertEquals("userListPage", result); + verify(userServiceClient).createUser(any(UserDTO.class), eq(REALM_NAME)); + verify(userServiceClient).resetPassword(eq("user-123"), eq(REALM_NAME), any(UserServiceClient.PasswordResetRequest.class)); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testCreateUserEmptyPassword() { + userCreationBean.setPassword(""); + userCreationBean.setPasswordConfirm(""); + + String result = userCreationBean.createUser(); + + assertNull(result); + verify(userServiceClient, never()).createUser(any(), anyString()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testCreateUserPasswordMismatch() { + userCreationBean.setPassword("password1"); + userCreationBean.setPasswordConfirm("password2"); + + String result = userCreationBean.createUser(); + + assertNull(result); + verify(userServiceClient, never()).createUser(any(), anyString()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testCreateUserPasswordTooShort() { + userCreationBean.setPassword("short"); + userCreationBean.setPasswordConfirm("short"); + + String result = userCreationBean.createUser(); + + assertNull(result); + verify(userServiceClient, never()).createUser(any(), anyString()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testCreateUserError() { + when(userServiceClient.createUser(any(UserDTO.class), eq(REALM_NAME))) + .thenThrow(new RuntimeException("Error")); + + userCreationBean.setPassword("password123"); + userCreationBean.setPasswordConfirm("password123"); + userCreationBean.setRealmName(REALM_NAME); + + String result = userCreationBean.createUser(); + + assertNull(result); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testResetForm() { + UserDTO user = UserDTO.builder() + .username("testuser") + .email("test@example.com") + .build(); + userCreationBean.setNewUser(user); + userCreationBean.setPassword("password123"); + userCreationBean.setPasswordConfirm("password123"); + + userCreationBean.resetForm(); + + assertNotNull(userCreationBean.getNewUser()); + assertNull(userCreationBean.getPassword()); + assertNull(userCreationBean.getPasswordConfirm()); + assertTrue(Boolean.TRUE.equals(userCreationBean.getNewUser().getEnabled())); + assertFalse(Boolean.TRUE.equals(userCreationBean.getNewUser().getEmailVerified())); + assertEquals(StatutUser.ACTIF, userCreationBean.getNewUser().getStatut()); + } + + @Test + void testCancel() { + String result = userCreationBean.cancel(); + + assertEquals("userListPage", result); + assertNotNull(userCreationBean.getNewUser()); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/UserListBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/UserListBeanTest.java new file mode 100644 index 0000000..c0ff4cc --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/UserListBeanTest.java @@ -0,0 +1,104 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.UserServiceClient; +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.primefaces.event.data.PageEvent; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserListBeanTest { + + @Mock + UserServiceClient userServiceClient; + + @Mock + FacesContext facesContext; + + @InjectMocks + UserListBean userListBean; + + MockedStatic facesContextMock; + + @BeforeEach + void setUp() { + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + } + + @AfterEach + void tearDown() { + facesContextMock.close(); + } + + @Test + void testInit() { + UserSearchResultDTO result = new UserSearchResultDTO(); + result.setUsers(Collections.singletonList(new UserDTO())); + result.setTotalCount(1L); + when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(result); + + userListBean.init(); + + assertFalse(userListBean.getUsers().isEmpty()); + assertEquals(1, userListBean.getTotalRecords()); + } + + @Test + void testSearch() { + UserSearchResultDTO result = new UserSearchResultDTO(); + result.setUsers(Collections.singletonList(new UserDTO())); + result.setTotalCount(10L); + when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(result); + + userListBean.setSearchText("test"); + userListBean.search(); + + assertFalse(userListBean.getUsers().isEmpty()); + assertEquals(0, userListBean.getCurrentPage()); // Should reset to 0 + verify(facesContext).addMessage(any(), any(FacesMessage.class)); + } + + @Test + void testOnPageChange() { + UserSearchResultDTO result = new UserSearchResultDTO(); + result.setUsers(Collections.emptyList()); + when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(result); + + PageEvent event = mock(PageEvent.class); + when(event.getPage()).thenReturn(2); + + userListBean.onPageChange(event); + + assertEquals(2, userListBean.getCurrentPage()); + verify(userServiceClient).searchUsers(any(UserSearchCriteriaDTO.class)); + } + + @Test + void testActivateUser() { + doNothing().when(userServiceClient).activateUser(anyString(), anyString()); + // mock loadUsers calls searchUsers + when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(new UserSearchResultDTO()); + + userListBean.activateUser("1"); + + verify(userServiceClient).activateUser(eq("1"), anyString()); + verify(facesContext).addMessage(any(), any(FacesMessage.class)); + } +} diff --git a/src/test/java/dev/lions/user/manager/client/view/UserProfilBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/UserProfilBeanTest.java new file mode 100644 index 0000000..62f34a8 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/UserProfilBeanTest.java @@ -0,0 +1,351 @@ +package dev.lions.user.manager.client.view; + +import dev.lions.user.manager.client.service.RestClientExceptionMapper; +import dev.lions.user.manager.client.service.UserServiceClient; +import dev.lions.user.manager.dto.user.UserDTO; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.ExternalContext; +import jakarta.faces.context.FacesContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class UserProfilBeanTest { + + @Mock + UserServiceClient userServiceClient; + + @Mock + RoleGestionBean roleGestionBean; + + @Mock + FacesContext facesContext; + + @Mock + ExternalContext externalContext; + + @InjectMocks + UserProfilBean userProfilBean; + + MockedStatic facesContextMock; + + private static final String USER_ID = "test-user-id"; + private static final String REALM_NAME = "lions-user-manager"; + + @BeforeEach + void setUp() { + facesContextMock = mockStatic(FacesContext.class); + facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext); + + when(facesContext.getExternalContext()).thenReturn(externalContext); + + Map params = new HashMap<>(); + params.put("userId", USER_ID); + when(externalContext.getRequestParameterMap()).thenReturn(params); + } + + @AfterEach + void tearDown() { + facesContextMock.close(); + } + + @Test + void testInitWithUserId() { + UserDTO user = UserDTO.builder() + .id(USER_ID) + .username("testuser") + .build(); + when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user); + + userProfilBean.init(); + + assertNotNull(userProfilBean.getUser()); + assertEquals(USER_ID, userProfilBean.getUserId()); + assertEquals(REALM_NAME, userProfilBean.getRealmName()); + verify(roleGestionBean).setRealmName(REALM_NAME); + verify(roleGestionBean).loadRealmRoles(); + } + + @Test + void testInitWithoutUserId() { + Map emptyParams = new HashMap<>(); + when(externalContext.getRequestParameterMap()).thenReturn(emptyParams); + + userProfilBean.init(); + + assertNull(userProfilBean.getUser()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLoadUser() { + UserDTO user = UserDTO.builder() + .id(USER_ID) + .username("testuser") + .build(); + when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.loadUser(); + + assertNotNull(userProfilBean.getUser()); + assertEquals("testuser", userProfilBean.getUser().getUsername()); + } + + @Test + void testLoadUserNotFound() { + when(userServiceClient.getUserById(USER_ID, REALM_NAME)) + .thenThrow(new RestClientExceptionMapper.NotFoundException("User not found")); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.loadUser(); + + assertNull(userProfilBean.getUser()); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLoadUserError() { + when(userServiceClient.getUserById(USER_ID, REALM_NAME)) + .thenThrow(new RuntimeException("Error")); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.loadUser(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testEnableEditMode() { + userProfilBean.enableEditMode(); + assertTrue(userProfilBean.isEditMode()); + } + + @Test + void testCancelEdit() { + UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build(); + when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.setEditMode(true); + userProfilBean.cancelEdit(); + + assertFalse(userProfilBean.isEditMode()); + verify(userServiceClient).getUserById(USER_ID, REALM_NAME); + } + + @Test + void testUpdateUser() { + UserDTO user = UserDTO.builder() + .id(USER_ID) + .username("testuser") + .email("test@example.com") + .build(); + UserDTO updatedUser = UserDTO.builder() + .id(USER_ID) + .username("testuser") + .email("updated@example.com") + .build(); + + when(userServiceClient.updateUser(eq(USER_ID), any(UserDTO.class), eq(REALM_NAME))) + .thenReturn(updatedUser); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.setUser(user); + userProfilBean.setEditMode(true); + userProfilBean.updateUser(); + + assertFalse(userProfilBean.isEditMode()); + verify(userServiceClient).updateUser(eq(USER_ID), any(UserDTO.class), eq(REALM_NAME)); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testUpdateUserError() { + UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build(); + when(userServiceClient.updateUser(eq(USER_ID), any(UserDTO.class), eq(REALM_NAME))) + .thenThrow(new RuntimeException("Error")); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.setUser(user); + userProfilBean.updateUser(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testResetPassword() { + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.setNewPassword("newPassword123"); + userProfilBean.setNewPasswordConfirm("newPassword123"); + + doNothing().when(userServiceClient).resetPassword(eq(USER_ID), eq(REALM_NAME), any(UserServiceClient.PasswordResetRequest.class)); + + userProfilBean.resetPassword(); + + assertNull(userProfilBean.getNewPassword()); + assertNull(userProfilBean.getNewPasswordConfirm()); + verify(userServiceClient).resetPassword(eq(USER_ID), eq(REALM_NAME), any(UserServiceClient.PasswordResetRequest.class)); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testResetPasswordEmpty() { + userProfilBean.setNewPassword(""); + userProfilBean.resetPassword(); + + verify(userServiceClient, never()).resetPassword(anyString(), anyString(), any(UserServiceClient.PasswordResetRequest.class)); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testResetPasswordMismatch() { + userProfilBean.setNewPassword("password1"); + userProfilBean.setNewPasswordConfirm("password2"); + userProfilBean.resetPassword(); + + verify(userServiceClient, never()).resetPassword(anyString(), anyString(), any(UserServiceClient.PasswordResetRequest.class)); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testResetPasswordError() { + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.setNewPassword("newPassword123"); + userProfilBean.setNewPasswordConfirm("newPassword123"); + + doThrow(new RuntimeException("Error")) + .when(userServiceClient).resetPassword(eq(USER_ID), eq(REALM_NAME), any(UserServiceClient.PasswordResetRequest.class)); + + userProfilBean.resetPassword(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testActivateUser() { + UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build(); + when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user); + doNothing().when(userServiceClient).activateUser(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.activateUser(); + + verify(userServiceClient).activateUser(USER_ID, REALM_NAME); + verify(userServiceClient).getUserById(USER_ID, REALM_NAME); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testActivateUserError() { + doThrow(new RuntimeException("Error")) + .when(userServiceClient).activateUser(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.activateUser(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testDeactivateUser() { + UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build(); + when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user); + doNothing().when(userServiceClient).deactivateUser(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.deactivateUser(); + + verify(userServiceClient).deactivateUser(USER_ID, REALM_NAME); + verify(userServiceClient).getUserById(USER_ID, REALM_NAME); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testDeactivateUserError() { + doThrow(new RuntimeException("Error")) + .when(userServiceClient).deactivateUser(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.deactivateUser(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testSendVerificationEmail() { + doNothing().when(userServiceClient).sendVerificationEmail(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.sendVerificationEmail(); + + verify(userServiceClient).sendVerificationEmail(USER_ID, REALM_NAME); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testSendVerificationEmailError() { + doThrow(new RuntimeException("Error")) + .when(userServiceClient).sendVerificationEmail(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.sendVerificationEmail(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLogoutAllSessions() { + doNothing().when(userServiceClient).logoutAllSessions(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.logoutAllSessions(); + + verify(userServiceClient).logoutAllSessions(USER_ID, REALM_NAME); + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } + + @Test + void testLogoutAllSessionsError() { + doThrow(new RuntimeException("Error")) + .when(userServiceClient).logoutAllSessions(USER_ID, REALM_NAME); + + userProfilBean.setUserId(USER_ID); + userProfilBean.setRealmName(REALM_NAME); + userProfilBean.logoutAllSessions(); + + verify(facesContext).addMessage(isNull(), any(FacesMessage.class)); + } +} + diff --git a/src/test/java/dev/lions/user/manager/client/view/UserSessionBeanTest.java b/src/test/java/dev/lions/user/manager/client/view/UserSessionBeanTest.java new file mode 100644 index 0000000..5bde633 --- /dev/null +++ b/src/test/java/dev/lions/user/manager/client/view/UserSessionBeanTest.java @@ -0,0 +1,298 @@ +package dev.lions.user.manager.client.view; + +import io.quarkus.oidc.IdToken; +import io.quarkus.oidc.OidcSession; +import io.quarkus.security.identity.SecurityIdentity; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class UserSessionBeanTest { + + @Mock + SecurityIdentity securityIdentity; + + @Mock + @IdToken + JsonWebToken idToken; + + @Mock + OidcSession oidcSession; + + @InjectMocks + UserSessionBean userSessionBean; + + @BeforeEach + void setUp() { + // Configuration par défaut pour les tests + } + + @Test + void testLoadUserInfoWithToken() { + when(securityIdentity.isAnonymous()).thenReturn(false); + when(idToken.getClaim("preferred_username")).thenReturn("testuser"); + when(idToken.getClaim("email")).thenReturn("test@example.com"); + when(idToken.getClaim("given_name")).thenReturn("John"); + when(idToken.getClaim("family_name")).thenReturn("Doe"); + when(idToken.getClaim("name")).thenReturn("John Doe"); + + userSessionBean.loadUserInfo(); + + assertEquals("testuser", userSessionBean.getUsername()); + assertEquals("test@example.com", userSessionBean.getEmail()); + assertEquals("John", userSessionBean.getFirstName()); + assertEquals("Doe", userSessionBean.getLastName()); + assertEquals("John Doe", userSessionBean.getFullName()); + assertEquals("JD", userSessionBean.getInitials()); + } + + @Test + void testLoadUserInfoAnonymous() { + when(securityIdentity.isAnonymous()).thenReturn(true); + + userSessionBean.loadUserInfo(); + + assertEquals("Utilisateur", userSessionBean.getUsername()); + assertEquals("utilisateur@lions.dev", userSessionBean.getEmail()); + assertEquals("Utilisateur", userSessionBean.getFullName()); + assertEquals("U", userSessionBean.getInitials()); + } + + @Test + void testLoadUserInfoNullToken() { + when(securityIdentity.isAnonymous()).thenReturn(true); + // idToken is null by default when securityIdentity.isAnonymous() is true + + userSessionBean.loadUserInfo(); + + assertEquals("Utilisateur", userSessionBean.getUsername()); + } + + @Test + void testGenerateInitials() { + // Test avec nom complet + when(securityIdentity.isAnonymous()).thenReturn(false); + when(idToken.getClaim("name")).thenReturn("John Doe"); + when(idToken.getClaim("preferred_username")).thenReturn("testuser"); + when(idToken.getClaim("email")).thenReturn("test@example.com"); + when(idToken.getClaim("given_name")).thenReturn("John"); + when(idToken.getClaim("family_name")).thenReturn("Doe"); + + userSessionBean.loadUserInfo(); + + assertEquals("JD", userSessionBean.getInitials()); + } + + @Test + void testGenerateInitialsSingleName() { + when(securityIdentity.isAnonymous()).thenReturn(false); + when(idToken.getClaim("name")).thenReturn("John"); + when(idToken.getClaim("preferred_username")).thenReturn("testuser"); + when(idToken.getClaim("email")).thenReturn("test@example.com"); + when(idToken.getClaim("given_name")).thenReturn("John"); + when(idToken.getClaim("family_name")).thenReturn(null); + + userSessionBean.loadUserInfo(); + + assertEquals("JO", userSessionBean.getInitials()); + } + + @Test + void testGetPrimaryRole() { + when(securityIdentity.isAnonymous()).thenReturn(false); + when(securityIdentity.getRoles()).thenReturn(Set.of("admin", "user_manager")); + // Load user info first to initialize the bean + userSessionBean.loadUserInfo(); + + String primaryRole = userSessionBean.getPrimaryRole(); + + assertEquals("Administrateur", primaryRole); + } + + @Test + void testGetPrimaryRoleUserManager() { + when(securityIdentity.isAnonymous()).thenReturn(false); + when(securityIdentity.getRoles()).thenReturn(Set.of("user_manager")); + // Load user info first to initialize the bean + userSessionBean.loadUserInfo(); + + String primaryRole = userSessionBean.getPrimaryRole(); + + assertEquals("Gestionnaire", primaryRole); + } + + @Test + void testGetPrimaryRoleUserViewer() { + when(securityIdentity.isAnonymous()).thenReturn(false); + when(securityIdentity.getRoles()).thenReturn(Set.of("user_viewer")); + // Load user info first to initialize the bean + userSessionBean.loadUserInfo(); + + String primaryRole = userSessionBean.getPrimaryRole(); + + assertEquals("Consultant", primaryRole); + } + + @Test + void testGetRoles() { + when(securityIdentity.isAnonymous()).thenReturn(false); + when(securityIdentity.getRoles()).thenReturn(Set.of("admin", "user_manager")); + // Load user info first to initialize the bean + userSessionBean.loadUserInfo(); + + Set roles = userSessionBean.getRoles(); + + assertFalse(roles.isEmpty()); + assertTrue(roles.contains("admin")); + assertTrue(roles.contains("user_manager")); + } + + @Test + void testGetRolesAnonymous() { + when(securityIdentity.isAnonymous()).thenReturn(true); + // Load user info first to initialize the bean + userSessionBean.loadUserInfo(); + + Set roles = userSessionBean.getRoles(); + + assertFalse(roles.isEmpty()); + assertTrue(roles.contains("Utilisateur")); + } + + @Test + void testIsAuthenticated() { + when(securityIdentity.isAnonymous()).thenReturn(false); + + assertTrue(userSessionBean.isAuthenticated()); + } + + @Test + void testIsAuthenticatedAnonymous() { + when(securityIdentity.isAnonymous()).thenReturn(true); + + assertFalse(userSessionBean.isAuthenticated()); + } + + @Test + void testHasRole() { + when(securityIdentity.isAnonymous()).thenReturn(false); + when(securityIdentity.getRoles()).thenReturn(Set.of("admin", "user_manager")); + // Load user info first to initialize the bean + userSessionBean.loadUserInfo(); + + assertTrue(userSessionBean.hasRole("admin")); + assertTrue(userSessionBean.hasRole("user_manager")); + assertFalse(userSessionBean.hasRole("auditor")); + } + + @Test + void testHasRoleAnonymous() { + when(securityIdentity.isAnonymous()).thenReturn(true); + // Load user info first to initialize the bean + userSessionBean.loadUserInfo(); + + assertFalse(userSessionBean.hasRole("admin")); + } + + @Test + void testGetIssuer() { + when(idToken.getIssuer()).thenReturn("https://security.lions.dev/realms/master"); + + String issuer = userSessionBean.getIssuer(); + + assertEquals("https://security.lions.dev/realms/master", issuer); + } + + @Test + void testGetIssuerNull() { + // Mock idToken.getIssuer() to throw an exception to simulate null token + when(idToken.getIssuer()).thenThrow(new RuntimeException("Token is null")); + + String issuer = userSessionBean.getIssuer(); + + assertEquals("Non disponible", issuer); + } + + @Test + void testGetSubject() { + when(idToken.getSubject()).thenReturn("user-123"); + + String subject = userSessionBean.getSubject(); + + assertEquals("user-123", subject); + } + + @Test + void testGetSessionId() { + when(idToken.getClaim("sid")).thenReturn("session-123"); + + String sessionId = userSessionBean.getSessionId(); + + assertEquals("session-123", sessionId); + } + + @Test + void testGetExpirationTime() { + when(idToken.getExpirationTime()).thenReturn(1735689600L); // 2025-01-01 00:00:00 UTC + + java.util.Date expiration = userSessionBean.getExpirationTime(); + + assertNotNull(expiration); + } + + @Test + void testGetIssuedAt() { + when(idToken.getIssuedAtTime()).thenReturn(1735603200L); // 2024-12-31 00:00:00 UTC + + java.util.Date issuedAt = userSessionBean.getIssuedAt(); + + assertNotNull(issuedAt); + } + + @Test + void testGetAudience() { + when(idToken.getAudience()).thenReturn(Set.of("client1", "client2")); + + String audience = userSessionBean.getAudience(); + + assertTrue(audience.contains("client1")); + assertTrue(audience.contains("client2")); + } + + @Test + void testGetAuthorizedParty() { + when(idToken.getClaim("azp")).thenReturn("lions-user-manager-client"); + + String azp = userSessionBean.getAuthorizedParty(); + + assertEquals("lions-user-manager-client", azp); + } + + @Test + void testIsEmailVerified() { + when(idToken.getClaim("email_verified")).thenReturn(true); + + assertTrue(userSessionBean.isEmailVerified()); + } + + @Test + void testIsEmailVerifiedFalse() { + when(idToken.getClaim("email_verified")).thenReturn(false); + + assertFalse(userSessionBean.isEmailVerified()); + } +} +