From 9fce8f1d0a5855bd867111580eb6c68472846e4b Mon Sep 17 00:00:00 2001 From: lionsdev Date: Wed, 18 Feb 2026 03:28:07 +0000 Subject: [PATCH] feat(client): corrections UI/UX pages dashboard, audit, roles, users - fix REST clients, KPI, navigation et formulaires Co-authored-by: Cursor --- BUILD-SYNC-API.md | 19 ++ script/docker/dependencies-docker-compose.yml | 37 +++ script/docker/docker-compose.yml | 49 ++++ script/docker/run-dev.bat | 5 + script/docker/run-dev.sh | 7 + src/main/docker/Dockerfile.jvm | 11 + .../manager/client/api/SyncRestClient.java | 33 --- .../manager/client/api/UserRestClient.java | 22 +- .../ViewExpiredExceptionHandler.java | 61 ++++ .../ViewExpiredExceptionHandlerFactory.java | 21 ++ .../client/service/AuditServiceClient.java | 2 + .../service/RealmAssignmentServiceClient.java | 2 + .../client/service/RealmServiceClient.java | 2 + .../client/service/RoleServiceClient.java | 2 + .../client/service/SyncServiceClient.java | 15 +- .../service/UserMetricsServiceClient.java | 21 ++ .../client/service/UserServiceClient.java | 2 + .../client/view/AuditConsultationBean.java | 194 ++++++------- .../manager/client/view/DashboardBean.java | 150 +++++----- .../manager/client/view/RoleGestionBean.java | 77 +++-- .../client/view/SyncDashboardBean.java | 104 ++++++- .../manager/client/view/UserCreationBean.java | 67 +++-- .../manager/client/view/UserListBean.java | 175 ++++++++---- src/main/resources/META-INF/faces-config.xml | 4 + .../pages/user-manager/audit/logs.xhtml | 262 ++++++++++++------ .../pages/user-manager/dashboard.xhtml | 14 +- .../pages/user-manager/roles/list.xhtml | 205 ++++++++------ .../pages/user-manager/sync/dashboard.xhtml | 72 +++++ .../resources/pages/user-manager/users.xhtml | 11 +- .../pages/user-manager/users/create.xhtml | 212 +++++++------- .../pages/user-manager/users/list.xhtml | 53 ++-- .../pages/user-manager/users/profile.xhtml | 10 - .../pages/user-manager/users/view.xhtml | 6 + .../role-management/role-card.xhtml | 19 +- .../role-management/role-form-content.xhtml | 29 +- .../role-management/role-form.xhtml | 2 + .../shared/cards/kpi-card-content.xhtml | 18 +- .../components/shared/cards/kpi-card.xhtml | 30 +- .../shared/tables/user-data-table.xhtml | 4 +- .../user-management/user-actions.xhtml | 93 ++++--- src/main/resources/application-dev.properties | 7 +- .../resources/application-prod.properties | 2 +- src/main/resources/application.properties | 6 + .../view/AuditConsultationBeanTest.java | 25 +- .../client/view/DashboardBeanTest.java | 14 + .../client/view/RoleGestionBeanTest.java | 7 +- .../client/view/UserCreationBeanTest.java | 5 + 47 files changed, 1429 insertions(+), 759 deletions(-) create mode 100644 BUILD-SYNC-API.md create mode 100644 script/docker/dependencies-docker-compose.yml create mode 100644 script/docker/docker-compose.yml create mode 100644 script/docker/run-dev.bat create mode 100644 script/docker/run-dev.sh create mode 100644 src/main/docker/Dockerfile.jvm delete mode 100644 src/main/java/dev/lions/user/manager/client/api/SyncRestClient.java create mode 100644 src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandler.java create mode 100644 src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandlerFactory.java create mode 100644 src/main/java/dev/lions/user/manager/client/service/UserMetricsServiceClient.java diff --git a/BUILD-SYNC-API.md b/BUILD-SYNC-API.md new file mode 100644 index 0000000..a13c458 --- /dev/null +++ b/BUILD-SYNC-API.md @@ -0,0 +1,19 @@ +# Build pour éviter 404 sur le dashboard Sync + +Le client utilise l’interface **SyncResourceApi** du module `lions-user-manager-server-api`. +Si vous lancez uniquement `mvn quarkus:dev` dans le client, une ancienne version de l’API (en cache dans `.m2`) peut être utilisée et provoquer des **404** sur `/api/sync/keycloak-health`, `/api/sync/users`, etc. + +**À faire une fois** (ou après toute modification de l’API) : + +Depuis la **racine** `lions-user-manager` : + +```bash +mvn clean install -pl lions-user-manager-server-api,lions-user-manager-server-impl-quarkus,lions-user-manager-client-quarkus-primefaces-freya -am +``` + +Puis lancer le serveur et le client chacun dans son terminal : + +- **Terminal 1** (serveur) : `cd lions-user-manager-server-impl-quarkus && mvn quarkus:dev` +- **Terminal 2** (client) : `cd lions-user-manager-client-quarkus-primefaces-freya && mvn quarkus:dev` + +Le client appelle le backend sur **http://localhost:8081** (configuré dans `application-dev.properties`). diff --git a/script/docker/dependencies-docker-compose.yml b/script/docker/dependencies-docker-compose.yml new file mode 100644 index 0000000..68c05d6 --- /dev/null +++ b/script/docker/dependencies-docker-compose.yml @@ -0,0 +1,37 @@ +# Dépendances pour le client : Postgres + Keycloak (et optionnellement le serveur API) +# Pour run-dev, le serveur tourne en local (mvn quarkus:dev) et le client pointe vers localhost:8080 +services: + postgres: + image: postgres:15 + environment: + POSTGRES_DB: ${DB_NAME:-lions_user_manager} + POSTGRES_USER: ${DB_USER:-lions} + POSTGRES_PASSWORD: ${DB_PASSWORD:-lions} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"] + interval: 5s + timeout: 5s + retries: 5 + + keycloak: + image: quay.io/keycloak/keycloak:26.3.3 + command: start-dev + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} + KC_DB_USERNAME: ${DB_USER:-lions} + KC_DB_PASSWORD: ${DB_PASSWORD:-lions} + KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin} + ports: + - "${KC_PORT:-8180}:8080" + depends_on: + postgres: + condition: service_healthy + +volumes: + postgres_data: diff --git a/script/docker/docker-compose.yml b/script/docker/docker-compose.yml new file mode 100644 index 0000000..cb37e4e --- /dev/null +++ b/script/docker/docker-compose.yml @@ -0,0 +1,49 @@ +# Client + Keycloak + Postgres (serveur API à lancer à part ou via stack racine) +services: + postgres: + image: postgres:15 + environment: + POSTGRES_DB: ${DB_NAME:-lions_user_manager} + POSTGRES_USER: ${DB_USER:-lions} + POSTGRES_PASSWORD: ${DB_PASSWORD:-lions} + ports: + - "${DB_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"] + interval: 5s + timeout: 5s + retries: 5 + + keycloak: + image: quay.io/keycloak/keycloak:26.3.3 + command: start-dev + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager} + KC_DB_USERNAME: ${DB_USER:-lions} + KC_DB_PASSWORD: ${DB_PASSWORD:-lions} + KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin} + ports: + - "${KC_PORT:-8180}:8080" + depends_on: + postgres: + condition: service_healthy + + lions-user-manager-client: + build: + context: ../.. + dockerfile: src/main/docker/Dockerfile.jvm + ports: + - "${CLIENT_PORT:-8082}:8082" + environment: + KEYCLOAK_SERVER_URL: http://keycloak:8080 + LIONS_USER_MANAGER_API_URL: ${LIONS_USER_MANAGER_API_URL:-http://host.docker.internal:8080} + depends_on: + keycloak: + condition: service_started + +volumes: + postgres_data: diff --git a/script/docker/run-dev.bat b/script/docker/run-dev.bat new file mode 100644 index 0000000..5e86215 --- /dev/null +++ b/script/docker/run-dev.bat @@ -0,0 +1,5 @@ +@echo off +REM Demarre les dependances (postgres + keycloak) puis le client en mode dev (mvn quarkus:dev -P dev) +cd /d "%~dp0\..\.." +docker-compose -f script/docker/dependencies-docker-compose.yml up -d +mvn quarkus:dev -P dev diff --git a/script/docker/run-dev.sh b/script/docker/run-dev.sh new file mode 100644 index 0000000..d997e63 --- /dev/null +++ b/script/docker/run-dev.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Démarre les dépendances (postgres + keycloak) puis le client en mode dev (mvn quarkus:dev -P dev) +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/../.." +docker-compose -f script/docker/dependencies-docker-compose.yml up -d +mvn quarkus:dev -P dev diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..8277526 --- /dev/null +++ b/src/main/docker/Dockerfile.jvm @@ -0,0 +1,11 @@ +FROM registry.access.redhat.com/ubi8/openjdk-17:1.20 +ENV LANGUAGE='en_US:en' +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ +EXPOSE 8082 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" +ENTRYPOINT ["/opt/jboss/container/java/run/run-java.sh"] 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 deleted file mode 100644 index 8f1700e..0000000 --- a/src/main/java/dev/lions/user/manager/client/api/SyncRestClient.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.lions.user.manager.client.api; - -import dev.lions.user.manager.client.filter.AuthHeaderFactory; -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.annotation.RegisterClientHeaders; -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; - -@RegisterRestClient(configKey = "user-api") -@RegisterClientHeaders(AuthHeaderFactory.class) -@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); - - @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 index af95cd1..f1560ee 100644 --- a/src/main/java/dev/lions/user/manager/client/api/UserRestClient.java +++ b/src/main/java/dev/lions/user/manager/client/api/UserRestClient.java @@ -1,6 +1,7 @@ package dev.lions.user.manager.client.api; import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import dev.lions.user.manager.dto.user.SessionsRevokedDTO; import dev.lions.user.manager.dto.user.UserDTO; import dev.lions.user.manager.dto.user.UserSearchResultDTO; import jakarta.ws.rs.*; @@ -46,11 +47,13 @@ public interface UserRestClient { void deleteUser(@PathParam("id") String id, @QueryParam("realm") String realmName, @QueryParam("hard") boolean hardDelete); - @PUT + // Correction : @POST (était @PUT — mismatch avec le serveur → 405) + @POST @Path("/{id}/activate") void activateUser(@PathParam("id") String id, @QueryParam("realm") String realmName); - @PUT + // Correction : @POST (était @PUT — mismatch avec le serveur → 405) + @POST @Path("/{id}/deactivate") void deactivateUser(@PathParam("id") String id, @QueryParam("realm") String realmName, @QueryParam("reason") String reason); @@ -60,16 +63,27 @@ public interface UserRestClient { void resetPassword(@PathParam("id") String id, @QueryParam("realm") String realmName, PasswordResetRequest request); + // Correction : path send-verification-email (était send-verify-email → 404) @POST - @Path("/{id}/send-verify-email") + @Path("/{id}/send-verification-email") void sendVerificationEmail(@PathParam("id") String id, @QueryParam("realm") String realmName); + // Ajout : correspondance avec UserResourceApi.logoutAllSessions() + @POST + @Path("/{id}/logout-sessions") + SessionsRevokedDTO logoutAllSessions(@PathParam("id") String id, @QueryParam("realm") String realmName); + + // Ajout : correspondance avec UserResourceApi.getActiveSessions() + @GET + @Path("/{id}/sessions") + List getActiveSessions(@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 + // Inner class pour la réinitialisation de mot de passe class PasswordResetRequest { public String password; public boolean temporary; diff --git a/src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandler.java b/src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandler.java new file mode 100644 index 0000000..7ea65da --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandler.java @@ -0,0 +1,61 @@ +package dev.lions.user.manager.client.exception; + +import jakarta.faces.application.ViewExpiredException; +import jakarta.faces.context.ExceptionHandler; +import jakarta.faces.context.ExceptionHandlerWrapper; +import jakarta.faces.context.FacesContext; +import jakarta.faces.event.ExceptionQueuedEvent; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Iterator; + +/** + * Redirige vers la page d'accueil lorsque la vue a expiré (session ou state saving), + * au lieu d'afficher une stack trace. + */ +public class ViewExpiredExceptionHandler extends ExceptionHandlerWrapper { + + private final ExceptionHandler wrapped; + + public ViewExpiredExceptionHandler(ExceptionHandler wrapped) { + this.wrapped = wrapped; + } + + @Override + public ExceptionHandler getWrapped() { + return wrapped; + } + + @Override + public void handle() { + Iterator it = getUnhandledExceptionQueuedEvents().iterator(); + while (it.hasNext()) { + ExceptionQueuedEvent event = it.next(); + Throwable t = event.getContext().getException(); + if (t instanceof ViewExpiredException) { + it.remove(); + FacesContext fc = FacesContext.getCurrentInstance(); + if (fc != null && !fc.getResponseComplete()) { + try { + String ctx = fc.getExternalContext().getRequestContextPath(); + fc.getExternalContext().redirect(ctx == null || ctx.isEmpty() ? "/" : ctx + "/"); + fc.responseComplete(); + } catch (IOException e) { + // fallback: set status and let default handling + HttpServletResponse resp = (HttpServletResponse) fc.getExternalContext().getResponse(); + if (resp != null && !resp.isCommitted()) { + resp.setStatus(HttpServletResponse.SC_FOUND); + try { + resp.sendRedirect(fc.getExternalContext().getRequestContextPath() + "/"); + } catch (IOException ignored) { + } + } + } + } + return; + } + } + getWrapped().handle(); + } +} diff --git a/src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandlerFactory.java b/src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandlerFactory.java new file mode 100644 index 0000000..f5aa5d9 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/exception/ViewExpiredExceptionHandlerFactory.java @@ -0,0 +1,21 @@ +package dev.lions.user.manager.client.exception; + +import jakarta.faces.context.ExceptionHandler; +import jakarta.faces.context.ExceptionHandlerFactory; + +/** + * Factory pour enregistrer le gestionnaire de ViewExpiredException. + */ +public class ViewExpiredExceptionHandlerFactory extends ExceptionHandlerFactory { + + private final ExceptionHandlerFactory parent; + + public ViewExpiredExceptionHandlerFactory(ExceptionHandlerFactory parent) { + this.parent = parent; + } + + @Override + public ExceptionHandler getExceptionHandler() { + return new ViewExpiredExceptionHandler(parent.getExceptionHandler()); + } +} 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 b0752f7..68c5342 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 @@ -2,6 +2,7 @@ package dev.lions.user.manager.client.service; import dev.lions.user.manager.api.AuditResourceApi; import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import jakarta.ws.rs.Path; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -9,6 +10,7 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; * REST Client pour le service d'audit. * Étend maintenant l'interface API commune définie dans server-api. */ +@Path("/api/audit") @RegisterRestClient(configKey = "lions-user-manager-api") @RegisterClientHeaders(AuthHeaderFactory.class) public interface AuditServiceClient extends AuditResourceApi { 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 index ef71146..0192789 100644 --- a/src/main/java/dev/lions/user/manager/client/service/RealmAssignmentServiceClient.java +++ b/src/main/java/dev/lions/user/manager/client/service/RealmAssignmentServiceClient.java @@ -2,6 +2,7 @@ package dev.lions.user.manager.client.service; import dev.lions.user.manager.api.RealmAssignmentResourceApi; import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import jakarta.ws.rs.Path; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -9,6 +10,7 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; * REST Client pour le service de gestion des affectations de realms. * Étend maintenant l'interface API commune définie dans server-api. */ +@Path("/api/realm-assignments") @RegisterRestClient(configKey = "lions-user-manager-api") @RegisterClientHeaders(AuthHeaderFactory.class) public interface RealmAssignmentServiceClient extends RealmAssignmentResourceApi { 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 index df81e37..bfe6804 100644 --- a/src/main/java/dev/lions/user/manager/client/service/RealmServiceClient.java +++ b/src/main/java/dev/lions/user/manager/client/service/RealmServiceClient.java @@ -2,6 +2,7 @@ package dev.lions.user.manager.client.service; import dev.lions.user.manager.api.RealmResourceApi; import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import jakarta.ws.rs.Path; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -10,6 +11,7 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; * REST Client pour le service de gestion des realms Keycloak * Étend maintenant l'interface API commune définie dans server-api. */ +@Path("/api/realms") @RegisterRestClient(configKey = "lions-user-manager-api") @RegisterClientHeaders(AuthHeaderFactory.class) @RegisterProvider(RestClientExceptionMapper.class) 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 840fabc..3957b64 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 @@ -2,6 +2,7 @@ package dev.lions.user.manager.client.service; import dev.lions.user.manager.api.RoleResourceApi; import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import jakarta.ws.rs.Path; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -9,6 +10,7 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; * REST Client pour le service de gestion des rôles. * Étend maintenant l'interface API commune définie dans server-api. */ +@Path("/api/roles") @RegisterRestClient(configKey = "lions-user-manager-api") @RegisterClientHeaders(AuthHeaderFactory.class) public interface RoleServiceClient extends RoleResourceApi { 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 50f05ac..be5eb23 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 @@ -2,26 +2,21 @@ package dev.lions.user.manager.client.service; import dev.lions.user.manager.api.SyncResourceApi; import dev.lions.user.manager.client.filter.AuthHeaderFactory; -import dev.lions.user.manager.dto.sync.HealthStatusDTO; import jakarta.ws.rs.*; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; /** - * REST Client pour le service de synchronisation - * Utilise l'interface commune définie dans server-api pour garantir la - * cohérence du contrat. + * REST Client pour le service de synchronisation. + * Étend l'interface API commune définie dans server-api pour garantir + * la cohérence du contrat client-serveur. */ +@Path("/api/sync") @RegisterRestClient(configKey = "lions-user-manager-api") @RegisterClientHeaders(AuthHeaderFactory.class) public interface SyncServiceClient extends SyncResourceApi { - // Méthodes supplémentaires spécifiques au client (ou pas encore migrées dans - // l'API commune) - - @GET - @Path("/health") - HealthStatusDTO checkHealth(@QueryParam("realm") String realmName); + // checkKeycloakHealth() hérité de SyncResourceApi → GET /api/sync/health/keycloak @GET @Path("/exists/user/{username}") diff --git a/src/main/java/dev/lions/user/manager/client/service/UserMetricsServiceClient.java b/src/main/java/dev/lions/user/manager/client/service/UserMetricsServiceClient.java new file mode 100644 index 0000000..09b5128 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/service/UserMetricsServiceClient.java @@ -0,0 +1,21 @@ +package dev.lions.user.manager.client.service; + +import dev.lions.user.manager.api.UserMetricsResourceApi; +import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import jakarta.ws.rs.Path; +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +/** + * REST Client pour l'API de métriques utilisateurs. + * Étend l'interface API commune définie dans server-api. + */ +@Path("/api/metrics/users") +@RegisterRestClient(configKey = "lions-user-manager-api") +@RegisterClientHeaders(AuthHeaderFactory.class) +@RegisterProvider(RestClientExceptionMapper.class) +public interface UserMetricsServiceClient extends UserMetricsResourceApi { + +} + 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 d4a0c2f..af5767e 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 @@ -2,6 +2,7 @@ package dev.lions.user.manager.client.service; import dev.lions.user.manager.api.UserResourceApi; import dev.lions.user.manager.client.filter.AuthHeaderFactory; +import jakarta.ws.rs.Path; import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @@ -10,6 +11,7 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; * REST Client pour le service de gestion des utilisateurs * Étend maintenant l'interface API commune définie dans server-api. */ +@Path("/api/users") @RegisterRestClient(configKey = "lions-user-manager-api") @RegisterClientHeaders(AuthHeaderFactory.class) @RegisterProvider(RestClientExceptionMapper.class) 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 b777b2e..dcc9702 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,10 +1,13 @@ 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.dto.common.CountDTO; import dev.lions.user.manager.enums.audit.TypeActionAudit; 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; @@ -12,20 +15,17 @@ import jakarta.inject.Named; import lombok.Data; import org.eclipse.microprofile.rest.client.inject.RestClient; +import java.io.OutputStream; import java.io.Serializable; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.logging.Logger; -/** - * Bean JSF pour la consultation des logs d'audit - * - * @author Lions User Manager - * @version 1.0.0 - */ @Named("auditConsultationBean") @ViewScoped @Data @@ -33,53 +33,49 @@ public class AuditConsultationBean implements Serializable { private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.getLogger(AuditConsultationBean.class.getName()); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); @Inject @RestClient private AuditServiceClient auditServiceClient; - // Liste des logs + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + private List auditLogs = new ArrayList<>(); private AuditLogDTO selectedLog; - // Filtres de recherche private String acteurUsername; - private LocalDateTime dateDebut; - private LocalDateTime dateFin; + private Date dateDebut; + private Date dateFin; private TypeActionAudit selectedTypeAction; private String ressourceType; private Boolean succes; - // Pagination private int currentPage = 0; private int pageSize = 50; private long totalRecords = 0; - // Statistiques private Map actionStatistics; private Map userActivityStatistics; - private Long failureCount = 0L; - private Long successCount = 0L; + private long failureCount = 0; + private long successCount = 0; - // Options private List typeActionOptions = List.of(TypeActionAudit.values()); private List availableRealms = new ArrayList<>(); - private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); - @PostConstruct public void init() { loadRealms(); loadStatistics(); + loadRecentLogs(); } - /** - * Rechercher des logs d'audit - */ public void searchLogs() { try { - String dateDebutStr = dateDebut != null ? dateDebut.format(DATE_FORMATTER) : null; - String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null; + String dateDebutStr = toIsoString(dateDebut); + String dateFinStr = toIsoString(dateFin); auditLogs = auditServiceClient.searchLogs( acteurUsername, @@ -91,87 +87,92 @@ public class AuditConsultationBean implements Serializable { currentPage, pageSize); + if (auditLogs == null) auditLogs = new ArrayList<>(); totalRecords = auditLogs.size(); - addSuccessMessage("Recherche effectuée: " + totalRecords + " résultat(s) trouvé(s)"); + addSuccessMessage("Recherche effectuée : " + totalRecords + " résultat(s)"); } catch (Exception e) { LOGGER.severe("Erreur lors de la recherche: " + e.getMessage()); - addErrorMessage("Erreur lors de la recherche: " + e.getMessage()); + addErrorMessage("Erreur lors de la recherche : " + e.getMessage()); } } - /** - * Charger les logs par acteur - */ - public void loadLogsByActeur(String username) { + public void loadRecentLogs() { try { - auditLogs = auditServiceClient.getLogsByActor(username, 100); - totalRecords = auditLogs.size(); + auditLogs = auditServiceClient.searchLogs( + null, null, null, null, null, null, 0, pageSize); + if (auditLogs == null) auditLogs = new ArrayList<>(); + totalRecords = successCount + failureCount; + if (totalRecords == 0) { + totalRecords = auditLogs.size(); + } } catch (Exception e) { - LOGGER.severe("Erreur lors du chargement: " + e.getMessage()); - addErrorMessage("Erreur lors du chargement: " + e.getMessage()); - } - } - - /** - * Charger les logs par realm - */ - public void loadLogsByRealm(String realmName) { - try { - // Méthode supprimée de l'API standardisée - LOGGER.warning("Recherche par realm non supportée dans l'API standardisée"); + LOGGER.severe("Erreur lors du chargement initial des logs: " + e.getMessage()); auditLogs = new ArrayList<>(); - totalRecords = 0; - } catch (Exception e) { - LOGGER.severe("Erreur lors du chargement: " + e.getMessage()); - addErrorMessage("Erreur lors du chargement: " + e.getMessage()); } } - /** - * Charger les statistiques - */ public void loadStatistics() { try { - String dateDebutStr = dateDebut != null ? dateDebut.format(DATE_FORMATTER) : null; - String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null; + String dateDebutStr = toIsoString(dateDebut); + String dateFinStr = toIsoString(dateFin); - actionStatistics = auditServiceClient.getActionStatistics(dateDebutStr, dateFinStr); - userActivityStatistics = auditServiceClient.getUserActivityStatistics(dateDebutStr, dateFinStr); + CountDTO failures = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr); + failureCount = failures != null ? failures.getCount() : 0; - dev.lions.user.manager.dto.common.CountDTO failures = auditServiceClient.getFailureCount(dateDebutStr, - dateFinStr); - failureCount = failures != null ? failures.getCount() : 0L; + CountDTO successes = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr); + successCount = successes != null ? successes.getCount() : 0; - dev.lions.user.manager.dto.common.CountDTO successes = auditServiceClient.getSuccessCount(dateDebutStr, - dateFinStr); - successCount = successes != null ? successes.getCount() : 0L; + totalRecords = successCount + failureCount; } catch (Exception e) { LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage()); } - } - - /** - * Exporter les logs en CSV - */ - public void exportToCSV() { try { - String dateDebutStr = dateDebut != null ? dateDebut.format(DATE_FORMATTER) : null; - String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null; - - try (jakarta.ws.rs.core.Response response = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr)) { - // TODO: Implémenter le téléchargement du fichier CSV - // String csv = response.readEntity(String.class); // Non utilisé pour l'instant - addSuccessMessage("Export CSV généré avec succès"); - } + String dateDebutStr = toIsoString(dateDebut); + String dateFinStr = toIsoString(dateFin); + actionStatistics = auditServiceClient.getActionStatistics(dateDebutStr, dateFinStr); + userActivityStatistics = auditServiceClient.getUserActivityStatistics(dateDebutStr, dateFinStr); } catch (Exception e) { - LOGGER.severe("Erreur lors de l'export: " + e.getMessage()); - addErrorMessage("Erreur lors de l'export: " + e.getMessage()); + LOGGER.warning("Statistiques détaillées non disponibles: " + e.getMessage()); + } + } + + public void exportToCSV() { + try { + String dateDebutStr = toIsoString(dateDebut); + String dateFinStr = toIsoString(dateFin); + + try (jakarta.ws.rs.core.Response response = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr)) { + String csv = response.readEntity(String.class); + + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + + String disposition = response.getHeaderString("Content-Disposition"); + String fileName = "audit-logs.csv"; + if (disposition != null && disposition.contains("filename=")) { + fileName = disposition.substring(disposition.indexOf("filename=") + 9).replace("\"", ""); + } + + byte[] contentBytes = csv.getBytes(java.nio.charset.StandardCharsets.UTF_8); + + externalContext.setResponseContentType("text/csv; charset=UTF-8"); + externalContext.setResponseContentLength(contentBytes.length); + externalContext.setResponseHeader("Content-Disposition", + "attachment; filename=\"" + fileName + "\""); + + try (OutputStream out = externalContext.getResponseOutputStream()) { + out.write(contentBytes); + out.flush(); + } + + facesContext.responseComplete(); + } + } catch (Exception e) { + LOGGER.severe("Erreur lors de l'export: " + e.getMessage()); + addErrorMessage("Erreur lors de l'export : " + e.getMessage()); } } - /** - * Réinitialiser les filtres - */ public void resetFilters() { acteurUsername = null; dateDebut = null; @@ -180,12 +181,10 @@ public class AuditConsultationBean implements Serializable { ressourceType = null; succes = null; currentPage = 0; - auditLogs.clear(); + loadStatistics(); + loadRecentLogs(); } - /** - * Page précédente - */ public void previousPage() { if (currentPage > 0) { currentPage--; @@ -193,25 +192,32 @@ public class AuditConsultationBean implements Serializable { } } - /** - * Page suivante - */ public void nextPage() { currentPage++; searchLogs(); } - /** - * Charger les realms disponibles - */ - private void loadRealms() { - // TODO: Implémenter la récupération des realms depuis Keycloak - // Le realm "master" est le realm d'administration qui permet de gérer tous les - // autres realms - availableRealms = List.of("master", "lions-user-manager", "btpxpress", "unionflow"); + public long getSuccessRate() { + if (totalRecords == 0) return 0; + return (successCount * 100) / totalRecords; + } + + private void loadRealms() { + try { + List realms = realmServiceClient.getAllRealms(); + availableRealms = (realms != null && !realms.isEmpty()) ? new ArrayList<>(realms) : new ArrayList<>(); + } catch (Exception e) { + LOGGER.severe("Erreur lors du chargement des realms: " + e.getMessage()); + availableRealms = new ArrayList<>(); + } + } + + private String toIsoString(Date date) { + if (date == null) return null; + LocalDateTime ldt = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + return ldt.format(DATE_FORMATTER); } - // Méthodes utilitaires private void addSuccessMessage(String message) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message)); 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 index c667d4e..7023985 100644 --- a/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java +++ b/src/main/java/dev/lions/user/manager/client/view/DashboardBean.java @@ -2,6 +2,7 @@ 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.UserMetricsServiceClient; import dev.lions.user.manager.client.service.UserServiceClient; import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; import dev.lions.user.manager.dto.user.UserSearchResultDTO; @@ -10,6 +11,7 @@ import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; import lombok.Data; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; import jakarta.faces.application.FacesMessage; @@ -47,6 +49,13 @@ public class DashboardBean implements Serializable { @RestClient private AuditServiceClient auditServiceClient; + @Inject + @RestClient + private UserMetricsServiceClient userMetricsServiceClient; + + @ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "lions-user-manager") + String defaultRealm; + // Statistiques private Long totalUsers = 0L; private Long totalRoles = 0L; @@ -76,22 +85,31 @@ public class DashboardBean implements Serializable { return recentActions != null ? String.valueOf(recentActions) : "0"; } + public String getActiveSessionsDisplay() { + if (loading) + return "..."; + return activeSessions != null ? String.valueOf(activeSessions) : "0"; + } + + public String getOnlineUsersDisplay() { + if (loading) + return "..."; + return onlineUsers != null ? String.valueOf(onlineUsers) : "0"; + } + public boolean isLoading() { return loading; } - // Realm par défaut - private String realmName = "master"; + // Realm par défaut (initialisé depuis la config en @PostConstruct) + private String realmName; 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")); + this.realmName = defaultRealm; + LOGGER.info("Initialisation DashboardBean pour realm: " + realmName); loadStatistics(); } @@ -104,9 +122,7 @@ public class DashboardBean implements Serializable { loadTotalUsers(); loadTotalRoles(); loadRecentActions(); - // Les sessions actives nécessitent une API spécifique qui n'existe pas encore - // activeSessions = 0L; - // onlineUsers = 0L; + loadSessionStats(); } catch (Exception e) { LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage()); } finally { @@ -119,33 +135,16 @@ public class DashboardBean implements Serializable { */ 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 + .pageSize(1) .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; - } + totalUsers = (result != null && result.getTotalCount() != null) ? result.getTotalCount() : 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(); + LOGGER.severe("Erreur chargement total utilisateurs: " + e.getMessage()); totalUsers = 0L; addErrorMessage("Impossible de charger le nombre d'utilisateurs: " + e.getMessage()); } @@ -156,22 +155,10 @@ public class DashboardBean implements Serializable { */ 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; - } + totalRoles = (roles != null) ? (long) roles.size() : 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(); + LOGGER.severe("Erreur chargement total rôles: " + e.getMessage()); totalRoles = 0L; addErrorMessage("Impossible de charger le nombre de rôles: " + e.getMessage()); } @@ -182,58 +169,46 @@ public class DashboardBean implements Serializable { */ private void loadRecentActions() { try { - LocalDateTime dateDebut = LocalDateTime.now().minusDays(1); - String dateDebutStr = dateDebut.format(DATE_FORMATTER); + String dateDebutStr = LocalDateTime.now().minusDays(1).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()..."); - dev.lions.user.manager.dto.common.CountDTO successDto = auditServiceClient.getSuccessCount(dateDebutStr, - dateFinStr); - dev.lions.user.manager.dto.common.CountDTO failureDto = auditServiceClient.getFailureCount(dateDebutStr, - dateFinStr); + dev.lions.user.manager.dto.common.CountDTO successDto = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr); + dev.lions.user.manager.dto.common.CountDTO failureDto = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr); - Long successCount = (successDto != null) ? successDto.getCount() : 0L; - Long failureCount = (failureDto != null) ? failureDto.getCount() : 0L; - - LOGGER.info(" SuccessCount: " + successCount); - LOGGER.info(" FailureCount: " + failureCount); + long successCount = (successDto != null) ? successDto.getCount() : 0L; + long failureCount = (failureDto != null) ? failureDto.getCount() : 0L; recentActions = successCount + failureCount; - 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; - } + LOGGER.warning("Fallback searchLogs pour actions récentes: " + e2.getMessage()); + List logs = auditServiceClient.searchLogs(null, dateDebutStr, dateFinStr, null, null, null, 0, 100); + recentActions = (logs != null) ? (long) logs.size() : 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(); + LOGGER.severe("Erreur chargement actions récentes: " + e.getMessage()); recentActions = 0L; - addErrorMessage("Impossible de charger les actions récentes: " + e.getMessage()); + } + } + + /** + * Charger les statistiques de sessions / utilisateurs en ligne + */ + private void loadSessionStats() { + try { + dev.lions.user.manager.dto.common.UserSessionStatsDTO stats = userMetricsServiceClient + .getUserSessionStats(realmName); + + if (stats != null) { + this.activeSessions = stats.getActiveSessions(); + this.onlineUsers = stats.getOnlineUsers(); + } else { + this.activeSessions = 0L; + this.onlineUsers = 0L; + } + } catch (Exception e) { + LOGGER.severe("Erreur chargement stats sessions: " + e.getMessage()); + this.activeSessions = 0L; + this.onlineUsers = 0L; } } @@ -241,7 +216,6 @@ public class DashboardBean implements Serializable { * Rafraîchir les statistiques */ public void refreshStatistics() { - LOGGER.info("=== Rafraîchissement des statistiques ==="); loadStatistics(); addSuccessMessage("Statistiques rafraîchies avec succès"); } 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 a666521..2815c88 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 @@ -19,12 +19,10 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import java.util.stream.Collectors; /** * Bean JSF pour la gestion des rôles - * - * @author Lions User Manager - * @version 1.0.0 */ @Named("roleGestionBean") @ViewScoped @@ -54,8 +52,6 @@ public class RoleGestionBean implements Serializable { private boolean editMode = false; // Filtres - // Par défaut, utiliser le realm lions-user-manager où les utilisateurs sont - // configurés private String realmName = "lions-user-manager"; private String clientName; private TypeRole selectedTypeRole; @@ -86,6 +82,7 @@ public class RoleGestionBean implements Serializable { LOGGER.info("Chargement des rôles Realm pour le realm: " + realmName); realmRoles = roleServiceClient.getAllRealmRoles(realmName); updateAllRoles(); + loadClients(); LOGGER.info("Chargement réussi: " + realmRoles.size() + " rôles Realm trouvés"); if (realmRoles.isEmpty()) { addErrorMessage("Aucun rôle Realm trouvé dans le realm: " + realmName); @@ -96,7 +93,7 @@ public class RoleGestionBean implements Serializable { LOGGER.severe(errorMsg); LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e); addErrorMessage(errorMsg); - realmRoles = new ArrayList<>(); // Réinitialiser en cas d'erreur + realmRoles = new ArrayList<>(); updateAllRoles(); } } @@ -106,11 +103,13 @@ public class RoleGestionBean implements Serializable { */ public void loadClientRoles() { if (clientName == null || clientName.isEmpty()) { + clientRoles = new ArrayList<>(); + updateAllRoles(); return; } try { - clientRoles = roleServiceClient.getAllClientRoles(realmName, clientName); + clientRoles = roleServiceClient.getAllClientRoles(clientName, realmName); updateAllRoles(); LOGGER.info("Chargement de " + clientRoles.size() + " rôles Client"); } catch (Exception e) { @@ -128,6 +127,38 @@ public class RoleGestionBean implements Serializable { allRoles.addAll(clientRoles); } + /** + * Obtenir les rôles Realm filtrés par type + */ + public List getFilteredRealmRoles() { + if (selectedTypeRole == null) { + return realmRoles; + } + return realmRoles.stream() + .filter(r -> selectedTypeRole.equals(r.getTypeRole())) + .collect(Collectors.toList()); + } + + /** + * Obtenir les rôles Client filtrés par type + */ + public List getFilteredClientRoles() { + if (selectedTypeRole == null) { + return clientRoles; + } + return clientRoles.stream() + .filter(r -> selectedTypeRole.equals(r.getTypeRole())) + .collect(Collectors.toList()); + } + + /** + * Appliquer le filtre type (appelé depuis p:ajax) + */ + public void applyTypeFilter() { + // Les getFilteredRealmRoles/getFilteredClientRoles font le filtrage + LOGGER.info("Filtre type appliqué: " + selectedTypeRole); + } + /** * Créer un nouveau rôle Realm */ @@ -171,7 +202,7 @@ public class RoleGestionBean implements Serializable { public void deleteRealmRole(String roleName) { try { roleServiceClient.deleteRealmRole(roleName, realmName); - addSuccessMessage("Rôle Realm supprimé avec succès"); + addSuccessMessage("Rôle Realm supprimé avec succès: " + roleName); loadRealmRoles(); } catch (Exception e) { LOGGER.severe("Erreur lors de la suppression: " + e.getMessage()); @@ -190,7 +221,7 @@ public class RoleGestionBean implements Serializable { try { roleServiceClient.deleteClientRole(clientName, roleName, realmName); - addSuccessMessage("Rôle Client supprimé avec succès"); + addSuccessMessage("Rôle Client supprimé avec succès: " + roleName); loadClientRoles(); } catch (Exception e) { LOGGER.severe("Erreur lors de la suppression: " + e.getMessage()); @@ -274,7 +305,7 @@ public class RoleGestionBean implements Serializable { return allRoles.stream() .filter(role -> user.getRealmRoles().contains(role.getName())) - .collect(java.util.stream.Collectors.toList()); + .collect(Collectors.toList()); } /** @@ -285,9 +316,6 @@ public class RoleGestionBean implements Serializable { editMode = false; } - /** - * Charger les realms disponibles - */ /** * Charger les realms disponibles depuis Keycloak */ @@ -297,23 +325,36 @@ public class RoleGestionBean implements Serializable { LOGGER.info("Realms chargés: " + availableRealms); } catch (Exception e) { LOGGER.severe("Erreur lors du chargement des realms: " + e.getMessage()); - // Fallback en cas d'erreur availableRealms = List.of("master", "lions-user-manager", "btpxpress", "unionflow"); } } + /** + * Charger les clients disponibles pour le realm sélectionné + */ + private void loadClients() { + if (realmName == null || realmName.isEmpty()) { + availableClients = new ArrayList<>(); + return; + } + + try { + availableClients = realmServiceClient.getRealmClients(realmName); + LOGGER.info("Clients chargés pour " + realmName + ": " + availableClients.size()); + } catch (Exception e) { + LOGGER.severe("Erreur lors du chargement des clients: " + e.getMessage()); + availableClients = new ArrayList<>(); + } + } + /** * Charger les rôles du realm spécifié. - * Cette méthode sert de point d'entrée pour la page d'édition. - * - * @param realm Le nom du realm dont il faut charger les rôles */ public void loadRolesForUser(String realm) { if (realm != null && !realm.isEmpty()) { this.realmName = realm; loadRealmRoles(); } else { - // Utiliser le realm par défaut si non fourni loadRealmRoles(); } } diff --git a/src/main/java/dev/lions/user/manager/client/view/SyncDashboardBean.java b/src/main/java/dev/lions/user/manager/client/view/SyncDashboardBean.java index 1f1edb8..8aa8b56 100644 --- a/src/main/java/dev/lions/user/manager/client/view/SyncDashboardBean.java +++ b/src/main/java/dev/lions/user/manager/client/view/SyncDashboardBean.java @@ -2,6 +2,8 @@ package dev.lions.user.manager.client.view; import dev.lions.user.manager.client.service.SyncServiceClient; import dev.lions.user.manager.dto.sync.HealthStatusDTO; +import dev.lions.user.manager.dto.sync.SyncConsistencyDTO; +import dev.lions.user.manager.dto.sync.SyncHistoryDTO; import dev.lions.user.manager.dto.sync.SyncResultDTO; import jakarta.annotation.PostConstruct; import jakarta.faces.application.FacesMessage; @@ -9,9 +11,11 @@ import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; import java.io.Serializable; +import java.time.format.DateTimeFormatter; import java.util.logging.Logger; @Named @@ -20,17 +24,26 @@ public class SyncDashboardBean implements Serializable { private static final Logger LOGGER = Logger.getLogger(SyncDashboardBean.class.getName()); + private static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); + @Inject @RestClient SyncServiceClient syncService; + @ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "lions-user-manager") + String defaultRealm; + private HealthStatusDTO keycloakStatus; + private SyncConsistencyDTO consistencyResult; + private SyncHistoryDTO lastSyncStatus; private String syncMessage; - private String targetRealm = "lions-user-manager"; // Default realm matches OIDC config + private String targetRealm; @PostConstruct public void init() { + this.targetRealm = defaultRealm; checkKeycloakStatus(); + loadLastSyncStatus(); } public void checkKeycloakStatus() { @@ -67,6 +80,83 @@ public class SyncDashboardBean implements Serializable { } } + public void checkDataConsistency() { + try { + this.consistencyResult = syncService.checkDataConsistency(targetRealm); + String status = consistencyResult.getStatus(); + if ("OK".equals(status)) { + addMessage(FacesMessage.SEVERITY_INFO, "Cohérence OK", + consistencyResult.getUsersKeycloakCount() + " utilisateurs, " + + consistencyResult.getRolesKeycloakCount() + " rôles — données cohérentes."); + } else { + int missingLocal = (consistencyResult.getMissingUsersInLocal() != null + ? consistencyResult.getMissingUsersInLocal().size() : 0) + + (consistencyResult.getMissingRolesInLocal() != null + ? consistencyResult.getMissingRolesInLocal().size() : 0); + addMessage(FacesMessage.SEVERITY_WARN, "Incohérence détectée", + missingLocal + " élément(s) manquant(s) localement. Lancez une synchronisation."); + } + } catch (Exception e) { + LOGGER.severe("Erreur vérification cohérence: " + e.getMessage()); + addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", "Impossible de vérifier la cohérence: " + e.getMessage()); + } + } + + public void loadLastSyncStatus() { + try { + this.lastSyncStatus = syncService.getLastSyncStatus(targetRealm); + } catch (Exception e) { + LOGGER.warning("Impossible de charger le statut de sync: " + e.getMessage()); + } + } + + public void forceSyncRealm() { + try { + this.lastSyncStatus = syncService.forceSyncRealm(targetRealm); + String status = lastSyncStatus != null ? lastSyncStatus.getStatus() : "UNKNOWN"; + if ("SUCCESS".equals(status)) { + Integer items = lastSyncStatus.getItemsProcessed(); + addMessage(FacesMessage.SEVERITY_INFO, "Synchronisation forcée réussie", + (items != null ? items : 0) + " éléments traités."); + } else { + addMessage(FacesMessage.SEVERITY_WARN, "Synchronisation terminée avec avertissement", + lastSyncStatus != null ? lastSyncStatus.getErrorMessage() : "Statut inconnu"); + } + } catch (Exception e) { + LOGGER.severe("Erreur synchronisation forcée: " + e.getMessage()); + addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", "Echec de la synchronisation forcée: " + e.getMessage()); + } + } + + // Convenience methods for view + public String getLastSyncDate() { + if (lastSyncStatus == null || lastSyncStatus.getSyncDate() == null) return "Jamais synchronisé"; + return lastSyncStatus.getSyncDate().format(DISPLAY_FORMATTER); + } + + public String getLastSyncStatusLabel() { + if (lastSyncStatus == null) return "INCONNU"; + return lastSyncStatus.getStatus() != null ? lastSyncStatus.getStatus() : "INCONNU"; + } + + public String getLastSyncItemsProcessed() { + if (lastSyncStatus == null || lastSyncStatus.getItemsProcessed() == null) return "N/A"; + return String.valueOf(lastSyncStatus.getItemsProcessed()); + } + + public String getConsistencyStatusLabel() { + if (consistencyResult == null) return "Non vérifié"; + return consistencyResult.getStatus() != null ? consistencyResult.getStatus() : "N/A"; + } + + public int getConsistencyMissingCount() { + if (consistencyResult == null) return 0; + int missing = 0; + if (consistencyResult.getMissingUsersInLocal() != null) missing += consistencyResult.getMissingUsersInLocal().size(); + if (consistencyResult.getMissingRolesInLocal() != null) missing += consistencyResult.getMissingRolesInLocal().size(); + return missing; + } + private void handleSyncResult(String type, SyncResultDTO result) { String msg = result.isSuccess() ? "Synchronisation réussie. " + type + " synchronisés : " @@ -86,15 +176,11 @@ public class SyncDashboardBean implements Serializable { } // Getters and Setters - public HealthStatusDTO getKeycloakStatus() { - return keycloakStatus; - } + public HealthStatusDTO getKeycloakStatus() { return keycloakStatus; } + public void setKeycloakStatus(HealthStatusDTO keycloakStatus) { this.keycloakStatus = keycloakStatus; } + public SyncConsistencyDTO getConsistencyResult() { return consistencyResult; } + public SyncHistoryDTO getLastSyncStatusDTO() { return lastSyncStatus; } - public void setKeycloakStatus(HealthStatusDTO keycloakStatus) { - this.keycloakStatus = keycloakStatus; - } - - // Convenience methods for view public String getKeycloakStatusLabel() { return (keycloakStatus != null && keycloakStatus.isOverallHealthy()) ? "UP" : "DOWN"; } 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 057b196..f4b52f2 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; @@ -10,6 +11,7 @@ import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; import lombok.Data; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; import java.io.Serializable; @@ -35,12 +37,18 @@ public class UserCreationBean implements Serializable { @RestClient private UserServiceClient userServiceClient; + @Inject + @RestClient + private RealmServiceClient realmServiceClient; + + @ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "lions-user-manager") + String defaultRealm; + private UserDTO newUser = UserDTO.builder().build(); - // Le realm "master" est le realm d'administration qui permet de gérer tous les - // autres realms - private String realmName = "master"; + private String realmName; private String password; private String passwordConfirm; + private boolean creationSuccess; // Options pour les selects private List statutOptions = List.of(StatutUser.values()); @@ -48,6 +56,7 @@ public class UserCreationBean implements Serializable { @PostConstruct public void init() { + this.realmName = defaultRealm; loadRealms(); // Initialiser les valeurs par défaut newUser.setEnabled(true); @@ -58,40 +67,36 @@ public class UserCreationBean implements Serializable { /** * Créer un nouvel utilisateur */ - public String createUser() { - // Validation + public void createUser() { if (password == null || password.isEmpty()) { addErrorMessage("Le mot de passe est obligatoire"); - return null; + return; } if (!password.equals(passwordConfirm)) { addErrorMessage("Les mots de passe ne correspondent pas"); - return null; + return; } if (password.length() < 8) { addErrorMessage("Le mot de passe doit contenir au moins 8 caractères"); - return null; + return; } try { - // Créer l'utilisateur jakarta.ws.rs.core.Response response = userServiceClient.createUser(newUser, realmName); UserDTO createdUser = response.readEntity(UserDTO.class); - // Définir le mot de passe dev.lions.user.manager.dto.user.PasswordResetRequestDTO request = new dev.lions.user.manager.dto.user.PasswordResetRequestDTO( password, false); userServiceClient.resetPassword(createdUser.getId(), realmName, request); - addSuccessMessage("Utilisateur créé avec succès: " + createdUser.getUsername()); + addSuccessMessage("Utilisateur '" + createdUser.getUsername() + "' créé avec succès !"); + creationSuccess = true; resetForm(); - return "userListPage"; // Rediriger vers la liste } catch (Exception e) { LOGGER.severe("Erreur lors de la création: " + e.getMessage()); - addErrorMessage("Erreur lors de la création: " + e.getMessage()); - return null; + addErrorMessage("Erreur lors de la création : " + e.getMessage()); } } @@ -102,27 +107,45 @@ public class UserCreationBean implements Serializable { newUser = UserDTO.builder().build(); password = null; passwordConfirm = null; + creationSuccess = false; newUser.setEnabled(true); newUser.setEmailVerified(false); newUser.setStatut(StatutUser.ACTIF); } /** - * Annuler la création + * Annuler la création — redirige vers la liste */ - public String cancel() { - resetForm(); - return "userListPage"; + public void cancel() { + try { + FacesContext.getCurrentInstance().getExternalContext() + .redirect(FacesContext.getCurrentInstance().getExternalContext() + .getRequestContextPath() + "/pages/user-manager/users/list.xhtml"); + } catch (Exception e) { + LOGGER.severe("Erreur lors de la redirection: " + e.getMessage()); + } } /** * Charger les realms disponibles */ private void loadRealms() { - // TODO: Implémenter la récupération des realms depuis Keycloak - // Le realm "master" est le realm d'administration qui permet de gérer tous les - // autres realms - availableRealms = List.of("master", "lions-user-manager", "btpxpress", "unionflow"); + try { + LOGGER.info("Chargement des realms disponibles pour la création d'utilisateur"); + List realms = realmServiceClient.getAllRealms(); + + if (realms == null || realms.isEmpty()) { + LOGGER.warning("Aucun realm disponible lors du chargement des realms"); + availableRealms = new ArrayList<>(); + } else { + availableRealms = new ArrayList<>(realms); + } + } catch (Exception e) { + LOGGER.severe("Erreur lors du chargement des realms: " + e.getMessage()); + addErrorMessage("Erreur lors du chargement des realms: " + e.getMessage()); + // Fallback: liste vide plutôt que valeurs codées en dur + availableRealms = new ArrayList<>(); + } } // 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 da4c865..d36ed52 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 @@ -2,31 +2,39 @@ 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.importexport.ImportResultDTO; 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 dev.lions.user.manager.enums.user.StatutUser; import jakarta.annotation.PostConstruct; import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.ExternalContext; import jakarta.faces.context.FacesContext; import jakarta.faces.event.ActionEvent; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; +import jakarta.ws.rs.core.Response; import lombok.Data; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.primefaces.PrimeFaces; -import org.primefaces.event.data.PageEvent; import org.primefaces.model.FilterMeta; import org.primefaces.model.LazyDataModel; import org.primefaces.model.SortMeta; +import org.primefaces.model.file.UploadedFile; +import java.io.IOException; +import java.io.OutputStream; import java.io.Serializable; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.logging.Logger; +import org.eclipse.microprofile.config.inject.ConfigProperty; + /** * Bean JSF pour la liste et la recherche d'utilisateurs * @@ -54,6 +62,9 @@ public class UserListBean implements Serializable { @RestClient private RealmServiceClient realmServiceClient; + @ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "lions-user-manager") + String defaultRealm; + // Propriétés pour la liste private LazyDataModel users; private UserDTO selectedUser; @@ -62,9 +73,7 @@ public class UserListBean implements Serializable { // Propriétés pour la recherche private UserSearchCriteriaDTO searchCriteria = UserSearchCriteriaDTO.builder().build(); private String searchText; - // Par défaut, utiliser le realm lions-user-manager où les utilisateurs sont - // configurés - private String realmName = "lions-user-manager"; + private String realmName; private StatutUser selectedStatut; // Propriétés pour la pagination @@ -73,6 +82,11 @@ public class UserListBean implements Serializable { private long totalRecords = 0; private int totalPages = 0; + // KPIs chargés depuis le serveur (indépendants de la pagination/filtres) + private long kpiTotalUsers = 0; + private long activeUsersCount = 0; + private long disabledUsersCount = 0; + // Options pour les selects private List statutOptions = List.of(StatutUser.values()); private List availableRealms = new ArrayList<>(); @@ -81,10 +95,16 @@ public class UserListBean implements Serializable { private String newPassword; private String newPasswordConfirm; + // Champ pour l'import CSV + private UploadedFile importedFile; + private ImportResultDTO lastImportResult; + @PostConstruct public void init() { - LOGGER.info("Initialisation de UserListBean"); + this.realmName = defaultRealm; + LOGGER.info("Initialisation de UserListBean - realm: " + realmName); loadRealms(); + loadStats(); users = new LazyDataModel() { @Override @@ -122,6 +142,7 @@ public class UserListBean implements Serializable { .statut(selectedStatut) .page(page) .pageSize(pageSize) + .includeRoles(true) .build(); UserSearchResultDTO result = userServiceClient.searchUsers(criteria); @@ -229,6 +250,8 @@ public class UserListBean implements Serializable { public void activateUser(String userId) { try { userServiceClient.activateUser(userId, realmName); + activeUsersCount++; + disabledUsersCount = Math.max(0, disabledUsersCount - 1); addSuccessMessage("Utilisateur activé avec succès"); } catch (Exception e) { LOGGER.severe("Erreur lors de l'activation: " + e.getMessage()); @@ -242,6 +265,8 @@ public class UserListBean implements Serializable { public void deactivateUser(String userId) { try { userServiceClient.deactivateUser(userId, realmName, "Désactivé par l'administrateur"); + activeUsersCount = Math.max(0, activeUsersCount - 1); + disabledUsersCount++; addSuccessMessage("Utilisateur désactivé avec succès"); } catch (Exception e) { LOGGER.severe("Erreur lors de la désactivation: " + e.getMessage()); @@ -255,6 +280,7 @@ public class UserListBean implements Serializable { public void deleteUser(String userId) { try { userServiceClient.deleteUser(userId, realmName, false); + kpiTotalUsers = Math.max(0, kpiTotalUsers - 1); addSuccessMessage("Utilisateur supprimé avec succès"); } catch (Exception e) { LOGGER.severe("Erreur lors de la suppression: " + e.getMessage()); @@ -276,77 +302,124 @@ public class UserListBean implements Serializable { } /** - * Obtenir le nombre d'utilisateurs actifs - */ - public long getActiveUsersCount() { - return calculateStatusCount(true); - } - - /** - * Obtenir le nombre d'utilisateurs désactivés - */ - public long getDisabledUsersCount() { - return calculateStatusCount(false); - } - - // Helper for counts - private long calculateStatusCount(boolean enabled) { - if (users == null) - return 0; - List list = (List) users.getWrappedData(); - if (list == null || list.isEmpty()) { - return 0; - } - return list.stream() - .filter(user -> user.getEnabled() != null && user.getEnabled() == enabled) - .count(); - } - - /** - * Obtenir le pourcentage d'utilisateurs actifs + * Pourcentage d'utilisateurs actifs */ public int getActiveUsersPercentage() { - if (totalRecords == 0) { - return 0; - } - return (int) Math.round((double) getActiveUsersCount() / totalRecords * 100); + if (kpiTotalUsers == 0) return 0; + return (int) Math.round((double) activeUsersCount / kpiTotalUsers * 100); } /** - * Obtenir le pourcentage d'utilisateurs désactivés + * Pourcentage d'utilisateurs désactivés */ public int getDisabledUsersPercentage() { - if (totalRecords == 0) { - return 0; - } - return (int) Math.round((double) getDisabledUsersCount() / totalRecords * 100); + if (kpiTotalUsers == 0) return 0; + return (int) Math.round((double) disabledUsersCount / kpiTotalUsers * 100); } /** - * Rafraîchir les données + * Rafraîchir les données et les KPIs */ public void refreshData() { - loadUsers(); + loadStats(); addSuccessMessage("Données rafraîchies"); } /** - * Exporter vers CSV (placeholder) + * Changement de realm (filtre + rechargement des KPIs) + */ + public void onRealmChange() { + currentPage = 0; + loadStats(); + if (PrimeFaces.current().isAjaxRequest()) { + PrimeFaces.current().executeScript("PF('userTableWidget').getPaginator().setPage(0);"); + } + } + + /** + * Charger les statistiques KPI depuis le serveur + */ + private void loadStats() { + try { + UserSearchCriteriaDTO totalCriteria = UserSearchCriteriaDTO.builder() + .realmName(realmName).page(0).pageSize(1).build(); + UserSearchResultDTO totalResult = userServiceClient.searchUsers(totalCriteria); + kpiTotalUsers = totalResult.getTotalCount() != null ? totalResult.getTotalCount() : 0; + + UserSearchCriteriaDTO activeCriteria = UserSearchCriteriaDTO.builder() + .realmName(realmName).enabled(true).page(0).pageSize(1).build(); + UserSearchResultDTO activeResult = userServiceClient.searchUsers(activeCriteria); + activeUsersCount = activeResult.getTotalCount() != null ? activeResult.getTotalCount() : 0; + + disabledUsersCount = kpiTotalUsers - activeUsersCount; + } catch (Exception e) { + LOGGER.severe("Erreur chargement statistiques KPI: " + e.getMessage()); + } + } + + /** + * Exporter les utilisateurs vers un fichier CSV téléchargeable */ public void exportToCSV() { - addSuccessMessage("Fonctionnalité d'export en cours de développement"); + try { + Response response = userServiceClient.exportUsersToCSV(realmName); + String csvContent = response.readEntity(String.class); + + FacesContext facesContext = FacesContext.getCurrentInstance(); + ExternalContext externalContext = facesContext.getExternalContext(); + externalContext.responseReset(); + externalContext.setResponseContentType("text/csv"); + externalContext.setResponseHeader("Content-Disposition", + "attachment; filename=\"utilisateurs-" + realmName + ".csv\""); + externalContext.setResponseCharacterEncoding("UTF-8"); + + OutputStream outputStream = externalContext.getResponseOutputStream(); + outputStream.write(csvContent.getBytes(StandardCharsets.UTF_8)); + outputStream.flush(); + + facesContext.responseComplete(); + } catch (IOException e) { + LOGGER.severe("Erreur I/O lors de l'export CSV: " + e.getMessage()); + addErrorMessage("Erreur lors de l'export CSV: " + e.getMessage()); + } catch (Exception e) { + LOGGER.severe("Erreur lors de l'export CSV: " + e.getMessage()); + addErrorMessage("Erreur lors de l'export CSV: " + e.getMessage()); + } } /** - * Importer des utilisateurs (placeholder) + * Importer des utilisateurs depuis un fichier CSV */ public void importUsers() { - addSuccessMessage("Fonctionnalité d'import en cours de développement"); + if (importedFile == null) { + addErrorMessage("Veuillez sélectionner un fichier CSV à importer."); + return; + } + try { + String csvContent = new String(importedFile.getContent(), StandardCharsets.UTF_8); + this.lastImportResult = userServiceClient.importUsersFromCSV(realmName, csvContent); + + if (lastImportResult != null) { + String msg = lastImportResult.getSuccessCount() + " utilisateur(s) importé(s), " + + lastImportResult.getErrorCount() + " erreur(s)."; + if (lastImportResult.getErrorCount() == 0) { + addSuccessMessage(msg); + } else { + addMessage(FacesMessage.SEVERITY_WARN, "Import partiel", msg); + } + loadStats(); + } + importedFile = null; + } catch (Exception e) { + LOGGER.severe("Erreur lors de l'import CSV: " + e.getMessage()); + addErrorMessage("Erreur lors de l'import: " + e.getMessage()); + } + } + + private void addMessage(FacesMessage.Severity severity, String summary, String detail) { + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(severity, summary, detail)); } - /** - * Charger les realms disponibles - */ /** * Charger les realms disponibles depuis Keycloak */ diff --git a/src/main/resources/META-INF/faces-config.xml b/src/main/resources/META-INF/faces-config.xml index 586c719..25ac10e 100644 --- a/src/main/resources/META-INF/faces-config.xml +++ b/src/main/resources/META-INF/faces-config.xml @@ -7,6 +7,10 @@ Lions User Manager + + dev.lions.user.manager.client.exception.ViewExpiredExceptionHandlerFactory + + fr 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 2f3e17c..95859d6 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 @@ -31,7 +31,7 @@ - +
@@ -42,25 +42,28 @@ + + + - + + @@ -70,7 +73,11 @@
-
Filtres de recherche
+
+
+ Filtres de recherche +
+
@@ -82,15 +89,16 @@ - - + +
- + @@ -98,14 +106,14 @@
- +
- +
@@ -117,33 +125,55 @@
- + + update=":formAuditLogs:auditLogsTable @form :growlMessages" />
- + +
+ +
+ +
-
Logs d'Audit
+
+
+ Logs d'Audit +
+ +
+ + paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}" + currentPageReportTemplate="{startRecord}-{endRecord} sur {totalRecords}" + emptyMessage="Aucun log d'audit trouvé. Cliquez sur Rechercher pour lancer une requête." + styleClass="w-full" stripedRows="true" size="small"> - - + + + + + + + - - #{log.typeAction} + + #{log.typeAction} @@ -155,45 +185,51 @@ - -
- - #{log.ressourceType} -
+ + + + + + - + - +
- - #{log.dateAction} + + + +
- - - - #{log.details} - - + + + + #{log.description} + + - - + - - - #{log.adresseIp} - - + + + #{log.ipAddress} + + - - + - - + @@ -207,55 +243,111 @@ - - -
-
- Type d'action: -

#{auditConsultationBean.selectedLog.typeAction}

+ modal="true" resizable="false" responsive="true" styleClass="w-full md:w-30rem lg:w-35rem"> + + +
+ + +
+ + + + + + + + + + +
+ +
- Acteur: -

#{auditConsultationBean.selectedLog.acteurUsername}

+ Type d'action + + #{auditConsultationBean.selectedLog.typeAction} +
+ +
- Ressource: -

#{auditConsultationBean.selectedLog.ressourceType} - - #{auditConsultationBean.selectedLog.ressourceId}

+ Acteur +
+ + + #{auditConsultationBean.selectedLog.acteurUsername} + +
+ +
- Date: -

#{auditConsultationBean.selectedLog.dateAction}

+ Ressource + + #{auditConsultationBean.selectedLog.ressourceType} + + — #{auditConsultationBean.selectedLog.ressourceId} + +
- -
- Détails: -

#{auditConsultationBean.selectedLog.details}

+ + + + Description + #{auditConsultationBean.selectedLog.description} + + + + + Adresse IP + #{auditConsultationBean.selectedLog.ipAddress} + + + + + User Agent + #{auditConsultationBean.selectedLog.userAgent} + + + + +
+ + Message d'erreur + + + #{auditConsultationBean.selectedLog.errorMessage} +
- - -
- Adresse IP: -

#{auditConsultationBean.selectedLog.adresseIp}

-
-
- -
- User Agent: -

#{auditConsultationBean.selectedLog.userAgent}

-
-
- -
- Message d'erreur: -

#{auditConsultationBean.selectedLog.messageErreur}

-
-
+
+ + + + Realm + +
-
+ + + +
+ + Sélectionnez un log pour voir ses détails. +
+
- \ No newline at end of file + 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 035efe9..0ed9b84 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 @@ -62,15 +62,13 @@ - + - - - + + + - - - + @@ -116,7 +114,7 @@
Realm Keycloak - lions-user-manager + #{dashboardBean.realmName}
Statut 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 8f2c285..860524a 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 @@ -1,101 +1,121 @@ - + - + Gestion des Rôles - Lions User Manager - - + +
- - - - - - - - - - - - - - - - + +
-
+ + + +
- - - - - - - +
+ +
Filtres
+
+
+
+
+ + + + + + +
+
- - - - - - +
+
+ + + + + + +
+
- - - - - - +
+
+ + + + + + +
+
+
- + + +
+
+ + Rôles Realm +
+ +
+
- +
+ + + +
-
- -
-

Aucun rôle Realm trouvé

+ + +
+ + Aucun rôle Realm trouvé
- +
+
@@ -103,64 +123,83 @@
- + + +
+
+ + Rôles Client +
+ +
+
- +
+ + + +
-
- -
-

Aucun rôle Client trouvé

+ + +
+ + + Sélectionnez un client pour voir ses rôles + + + Aucun rôle Client trouvé +
- +
+
- + + - + + - + + - + + - diff --git a/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml b/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml index d0a8be7..5e6e220 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/sync/dashboard.xhtml @@ -92,6 +92,78 @@
+ + +
+
+
+
Dernière Synchronisation
+ +
+ +
+
+ Date: + #{syncDashboardBean.lastSyncDate} +
+
+ Statut: + +
+
+ Éléments traités: + #{syncDashboardBean.lastSyncItemsProcessed} +
+
+
+
+ + +
+
+
+
Cohérence des Données
+ +
+ + +
+ Résultat: + +
+
+ Utilisateurs Keycloak: + #{syncDashboardBean.consistencyResult.usersKeycloakCount} +
+
+ Utilisateurs locaux: + #{syncDashboardBean.consistencyResult.usersLocalCount} +
+
+ Éléments manquants: + + #{syncDashboardBean.consistencyMissingCount} + +
+
+ + +
+ + Cliquez sur "Vérifier" pour analyser la cohérence des données. +
+
+
+
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 index ac2f22a..0cf546d 100644 --- a/src/main/resources/META-INF/resources/pages/user-manager/users.xhtml +++ b/src/main/resources/META-INF/resources/pages/user-manager/users.xhtml @@ -53,15 +53,18 @@ - + + styleClass="p-button-text p-button-sm p-button-rounded ui-button-success" + process="@this" + style="margin-right: .25rem; padding: 0;"> - 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 fcfd70e..2cb1bbb 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 @@ -8,31 +8,45 @@
- +
-
-
-
- -
-

Nouvel Utilisateur

-

Créer un nouvel utilisateur dans Keycloak

-
-
- - - Retour à la liste + + + + + + + + + Retour à la liste -
-
+ +
- + + + + +
+
+ +
+ Utilisateur créé avec succès ! +

+ Vous pouvez créer un autre utilisateur ou retourner à la liste. +

+
+ + Voir la liste + +
+
+
+

@@ -41,7 +55,7 @@

- +

@@ -49,100 +63,104 @@ Informations de Base

-
- + + * + styleClass="w-full" required="true" + requiredMessage="Le nom d'utilisateur est obligatoire" + placeholder="ex: jdupont"> - Identifiant unique de connexion + + Identifiant unique de connexion
-
- + + * +
-
- + + * + styleClass="w-full" required="true" + requiredMessage="Le prénom est obligatoire" + placeholder="ex: Jean"> +
-
- + + * + styleClass="w-full" required="true" + requiredMessage="Le nom est obligatoire" + placeholder="ex: Dupont"> +
- +
-

Mot de Passe

-
- + + * - Au moins 8 caractères + + Au moins 8 caractères
-
- - - + + * + +
-

Configuration

-
- +
- -
- -
+
+
- - + value="#{userCreationBean.newUser.enabled}" /> +
- - -
+
- - + value="#{userCreationBean.newUser.emailVerified}" /> +
@@ -174,49 +186,36 @@
- +
-

- - Actions -

-
- - + update=":formUserCreation" validateClient="true" /> - + styleClass="p-button-outlined p-button-secondary" + action="#{userCreationBean.resetForm}" + process="@this" update=":formUserCreation" immediate="true"> - - - +
- - + onclick="PF('helpDialog').show();" />
- @@ -225,17 +224,7 @@
- - - - - - - +
@@ -247,6 +236,7 @@
  • Nom d'utilisateur : Identifiant unique (3-50 caractères)
  • Email : Adresse email valide
  • +
  • Prénom / Nom : Au moins 2 caractères chacun
  • Mot de passe : Au moins 8 caractères
@@ -279,4 +269,4 @@ - \ No newline at end of file + 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 6ff16d0..53dc3e7 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 @@ -7,31 +7,31 @@ Liste des Utilisateurs - Lions User Manager + + + + + + + +
+ + +
+
+
+
+ + + + + +
- -
-
-
-
- -
-

Gestion des Utilisateurs

-

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

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