feat(client): corrections UI/UX pages dashboard, audit, roles, users - fix REST clients, KPI, navigation et formulaires
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
19
BUILD-SYNC-API.md
Normal file
19
BUILD-SYNC-API.md
Normal file
@@ -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`).
|
||||
37
script/docker/dependencies-docker-compose.yml
Normal file
37
script/docker/dependencies-docker-compose.yml
Normal file
@@ -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:
|
||||
49
script/docker/docker-compose.yml
Normal file
49
script/docker/docker-compose.yml
Normal file
@@ -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:
|
||||
5
script/docker/run-dev.bat
Normal file
5
script/docker/run-dev.bat
Normal file
@@ -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
|
||||
7
script/docker/run-dev.sh
Normal file
7
script/docker/run-dev.sh
Normal file
@@ -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
|
||||
11
src/main/docker/Dockerfile.jvm
Normal file
11
src/main/docker/Dockerfile.jvm
Normal file
@@ -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"]
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<String> 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;
|
||||
|
||||
@@ -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<ExceptionQueuedEvent> 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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<AuditLogDTO> 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<TypeActionAudit, Long> actionStatistics;
|
||||
private Map<String, Long> userActivityStatistics;
|
||||
private Long failureCount = 0L;
|
||||
private Long successCount = 0L;
|
||||
private long failureCount = 0;
|
||||
private long successCount = 0;
|
||||
|
||||
// Options
|
||||
private List<TypeActionAudit> typeActionOptions = List.of(TypeActionAudit.values());
|
||||
private List<String> 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);
|
||||
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());
|
||||
}
|
||||
try {
|
||||
String dateDebutStr = toIsoString(dateDebut);
|
||||
String dateFinStr = toIsoString(dateFin);
|
||||
actionStatistics = auditServiceClient.getActionStatistics(dateDebutStr, dateFinStr);
|
||||
userActivityStatistics = auditServiceClient.getUserActivityStatistics(dateDebutStr, dateFinStr);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warning("Statistiques détaillées non disponibles: " + 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;
|
||||
String dateDebutStr = toIsoString(dateDebut);
|
||||
String dateFinStr = toIsoString(dateFin);
|
||||
|
||||
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 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());
|
||||
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<String> 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));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<RoleDTO> 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<RoleDTO> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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<StatutUser> 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<String> 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
|
||||
|
||||
@@ -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<UserDTO> 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<StatutUser> statutOptions = List.of(StatutUser.values());
|
||||
private List<String> 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<UserDTO>() {
|
||||
@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<UserDTO> list = (List<UserDTO>) 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
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
<name>Lions User Manager</name>
|
||||
|
||||
<factory>
|
||||
<exception-handler-factory>dev.lions.user.manager.client.exception.ViewExpiredExceptionHandlerFactory</exception-handler-factory>
|
||||
</factory>
|
||||
|
||||
<application>
|
||||
<locale-config>
|
||||
<default-locale>fr</default-locale>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</ui:decorate>
|
||||
</div>
|
||||
|
||||
<!-- Statistiques avec composants réutilisables -->
|
||||
<!-- Statistiques -->
|
||||
<div class="col-12">
|
||||
<ui:decorate template="/templates/components/shared/dashboard/kpi-group.xhtml">
|
||||
<ui:param name="title" value="Statistiques d'Audit" />
|
||||
@@ -42,25 +42,28 @@
|
||||
<ui:param name="value" value="#{auditConsultationBean.totalRecords}" />
|
||||
<ui:param name="icon" value="pi-history" />
|
||||
<ui:param name="iconColor" value="blue-600" />
|
||||
<ui:param name="subtitle" value="Toutes les actions enregistrées" />
|
||||
</ui:include>
|
||||
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
|
||||
<ui:param name="title" value="Actions Réussies" />
|
||||
<ui:param name="value" value="#{auditConsultationBean.successCount}" />
|
||||
<ui:param name="icon" value="pi-check-circle" />
|
||||
<ui:param name="iconColor" value="green-600" />
|
||||
<ui:param name="subtitle" value="Opérations complétées avec succès" />
|
||||
</ui:include>
|
||||
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
|
||||
<ui:param name="title" value="Actions Échouées" />
|
||||
<ui:param name="value" value="#{auditConsultationBean.failureCount}" />
|
||||
<ui:param name="icon" value="pi-times-circle" />
|
||||
<ui:param name="iconColor" value="red-600" />
|
||||
<ui:param name="subtitle" value="Opérations en erreur" />
|
||||
</ui:include>
|
||||
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
|
||||
<ui:param name="title" value="Taux de Réussite" />
|
||||
<ui:param name="value"
|
||||
value="#{auditConsultationBean.totalRecords > 0 ? (auditConsultationBean.successCount * 100 / auditConsultationBean.totalRecords) : 0}%" />
|
||||
<ui:param name="value" value="#{auditConsultationBean.successRate}%" />
|
||||
<ui:param name="icon" value="pi-percentage" />
|
||||
<ui:param name="iconColor" value="purple-600" />
|
||||
<ui:param name="subtitle" value="Ratio succès / total" />
|
||||
</ui:include>
|
||||
</ui:define>
|
||||
</ui:decorate>
|
||||
@@ -70,7 +73,11 @@
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h:form id="formFilters">
|
||||
<h5 class="mb-3">Filtres de recherche</h5>
|
||||
<div class="flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="m-0">
|
||||
<i class="pi pi-filter mr-2 text-color-secondary"></i>Filtres de recherche
|
||||
</h5>
|
||||
</div>
|
||||
<div class="grid ui-fluid">
|
||||
<div class="col-12 md:col-4 field">
|
||||
<p:outputLabel for="acteurFilter" value="Acteur" />
|
||||
@@ -82,15 +89,16 @@
|
||||
<p:outputLabel for="typeActionFilter" value="Type d'action" />
|
||||
<p:selectOneMenu id="typeActionFilter"
|
||||
value="#{auditConsultationBean.selectedTypeAction}">
|
||||
<f:selectItem itemLabel="Tous les types" itemValue="" />
|
||||
<f:selectItems value="#{auditConsultationBean.typeActionOptions}" />
|
||||
<f:selectItem itemLabel="Tous les types" itemValue="#{null}" />
|
||||
<f:selectItems value="#{auditConsultationBean.typeActionOptions}" var="ta"
|
||||
itemLabel="#{ta.libelle}" itemValue="#{ta}" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-4 field">
|
||||
<p:outputLabel for="succesFilter" value="Résultat" />
|
||||
<p:selectOneMenu id="succesFilter" value="#{auditConsultationBean.succes}">
|
||||
<f:selectItem itemLabel="Tous" itemValue="" />
|
||||
<f:selectItem itemLabel="Tous" itemValue="#{null}" />
|
||||
<f:selectItem itemLabel="Succès" itemValue="true" />
|
||||
<f:selectItem itemLabel="Échec" itemValue="false" />
|
||||
</p:selectOneMenu>
|
||||
@@ -98,14 +106,14 @@
|
||||
|
||||
<div class="col-12 md:col-4 field">
|
||||
<p:outputLabel for="dateDebutFilter" value="Date début" />
|
||||
<p:calendar id="dateDebutFilter" value="#{auditConsultationBean.dateDebut}"
|
||||
pattern="dd/MM/yyyy" />
|
||||
<p:datePicker id="dateDebutFilter" value="#{auditConsultationBean.dateDebut}"
|
||||
pattern="dd/MM/yyyy" showIcon="true" placeholder="JJ/MM/AAAA" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-4 field">
|
||||
<p:outputLabel for="dateFinFilter" value="Date fin" />
|
||||
<p:calendar id="dateFinFilter" value="#{auditConsultationBean.dateFin}"
|
||||
pattern="dd/MM/yyyy" />
|
||||
<p:datePicker id="dateFinFilter" value="#{auditConsultationBean.dateFin}"
|
||||
pattern="dd/MM/yyyy" showIcon="true" placeholder="JJ/MM/AAAA" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-4 field">
|
||||
@@ -117,33 +125,55 @@
|
||||
|
||||
<div class="flex gap-2 justify-content-end mt-3">
|
||||
<p:commandButton value="Rechercher" icon="pi pi-search" styleClass="p-button-primary"
|
||||
action="#{auditConsultationBean.searchLogs}" update=":formAuditLogs:auditLogsTable" />
|
||||
<p:commandButton value="Réinitialiser" icon="pi pi-refresh" styleClass="p-button-secondary"
|
||||
action="#{auditConsultationBean.searchLogs}"
|
||||
update=":formAuditLogs:auditLogsTable :growlMessages" />
|
||||
<p:commandButton value="Réinitialiser" icon="pi pi-refresh"
|
||||
styleClass="p-button-secondary p-button-outlined"
|
||||
action="#{auditConsultationBean.resetFilters}"
|
||||
update=":formAuditLogs:auditLogsTable @form" />
|
||||
update=":formAuditLogs:auditLogsTable @form :growlMessages" />
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des logs avec p:dataTable -->
|
||||
<!-- Messages globaux -->
|
||||
<div class="col-12">
|
||||
<p:growl id="growlMessages" showDetail="true" life="5000" />
|
||||
</div>
|
||||
|
||||
<!-- Liste des logs -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h:form id="formAuditLogs">
|
||||
<h5>Logs d'Audit</h5>
|
||||
<div class="flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="m-0">
|
||||
<i class="pi pi-list mr-2 text-color-secondary"></i>Logs d'Audit
|
||||
</h5>
|
||||
<p:tag value="#{auditConsultationBean.auditLogs.size()} entrée(s)"
|
||||
severity="info" styleClass="text-sm" />
|
||||
</div>
|
||||
|
||||
<p:dataTable id="auditLogsTable" value="#{auditConsultationBean.auditLogs}" var="log"
|
||||
rowKey="#{log.id}" paginator="true" rows="20" rowsPerPageTemplate="10,20,50,100"
|
||||
emptyMessage="Aucun log d'audit trouvé" styleClass="w-full">
|
||||
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">
|
||||
|
||||
<!-- Colonne Statut -->
|
||||
<p:column headerText="Statut" style="width: 5rem">
|
||||
<p:tag value="#{log.succes ? 'Succès' : 'Échec'}"
|
||||
severity="#{log.succes ? 'success' : 'danger'}" styleClass="text-xs" />
|
||||
<p:column headerText="Statut" style="width: 6rem; text-align: center;">
|
||||
<h:panelGroup rendered="#{log.successful}">
|
||||
<p:tag value="Succès" severity="success" icon="pi pi-check" styleClass="text-xs" />
|
||||
</h:panelGroup>
|
||||
<h:panelGroup rendered="#{not log.successful}">
|
||||
<p:tag value="Échec" severity="danger" icon="pi pi-times" styleClass="text-xs" />
|
||||
</h:panelGroup>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Type d'action -->
|
||||
<p:column headerText="Type d'action" sortBy="#{log.typeAction}" style="width: 15%">
|
||||
<strong class="text-900">#{log.typeAction}</strong>
|
||||
<p:column headerText="Type d'action" sortBy="#{log.typeAction}" filterBy="#{log.typeAction}"
|
||||
filterMatchMode="contains" style="width: 15%">
|
||||
<span class="font-semibold text-900">#{log.typeAction}</span>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Acteur -->
|
||||
@@ -155,45 +185,51 @@
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Ressource -->
|
||||
<p:column headerText="Ressource" style="width: 12%">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-database text-color-secondary"></i>
|
||||
<span>#{log.ressourceType}</span>
|
||||
</div>
|
||||
<p:column headerText="Ressource" style="width: 10%">
|
||||
<h:panelGroup rendered="#{not empty log.ressourceType}">
|
||||
<p:tag value="#{log.ressourceType}" severity="info" styleClass="text-xs" />
|
||||
</h:panelGroup>
|
||||
<h:panelGroup rendered="#{empty log.ressourceType}">
|
||||
<span class="text-color-secondary">-</span>
|
||||
</h:panelGroup>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Date -->
|
||||
<p:column headerText="Date" sortBy="#{log.dateAction}" style="width: 15%">
|
||||
<p:column headerText="Date" sortBy="#{log.dateAction}" style="width: 14%">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-calendar text-color-secondary"></i>
|
||||
<span>#{log.dateAction}</span>
|
||||
<i class="pi pi-calendar text-color-secondary text-sm"></i>
|
||||
<h:outputText value="#{log.dateAction}" styleClass="text-sm">
|
||||
<f:convertDateTime pattern="dd/MM/yyyy HH:mm" type="localDateTime" />
|
||||
</h:outputText>
|
||||
</div>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Détails -->
|
||||
<p:column headerText="Détails" style="width: 20%">
|
||||
<c:if test="#{not empty log.details}">
|
||||
<span class="text-color-secondary text-sm">#{log.details}</span>
|
||||
</c:if>
|
||||
<c:if test="#{empty log.details}">
|
||||
<!-- Colonne Description -->
|
||||
<p:column headerText="Description" style="width: 22%">
|
||||
<h:panelGroup rendered="#{not empty log.description}">
|
||||
<span class="text-color-secondary text-sm line-clamp"
|
||||
title="#{log.description}">#{log.description}</span>
|
||||
</h:panelGroup>
|
||||
<h:panelGroup rendered="#{empty log.description}">
|
||||
<span class="text-color-secondary text-sm">-</span>
|
||||
</c:if>
|
||||
</h:panelGroup>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne IP -->
|
||||
<p:column headerText="IP" style="width: 10%">
|
||||
<c:if test="#{not empty log.adresseIp}">
|
||||
<span class="text-color-secondary text-sm">#{log.adresseIp}</span>
|
||||
</c:if>
|
||||
<c:if test="#{empty log.adresseIp}">
|
||||
<p:column headerText="IP" style="width: 9%">
|
||||
<h:panelGroup rendered="#{not empty log.ipAddress}">
|
||||
<span class="text-color-secondary text-sm font-mono">#{log.ipAddress}</span>
|
||||
</h:panelGroup>
|
||||
<h:panelGroup rendered="#{empty log.ipAddress}">
|
||||
<span class="text-color-secondary text-sm">-</span>
|
||||
</c:if>
|
||||
</h:panelGroup>
|
||||
</p:column>
|
||||
|
||||
<!-- Colonne Actions -->
|
||||
<p:column headerText="Actions" style="width: 8%">
|
||||
<p:commandButton icon="pi pi-eye" styleClass="p-button-text p-button-sm"
|
||||
title="Voir les détails" update=":formAuditLogDetails"
|
||||
<p:column headerText="" style="width: 4rem; text-align: center;">
|
||||
<p:commandButton icon="pi pi-eye"
|
||||
styleClass="p-button-text p-button-rounded p-button-sm"
|
||||
title="Voir les détails" update=":dlgAuditLogDetails"
|
||||
oncomplete="PF('auditLogDetailsDialog').show()">
|
||||
<f:setPropertyActionListener target="#{auditConsultationBean.selectedLog}"
|
||||
value="#{log}" />
|
||||
@@ -207,53 +243,109 @@
|
||||
|
||||
<!-- Dialog de détails -->
|
||||
<p:dialog id="auditLogDetailsDialog" widgetVar="auditLogDetailsDialog" header="Détails du Log d'Audit"
|
||||
modal="true" resizable="false" styleClass="w-full md:w-30rem">
|
||||
<h:form id="formAuditLogDetails">
|
||||
<c:if test="#{not empty auditConsultationBean.selectedLog}">
|
||||
<div class="flex flex-column gap-3">
|
||||
modal="true" resizable="false" responsive="true" styleClass="w-full md:w-30rem lg:w-35rem">
|
||||
<h:form id="dlgAuditLogDetails">
|
||||
<h:panelGroup rendered="#{not empty auditConsultationBean.selectedLog}" layout="block">
|
||||
<div class="flex flex-column gap-3 p-2">
|
||||
|
||||
<!-- Statut en-tête -->
|
||||
<div class="flex align-items-center justify-content-between pb-3 border-bottom-1 surface-border">
|
||||
<h:panelGroup rendered="#{auditConsultationBean.selectedLog.successful}">
|
||||
<p:tag value="Succès" severity="success" icon="pi pi-check-circle"
|
||||
styleClass="text-sm" />
|
||||
</h:panelGroup>
|
||||
<h:panelGroup rendered="#{not auditConsultationBean.selectedLog.successful}">
|
||||
<p:tag value="Échec" severity="danger" icon="pi pi-times-circle"
|
||||
styleClass="text-sm" />
|
||||
</h:panelGroup>
|
||||
<span class="text-500 text-sm">
|
||||
<h:outputText value="#{auditConsultationBean.selectedLog.dateAction}">
|
||||
<f:convertDateTime pattern="dd/MM/yyyy à HH:mm:ss" type="localDateTime" />
|
||||
</h:outputText>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Type d'action -->
|
||||
<div>
|
||||
<strong>Type d'action:</strong>
|
||||
<p>#{auditConsultationBean.selectedLog.typeAction}</p>
|
||||
<span class="text-500 text-sm block mb-1">Type d'action</span>
|
||||
<span class="text-900 font-semibold">
|
||||
#{auditConsultationBean.selectedLog.typeAction}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Acteur -->
|
||||
<div>
|
||||
<strong>Acteur:</strong>
|
||||
<p>#{auditConsultationBean.selectedLog.acteurUsername}</p>
|
||||
<span class="text-500 text-sm block mb-1">Acteur</span>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-user text-color-secondary"></i>
|
||||
<span class="text-900">
|
||||
#{auditConsultationBean.selectedLog.acteurUsername}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ressource -->
|
||||
<div>
|
||||
<strong>Ressource:</strong>
|
||||
<p>#{auditConsultationBean.selectedLog.ressourceType} -
|
||||
#{auditConsultationBean.selectedLog.ressourceId}</p>
|
||||
<span class="text-500 text-sm block mb-1">Ressource</span>
|
||||
<span class="text-900">
|
||||
#{auditConsultationBean.selectedLog.ressourceType}
|
||||
<h:panelGroup rendered="#{not empty auditConsultationBean.selectedLog.ressourceId}">
|
||||
— #{auditConsultationBean.selectedLog.ressourceId}
|
||||
</h:panelGroup>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Date:</strong>
|
||||
<p>#{auditConsultationBean.selectedLog.dateAction}</p>
|
||||
|
||||
<!-- Description -->
|
||||
<h:panelGroup rendered="#{not empty auditConsultationBean.selectedLog.description}"
|
||||
layout="block">
|
||||
<span class="text-500 text-sm block mb-1">Description</span>
|
||||
<span class="text-900">#{auditConsultationBean.selectedLog.description}</span>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- Adresse IP -->
|
||||
<h:panelGroup rendered="#{not empty auditConsultationBean.selectedLog.ipAddress}"
|
||||
layout="block">
|
||||
<span class="text-500 text-sm block mb-1">Adresse IP</span>
|
||||
<span class="text-900 font-mono">#{auditConsultationBean.selectedLog.ipAddress}</span>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- User Agent -->
|
||||
<h:panelGroup rendered="#{not empty auditConsultationBean.selectedLog.userAgent}"
|
||||
layout="block">
|
||||
<span class="text-500 text-sm block mb-1">User Agent</span>
|
||||
<span class="text-700 text-sm"
|
||||
style="word-break:break-all;">#{auditConsultationBean.selectedLog.userAgent}</span>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- Message d'erreur -->
|
||||
<h:panelGroup rendered="#{not empty auditConsultationBean.selectedLog.errorMessage}"
|
||||
layout="block">
|
||||
<div class="p-3 border-round surface-ground border-1 border-red-200">
|
||||
<span class="text-red-600 text-sm font-semibold block mb-1">
|
||||
<i class="pi pi-exclamation-triangle mr-1"></i>Message d'erreur
|
||||
</span>
|
||||
<span class="text-red-700 text-sm">
|
||||
#{auditConsultationBean.selectedLog.errorMessage}
|
||||
</span>
|
||||
</div>
|
||||
<c:if test="#{not empty auditConsultationBean.selectedLog.details}">
|
||||
<div>
|
||||
<strong>Détails:</strong>
|
||||
<p>#{auditConsultationBean.selectedLog.details}</p>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- Realm -->
|
||||
<h:panelGroup rendered="#{not empty auditConsultationBean.selectedLog.realmName}"
|
||||
layout="block">
|
||||
<span class="text-500 text-sm block mb-1">Realm</span>
|
||||
<p:tag value="#{auditConsultationBean.selectedLog.realmName}" severity="info"
|
||||
styleClass="text-xs" />
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
</c:if>
|
||||
<c:if test="#{not empty auditConsultationBean.selectedLog.adresseIp}">
|
||||
<div>
|
||||
<strong>Adresse IP:</strong>
|
||||
<p>#{auditConsultationBean.selectedLog.adresseIp}</p>
|
||||
</h:panelGroup>
|
||||
|
||||
<h:panelGroup rendered="#{empty auditConsultationBean.selectedLog}" layout="block">
|
||||
<div class="text-center text-color-secondary p-4">
|
||||
<i class="pi pi-info-circle text-3xl mb-2 block"></i>
|
||||
Sélectionnez un log pour voir ses détails.
|
||||
</div>
|
||||
</c:if>
|
||||
<c:if test="#{not empty auditConsultationBean.selectedLog.userAgent}">
|
||||
<div>
|
||||
<strong>User Agent:</strong>
|
||||
<p>#{auditConsultationBean.selectedLog.userAgent}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
<c:if test="#{not empty auditConsultationBean.selectedLog.messageErreur}">
|
||||
<div>
|
||||
<strong class="text-red-600">Message d'erreur:</strong>
|
||||
<p class="text-red-600">#{auditConsultationBean.selectedLog.messageErreur}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
</div>
|
||||
</c:if>
|
||||
</h:panelGroup>
|
||||
</h:form>
|
||||
</p:dialog>
|
||||
</ui:define>
|
||||
|
||||
@@ -62,15 +62,13 @@
|
||||
<ui:param name="clickOutcome" value="/pages/user-manager/audit/logs" />
|
||||
</ui:include>
|
||||
|
||||
<!-- KPI 4: Rôles Client -->
|
||||
<!-- KPI 4: Sessions Actives -->
|
||||
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
|
||||
<ui:param name="title" value="Rôles Client" />
|
||||
<ui:param name="value" value="-" />
|
||||
<ui:param name="icon" value="pi-key" />
|
||||
<ui:param name="title" value="Sessions Actives" />
|
||||
<ui:param name="value" value="#{dashboardBean.activeSessionsDisplay}" />
|
||||
<ui:param name="icon" value="pi-desktop" />
|
||||
<ui:param name="iconColor" value="purple-600" />
|
||||
<ui:param name="subtitle" value="Rôles clients configurés" />
|
||||
<ui:param name="clickable" value="true" />
|
||||
<ui:param name="clickOutcome" value="/pages/user-manager/roles/list" />
|
||||
<ui:param name="subtitle" value="#{dashboardBean.onlineUsersDisplay} utilisateur(s) en ligne" />
|
||||
</ui:include>
|
||||
</ui:define>
|
||||
</ui:decorate>
|
||||
@@ -116,7 +114,7 @@
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<span class="text-600">Realm Keycloak</span>
|
||||
<span class="font-semibold">lions-user-manager</span>
|
||||
<span class="font-semibold">#{dashboardBean.realmName}</span>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<span class="text-600">Statut</span>
|
||||
|
||||
@@ -1,101 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:h="http://xmlns.jcp.org/jsf/html"
|
||||
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"
|
||||
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
|
||||
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">
|
||||
|
||||
<ui:param name="page" value="#{roleGestionBean}"/>
|
||||
<ui:param name="page" value="#{roleGestionBean}" />
|
||||
<ui:define name="title">Gestion des Rôles - Lions User Manager</ui:define>
|
||||
|
||||
<ui:define name="content">
|
||||
<!-- En-tête -->
|
||||
<ui:include src="/templates/components/layout/page-header.xhtml">
|
||||
<!-- En-tête — ui:decorate requis pour que ui:define name="actions" fonctionne -->
|
||||
<ui:decorate template="/templates/components/layout/page-header.xhtml">
|
||||
<ui:param name="icon" value="pi pi-shield text-purple-500" />
|
||||
<ui:param name="title" value="Gestion des Rôles" />
|
||||
<ui:param name="description" value="Gestion des rôles Realm et Client Keycloak" />
|
||||
<ui:define name="actions">
|
||||
<h:form id="formActionsRoles">
|
||||
<div class="flex gap-2">
|
||||
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
|
||||
<ui:param name="value" value="Nouveau Rôle Realm" />
|
||||
<ui:param name="icon" value="pi pi-plus" />
|
||||
<ui:param name="hasAction" value="false" />
|
||||
<ui:param name="hasOutcome" value="false" />
|
||||
<ui:param name="onclick" value="PF('createRealmRoleDialog').show()" />
|
||||
<ui:param name="severity" value="success" />
|
||||
</ui:include>
|
||||
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
|
||||
<ui:param name="value" value="Nouveau Rôle Client" />
|
||||
<ui:param name="icon" value="pi pi-plus-circle" />
|
||||
<ui:param name="hasAction" value="false" />
|
||||
<ui:param name="hasOutcome" value="false" />
|
||||
<ui:param name="onclick" value="PF('createClientRoleDialog').show()" />
|
||||
<ui:param name="severity" value="info" />
|
||||
</ui:include>
|
||||
<p:commandButton value="Nouveau Rôle Realm" icon="pi pi-plus"
|
||||
styleClass="p-button-success"
|
||||
type="button" onclick="PF('createRealmRoleDialog').show()" />
|
||||
<p:commandButton value="Nouveau Rôle Client" icon="pi pi-plus-circle"
|
||||
styleClass="p-button-info"
|
||||
type="button" onclick="PF('createClientRoleDialog').show()" />
|
||||
</div>
|
||||
</h:form>
|
||||
</ui:define>
|
||||
</ui:include>
|
||||
</ui:decorate>
|
||||
|
||||
<!-- Messages globaux -->
|
||||
<p:growl id="growlMessages" showDetail="true" life="5000" />
|
||||
|
||||
<!-- Filtres -->
|
||||
<div class="card mb-3">
|
||||
<h:form id="formFilters">
|
||||
<p:panelGrid columns="3" styleClass="w-full" columnClasses="col-12 md:col-4">
|
||||
<p:outputLabel for="realmFilter" value="Realm" />
|
||||
<p:selectOneMenu id="realmFilter"
|
||||
value="#{roleGestionBean.realmName}"
|
||||
styleClass="w-full">
|
||||
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
|
||||
<div class="flex align-items-center mb-3">
|
||||
<i class="pi pi-filter mr-2 text-color-secondary"></i>
|
||||
<h5 class="m-0">Filtres</h5>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field">
|
||||
<p:outputLabel for="realmFilter" value="Realm" styleClass="font-medium" />
|
||||
<p:selectOneMenu id="realmFilter" value="#{roleGestionBean.realmName}" styleClass="w-full">
|
||||
<f:selectItem itemLabel="Sélectionner un realm..." itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableRealms}" />
|
||||
<p:ajax event="change"
|
||||
listener="#{roleGestionBean.loadRealmRoles}"
|
||||
update=":formRealmRoles:realmRolesPanel :formClientRoles:clientRolesPanel" />
|
||||
<p:ajax event="change" listener="#{roleGestionBean.loadRealmRoles}"
|
||||
update=":formRealmRoles :formClientRoles :formFilters:clientFilter :growlMessages" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p:outputLabel for="clientFilter" value="Client" />
|
||||
<p:selectOneMenu id="clientFilter"
|
||||
value="#{roleGestionBean.clientName}"
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field">
|
||||
<p:outputLabel for="clientFilter" value="Client" styleClass="font-medium" />
|
||||
<p:selectOneMenu id="clientFilter" value="#{roleGestionBean.clientName}"
|
||||
styleClass="w-full">
|
||||
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
|
||||
<f:selectItem itemLabel="Sélectionner un client..." itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.availableClients}" />
|
||||
<p:ajax event="change"
|
||||
listener="#{roleGestionBean.loadClientRoles}"
|
||||
update=":formClientRoles:clientRolesPanel" />
|
||||
<p:ajax event="change" listener="#{roleGestionBean.loadClientRoles}"
|
||||
update=":formClientRoles :growlMessages" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p:outputLabel for="typeFilter" value="Type" />
|
||||
<p:selectOneMenu id="typeFilter"
|
||||
value="#{roleGestionBean.selectedTypeRole}"
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field">
|
||||
<p:outputLabel for="typeFilter" value="Filtrer par type" styleClass="font-medium" />
|
||||
<p:selectOneMenu id="typeFilter" value="#{roleGestionBean.selectedTypeRole}"
|
||||
styleClass="w-full">
|
||||
<f:selectItem itemLabel="Tous les types" itemValue="" />
|
||||
<f:selectItems value="#{roleGestionBean.typeRoleOptions}" />
|
||||
<f:selectItem itemLabel="Tous les types" itemValue="#{null}" />
|
||||
<f:selectItems value="#{roleGestionBean.typeRoleOptions}" var="t"
|
||||
itemLabel="#{t.libelle}" itemValue="#{t}" />
|
||||
<p:ajax event="change" listener="#{roleGestionBean.applyTypeFilter}"
|
||||
update=":formRealmRoles :formClientRoles" />
|
||||
</p:selectOneMenu>
|
||||
</p:panelGrid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</div>
|
||||
|
||||
<!-- Rôles Realm -->
|
||||
<div class="card mb-3">
|
||||
<h:form id="formRealmRoles">
|
||||
<p:panel id="realmRolesPanel" header="Rôles Realm" toggleable="true" collapsed="false">
|
||||
<p:panel id="realmRolesPanel" toggleable="true" collapsed="false">
|
||||
<f:facet name="header">
|
||||
<div class="flex align-items-center justify-content-between w-full">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-globe text-green-500"></i>
|
||||
<span class="font-semibold">Rôles Realm</span>
|
||||
</div>
|
||||
<p:tag value="#{roleGestionBean.filteredRealmRoles.size()}" severity="success"
|
||||
styleClass="text-xs" rendered="#{not empty roleGestionBean.filteredRealmRoles}" />
|
||||
</div>
|
||||
</f:facet>
|
||||
<div class="grid">
|
||||
<c:forEach var="role" items="#{roleGestionBean.realmRoles}">
|
||||
<ui:repeat var="role" value="#{roleGestionBean.filteredRealmRoles}">
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<ui:include src="/templates/components/role-management/role-card.xhtml">
|
||||
<ui:param name="role" value="#{role}" />
|
||||
<ui:param name="showActions" value="true" />
|
||||
<ui:param name="clickable" value="false" />
|
||||
<ui:param name="showEdit" value="false" />
|
||||
<ui:param name="deleteBean" value="#{roleGestionBean}" />
|
||||
<ui:param name="deleteMethod" value="deleteRealmRole" />
|
||||
</ui:include>
|
||||
</div>
|
||||
</c:forEach>
|
||||
<c:if test="#{empty roleGestionBean.realmRoles}">
|
||||
<div class="col-12">
|
||||
<p class="text-center text-color-secondary">Aucun rôle Realm trouvé</p>
|
||||
</ui:repeat>
|
||||
<h:panelGroup layout="block" styleClass="col-12"
|
||||
rendered="#{empty roleGestionBean.filteredRealmRoles}">
|
||||
<div class="text-center text-color-secondary py-4">
|
||||
<i class="pi pi-info-circle text-2xl block mb-2"></i>
|
||||
<span>Aucun rôle Realm trouvé</span>
|
||||
</div>
|
||||
</c:if>
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
|
||||
</p:panel>
|
||||
</h:form>
|
||||
</div>
|
||||
@@ -103,64 +123,83 @@
|
||||
<!-- Rôles Client -->
|
||||
<div class="card">
|
||||
<h:form id="formClientRoles">
|
||||
<p:panel id="clientRolesPanel" header="Rôles Client" toggleable="true" collapsed="false">
|
||||
<p:panel id="clientRolesPanel" toggleable="true" collapsed="false">
|
||||
<f:facet name="header">
|
||||
<div class="flex align-items-center justify-content-between w-full">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-desktop text-blue-500"></i>
|
||||
<span class="font-semibold">Rôles Client</span>
|
||||
</div>
|
||||
<p:tag value="#{roleGestionBean.filteredClientRoles.size()}" severity="info"
|
||||
styleClass="text-xs" rendered="#{not empty roleGestionBean.filteredClientRoles}" />
|
||||
</div>
|
||||
</f:facet>
|
||||
<div class="grid">
|
||||
<c:forEach var="role" items="#{roleGestionBean.clientRoles}">
|
||||
<ui:repeat var="role" value="#{roleGestionBean.filteredClientRoles}">
|
||||
<div class="col-12 md:col-6 lg:col-4">
|
||||
<ui:include src="/templates/components/role-management/role-card.xhtml">
|
||||
<ui:param name="role" value="#{role}" />
|
||||
<ui:param name="showActions" value="true" />
|
||||
<ui:param name="clickable" value="false" />
|
||||
<ui:param name="showEdit" value="false" />
|
||||
<ui:param name="deleteBean" value="#{roleGestionBean}" />
|
||||
<ui:param name="deleteMethod" value="deleteClientRole" />
|
||||
</ui:include>
|
||||
</div>
|
||||
</c:forEach>
|
||||
<c:if test="#{empty roleGestionBean.clientRoles}">
|
||||
<div class="col-12">
|
||||
<p class="text-center text-color-secondary">Aucun rôle Client trouvé</p>
|
||||
</ui:repeat>
|
||||
<h:panelGroup layout="block" styleClass="col-12"
|
||||
rendered="#{empty roleGestionBean.filteredClientRoles}">
|
||||
<div class="text-center text-color-secondary py-4">
|
||||
<i class="pi pi-info-circle text-2xl block mb-2"></i>
|
||||
<h:panelGroup rendered="#{empty roleGestionBean.clientName}">
|
||||
<span>Sélectionnez un client pour voir ses rôles</span>
|
||||
</h:panelGroup>
|
||||
<h:panelGroup rendered="#{not empty roleGestionBean.clientName}">
|
||||
<span>Aucun rôle Client trouvé</span>
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
</c:if>
|
||||
</h:panelGroup>
|
||||
</div>
|
||||
|
||||
</p:panel>
|
||||
</h:form>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Création Rôle Realm -->
|
||||
<p:dialog id="createRealmRoleDialog"
|
||||
header="Nouveau Rôle Realm"
|
||||
widgetVar="createRealmRoleDialog"
|
||||
modal="true"
|
||||
styleClass="w-full md:w-6">
|
||||
<p:dialog id="createRealmRoleDialog" header="Nouveau Rôle Realm" widgetVar="createRealmRoleDialog" modal="true"
|
||||
responsive="true" styleClass="w-full md:w-6">
|
||||
<h:form id="formCreateRealmRole">
|
||||
<ui:include src="/templates/components/role-management/role-form.xhtml">
|
||||
<ui:param name="role" value="#{roleGestionBean.newRole}" />
|
||||
<ui:param name="mode" value="create" />
|
||||
<ui:param name="showClientSelector" value="false" />
|
||||
<ui:param name="showRealmSelector" value="false" />
|
||||
<ui:param name="submitAction" value="#{roleGestionBean.createRealmRole}" />
|
||||
<ui:param name="hasSubmitAction" value="true" />
|
||||
<ui:param name="update" value=":formRealmRoles:realmRolesPanel" />
|
||||
<ui:param name="update" value=":formRealmRoles :growlMessages" />
|
||||
<ui:param name="useParentForm" value="true" />
|
||||
<ui:param name="dialogWidgetVar" value="createRealmRoleDialog" />
|
||||
</ui:include>
|
||||
</h:form>
|
||||
</p:dialog>
|
||||
|
||||
<!-- Dialog Création Rôle Client -->
|
||||
<p:dialog id="createClientRoleDialog"
|
||||
header="Nouveau Rôle Client"
|
||||
widgetVar="createClientRoleDialog"
|
||||
modal="true"
|
||||
styleClass="w-full md:w-6">
|
||||
<p:dialog id="createClientRoleDialog" header="Nouveau Rôle Client" widgetVar="createClientRoleDialog"
|
||||
modal="true" responsive="true" styleClass="w-full md:w-6">
|
||||
<h:form id="formCreateClientRole">
|
||||
<ui:include src="/templates/components/role-management/role-form.xhtml">
|
||||
<ui:param name="role" value="#{roleGestionBean.newRole}" />
|
||||
<ui:param name="mode" value="create" />
|
||||
<ui:param name="showClientSelector" value="true" />
|
||||
<ui:param name="showRealmSelector" value="false" />
|
||||
<ui:param name="submitAction" value="#{roleGestionBean.createClientRole}" />
|
||||
<ui:param name="hasSubmitAction" value="true" />
|
||||
<ui:param name="update" value=":formClientRoles:clientRolesPanel" />
|
||||
<ui:param name="update" value=":formClientRoles :growlMessages" />
|
||||
<ui:param name="useParentForm" value="true" />
|
||||
<ui:param name="dialogWidgetVar" value="createClientRoleDialog" />
|
||||
</ui:include>
|
||||
</h:form>
|
||||
</p:dialog>
|
||||
</ui:define>
|
||||
|
||||
</ui:composition>
|
||||
|
||||
|
||||
@@ -92,6 +92,78 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dernier Statut de Sync Card -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="card h-full">
|
||||
<div class="flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="m-0">Dernière Synchronisation</h5>
|
||||
<p:commandButton value="Sync complète" icon="pi pi-play"
|
||||
actionListener="#{syncDashboardBean.forceSyncRealm}"
|
||||
update="@form" styleClass="p-button-sm p-button-warning"
|
||||
title="Forcer une synchronisation complète utilisateurs + rôles" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<span class="text-600">Date:</span>
|
||||
<span class="text-900 font-medium">#{syncDashboardBean.lastSyncDate}</span>
|
||||
</div>
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<span class="text-600">Statut:</span>
|
||||
<p:tag value="#{syncDashboardBean.lastSyncStatusLabel}"
|
||||
severity="#{syncDashboardBean.lastSyncStatusLabel eq 'SUCCESS' ? 'success' :
|
||||
syncDashboardBean.lastSyncStatusLabel eq 'FAILURE' ? 'danger' : 'info'}" />
|
||||
</div>
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<span class="text-600">Éléments traités:</span>
|
||||
<span class="text-900">#{syncDashboardBean.lastSyncItemsProcessed}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cohérence des Données Card -->
|
||||
<div class="col-12 md:col-6">
|
||||
<div class="card h-full">
|
||||
<div class="flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="m-0">Cohérence des Données</h5>
|
||||
<p:commandButton value="Vérifier" icon="pi pi-check-circle"
|
||||
actionListener="#{syncDashboardBean.checkDataConsistency}"
|
||||
update="@form" styleClass="p-button-sm p-button-outlined" />
|
||||
</div>
|
||||
|
||||
<p:panelGrid columns="1" styleClass="w-full" rendered="#{syncDashboardBean.consistencyResult ne null}">
|
||||
<div class="flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-600">Résultat:</span>
|
||||
<p:tag value="#{syncDashboardBean.consistencyStatusLabel}"
|
||||
severity="#{syncDashboardBean.consistencyStatusLabel eq 'OK' ? 'success' :
|
||||
syncDashboardBean.consistencyStatusLabel eq 'ERROR' ? 'danger' : 'warning'}" />
|
||||
</div>
|
||||
<div class="flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-600">Utilisateurs Keycloak:</span>
|
||||
<span class="text-900">#{syncDashboardBean.consistencyResult.usersKeycloakCount}</span>
|
||||
</div>
|
||||
<div class="flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-600">Utilisateurs locaux:</span>
|
||||
<span class="text-900">#{syncDashboardBean.consistencyResult.usersLocalCount}</span>
|
||||
</div>
|
||||
<div class="flex justify-content-between align-items-center">
|
||||
<span class="text-600">Éléments manquants:</span>
|
||||
<span class="text-900 #{syncDashboardBean.consistencyMissingCount gt 0 ? 'text-red-500 font-bold' : ''}">
|
||||
#{syncDashboardBean.consistencyMissingCount}
|
||||
</span>
|
||||
</div>
|
||||
</p:panelGrid>
|
||||
|
||||
<p:outputPanel rendered="#{syncDashboardBean.consistencyResult eq null}">
|
||||
<div class="flex align-items-center justify-content-center py-4 text-600">
|
||||
<i class="pi pi-info-circle mr-2"></i>
|
||||
Cliquez sur "Vérifier" pour analyser la cohérence des données.
|
||||
</div>
|
||||
</p:outputPanel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</ui:define>
|
||||
|
||||
@@ -53,15 +53,18 @@
|
||||
</span>
|
||||
</p:column>
|
||||
|
||||
<p:column exportable="false">
|
||||
<p:column exportable="false" style="width: 70px; text-align: center;">
|
||||
<p:commandButton icon="pi pi-pencil" update=":dialogs:manage-user-content"
|
||||
oncomplete="PF('manageUserDialog').show()"
|
||||
styleClass="edit-button rounded-button ui-button-success" process="@this"
|
||||
style="margin-right: 5px;">
|
||||
styleClass="p-button-text p-button-sm p-button-rounded ui-button-success"
|
||||
process="@this"
|
||||
style="margin-right: .25rem; padding: 0;">
|
||||
<f:setPropertyActionListener value="#{user}" target="#{userView.selectedUser}" />
|
||||
<p:resetInput target=":dialogs:manage-user-content" />
|
||||
</p:commandButton>
|
||||
<p:commandButton class="ui-button-warning rounded-button" icon="pi pi-trash" process="@this"
|
||||
<p:commandButton styleClass="p-button-text p-button-sm p-button-rounded ui-button-warning"
|
||||
icon="pi pi-trash" process="@this"
|
||||
style="padding: 0;"
|
||||
oncomplete="PF('deleteUserDialog').show()">
|
||||
<f:setPropertyActionListener value="#{user}" target="#{userView.selectedUser}" />
|
||||
</p:commandButton>
|
||||
|
||||
@@ -8,31 +8,45 @@
|
||||
|
||||
<ui:define name="content">
|
||||
<div class="grid">
|
||||
<!-- ================================================================
|
||||
EN-TÊTE DE LA PAGE
|
||||
================================================================ -->
|
||||
<!-- En-tête -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-user-plus text-green-500" style="font-size: 2rem"></i>
|
||||
<div>
|
||||
<h3 class="m-0 mb-1">Nouvel Utilisateur</h3>
|
||||
<p class="text-600 m-0">Créer un nouvel utilisateur dans Keycloak</p>
|
||||
<ui:decorate template="/templates/components/layout/page-header.xhtml">
|
||||
<ui:param name="icon" value="pi pi-user-plus text-green-500" />
|
||||
<ui:param name="title" value="Nouvel Utilisateur" />
|
||||
<ui:param name="description" value="Créer un nouvel utilisateur dans Keycloak" />
|
||||
<ui:param name="breadcrumbParent" value="Utilisateurs" />
|
||||
<ui:param name="breadcrumbParentLink" value="/pages/user-manager/users/list" />
|
||||
<ui:define name="actions">
|
||||
<h:link outcome="/pages/user-manager/users/list"
|
||||
styleClass="p-button p-button-outlined p-button-secondary">
|
||||
<i class="pi pi-arrow-left mr-2"></i>Retour à la liste
|
||||
</h:link>
|
||||
</ui:define>
|
||||
</ui:decorate>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire -->
|
||||
<h:form id="formUserCreation" styleClass="col-12 grid m-0 p-0">
|
||||
|
||||
<!-- Bandeau succès après création -->
|
||||
<h:panelGroup layout="block" styleClass="col-12" rendered="#{userCreationBean.creationSuccess}">
|
||||
<div class="card surface-ground border-1 border-green-300 border-round">
|
||||
<div class="flex align-items-center gap-3">
|
||||
<i class="pi pi-check-circle text-green-500 text-3xl"></i>
|
||||
<div class="flex-grow-1">
|
||||
<span class="text-900 font-semibold">Utilisateur créé avec succès !</span>
|
||||
<p class="text-600 text-sm m-0 mt-1">
|
||||
Vous pouvez créer un autre utilisateur ou retourner à la liste.
|
||||
</p>
|
||||
</div>
|
||||
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button p-button-text">
|
||||
<i class="pi pi-arrow-left mr-2"></i>
|
||||
Retour à la liste
|
||||
<h:link outcome="/pages/user-manager/users/list"
|
||||
styleClass="p-button p-button-success p-button-sm">
|
||||
<i class="pi pi-list mr-2"></i>Voir la liste
|
||||
</h:link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:panelGroup>
|
||||
|
||||
<!-- ================================================================
|
||||
FORMULAIRE DE CRÉATION
|
||||
================================================================ -->
|
||||
<h:form id="formUserCreation" styleClass="col-12 grid m-0 p-0">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
@@ -41,7 +55,7 @@
|
||||
</h3>
|
||||
|
||||
<div class="grid">
|
||||
<!-- Colonne gauche: Informations de base -->
|
||||
<!-- Colonne gauche -->
|
||||
<div class="col-12 lg:col-6">
|
||||
<div class="surface-50 border-round p-3 mb-3">
|
||||
<h4 class="text-900 font-semibold mb-3 flex align-items-center gap-2">
|
||||
@@ -49,100 +63,104 @@
|
||||
<span>Informations de Base</span>
|
||||
</h4>
|
||||
|
||||
<!-- Nom d'utilisateur -->
|
||||
<div class="field mb-3">
|
||||
<label for="username" class="block text-900 font-medium mb-2">
|
||||
Nom d'utilisateur <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:outputLabel for="username" value="Nom d'utilisateur"
|
||||
styleClass="font-medium" />
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
<p:inputText id="username" value="#{userCreationBean.newUser.username}"
|
||||
styleClass="w-full" required="true" placeholder="ex: jdupont">
|
||||
styleClass="w-full" required="true"
|
||||
requiredMessage="Le nom d'utilisateur est obligatoire"
|
||||
placeholder="ex: jdupont">
|
||||
<f:validateLength minimum="3" maximum="50" />
|
||||
</p:inputText>
|
||||
<small class="text-500">Identifiant unique de connexion</small>
|
||||
<p:message for="username" display="text" styleClass="mt-1" />
|
||||
<small class="text-500 block mt-1">Identifiant unique de connexion</small>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="field mb-3">
|
||||
<label for="email" class="block text-900 font-medium mb-2">
|
||||
Adresse email <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:outputLabel for="email" value="Adresse email" styleClass="font-medium" />
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
<p:inputText id="email" value="#{userCreationBean.newUser.email}"
|
||||
styleClass="w-full" required="true" type="email"
|
||||
styleClass="w-full" required="true"
|
||||
requiredMessage="L'adresse email est obligatoire"
|
||||
placeholder="ex: jean.dupont@example.com">
|
||||
<f:validateRegex pattern="^[A-Za-z0-9+_.-]+@(.+)$" />
|
||||
</p:inputText>
|
||||
<p:message for="email" display="text" styleClass="mt-1" />
|
||||
</div>
|
||||
|
||||
<!-- Prénom -->
|
||||
<div class="field mb-3">
|
||||
<label for="prenom" class="block text-900 font-medium mb-2">
|
||||
Prénom <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:outputLabel for="prenom" value="Prénom" styleClass="font-medium" />
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
<p:inputText id="prenom" value="#{userCreationBean.newUser.prenom}"
|
||||
styleClass="w-full" required="true" placeholder="ex: Jean">
|
||||
styleClass="w-full" required="true"
|
||||
requiredMessage="Le prénom est obligatoire"
|
||||
placeholder="ex: Jean">
|
||||
<f:validateLength minimum="2" maximum="100" />
|
||||
</p:inputText>
|
||||
<p:message for="prenom" display="text" styleClass="mt-1" />
|
||||
</div>
|
||||
|
||||
<!-- Nom -->
|
||||
<div class="field mb-0">
|
||||
<label for="nom" class="block text-900 font-medium mb-2">
|
||||
Nom <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:outputLabel for="nom" value="Nom" styleClass="font-medium" />
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
<p:inputText id="nom" value="#{userCreationBean.newUser.nom}"
|
||||
styleClass="w-full" required="true" placeholder="ex: Dupont">
|
||||
styleClass="w-full" required="true"
|
||||
requiredMessage="Le nom est obligatoire"
|
||||
placeholder="ex: Dupont">
|
||||
<f:validateLength minimum="2" maximum="100" />
|
||||
</p:inputText>
|
||||
<p:message for="nom" display="text" styleClass="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite: Mot de passe et Configuration -->
|
||||
<!-- Colonne droite -->
|
||||
<div class="col-12 lg:col-6">
|
||||
<!-- Section Mot de passe -->
|
||||
<div class="surface-50 border-round p-3 mb-3 ui-fluid">
|
||||
<h4 class="text-900 font-semibold mb-3 flex align-items-center gap-2">
|
||||
<i class="pi pi-key text-orange-500"></i>
|
||||
<span>Mot de Passe</span>
|
||||
</h4>
|
||||
|
||||
<!-- Mot de passe -->
|
||||
<div class="field mb-3">
|
||||
<label for="password" class="block text-900 font-medium mb-2">
|
||||
Mot de passe <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:outputLabel for="password" value="Mot de passe"
|
||||
styleClass="font-medium" />
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
<p:password id="password" value="#{userCreationBean.password}"
|
||||
styleClass="w-full" required="true" feedback="true" toggleMask="true"
|
||||
styleClass="w-full" required="true"
|
||||
requiredMessage="Le mot de passe est obligatoire"
|
||||
feedback="true" toggleMask="true"
|
||||
placeholder="Minimum 8 caractères">
|
||||
<f:validateLength minimum="8" maximum="100" />
|
||||
</p:password>
|
||||
<small class="text-500">Au moins 8 caractères</small>
|
||||
<p:message for="password" display="text" styleClass="mt-1" />
|
||||
<small class="text-500 block mt-1">Au moins 8 caractères</small>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation mot de passe -->
|
||||
<div class="field mb-0">
|
||||
<label for="passwordConfirm" class="block text-900 font-medium mb-2">
|
||||
Confirmer le mot de passe <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<p:password id="passwordConfirm" value="#{userCreationBean.passwordConfirm}"
|
||||
styleClass="w-full" required="true" feedback="false" toggleMask="true"
|
||||
placeholder="Confirmer le mot de passe">
|
||||
</p:password>
|
||||
<p:outputLabel for="passwordConfirm" value="Confirmer le mot de passe"
|
||||
styleClass="font-medium" />
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
<p:password id="passwordConfirm"
|
||||
value="#{userCreationBean.passwordConfirm}"
|
||||
styleClass="w-full" required="true"
|
||||
requiredMessage="Veuillez confirmer le mot de passe"
|
||||
feedback="false" toggleMask="true"
|
||||
placeholder="Confirmer le mot de passe" />
|
||||
<p:message for="passwordConfirm" display="text" styleClass="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Configuration -->
|
||||
<div class="surface-50 border-round p-3">
|
||||
<h4 class="text-900 font-semibold mb-3 flex align-items-center gap-2">
|
||||
<i class="pi pi-cog text-purple-500"></i>
|
||||
<span>Configuration</span>
|
||||
</h4>
|
||||
|
||||
<!-- Realm -->
|
||||
<div class="field mb-3">
|
||||
<label for="realm" class="block text-900 font-medium mb-2">
|
||||
Realm Keycloak
|
||||
</label>
|
||||
<p:outputLabel for="realm" value="Realm Keycloak"
|
||||
styleClass="font-medium" />
|
||||
<p:selectOneMenu id="realm" value="#{userCreationBean.realmName}"
|
||||
styleClass="w-full">
|
||||
<f:selectItems value="#{userCreationBean.availableRealms}" var="realm"
|
||||
@@ -150,22 +168,16 @@
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
|
||||
<!-- Options de configuration -->
|
||||
<div class="flex flex-column gap-2">
|
||||
<!-- Compte activé -->
|
||||
<div class="field-checkbox mb-0">
|
||||
<div class="flex flex-column gap-3">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<p:selectBooleanCheckbox id="enabled"
|
||||
value="#{userCreationBean.newUser.enabled}">
|
||||
</p:selectBooleanCheckbox>
|
||||
<label for="enabled" class="ml-2">Compte activé</label>
|
||||
value="#{userCreationBean.newUser.enabled}" />
|
||||
<p:outputLabel for="enabled" value="Compte activé" />
|
||||
</div>
|
||||
|
||||
<!-- Email vérifié -->
|
||||
<div class="field-checkbox mb-0">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<p:selectBooleanCheckbox id="emailVerified"
|
||||
value="#{userCreationBean.newUser.emailVerified}">
|
||||
</p:selectBooleanCheckbox>
|
||||
<label for="emailVerified" class="ml-2">Email vérifié</label>
|
||||
value="#{userCreationBean.newUser.emailVerified}" />
|
||||
<p:outputLabel for="emailVerified" value="Email vérifié" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,49 +186,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
ACTIONS
|
||||
================================================================ -->
|
||||
<!-- Actions -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<h3 class="text-900 font-semibold text-lg mb-4 flex align-items-center gap-2">
|
||||
<i class="pi pi-check-circle text-green-500"></i>
|
||||
Actions
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-wrap gap-2 align-items-center">
|
||||
<!-- Bouton Créer -->
|
||||
<p:commandButton value="Créer l'utilisateur" icon="pi pi-check"
|
||||
styleClass="p-button-success" action="#{userCreationBean.createUser}"
|
||||
update=":formUserCreation" validateClient="true">
|
||||
</p:commandButton>
|
||||
update=":formUserCreation" validateClient="true" />
|
||||
|
||||
<!-- Bouton Réinitialiser -->
|
||||
<p:commandButton value="Réinitialiser" icon="pi pi-refresh"
|
||||
styleClass="p-button-outlined p-button-secondary" action="#{userCreationBean.resetForm}"
|
||||
update=":formUserCreation" immediate="true">
|
||||
styleClass="p-button-outlined p-button-secondary"
|
||||
action="#{userCreationBean.resetForm}"
|
||||
process="@this" update=":formUserCreation" immediate="true">
|
||||
<p:confirm header="Confirmation"
|
||||
message="Voulez-vous vraiment réinitialiser le formulaire ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
|
||||
<!-- Bouton Annuler -->
|
||||
<p:commandButton value="Annuler" icon="pi pi-times" styleClass="p-button-outlined"
|
||||
action="#{userCreationBean.cancel}" immediate="true">
|
||||
<p:confirm header="Confirmation" message="Voulez-vous vraiment annuler la création ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
<p:commandButton value="Annuler" icon="pi pi-times"
|
||||
styleClass="p-button-outlined"
|
||||
action="#{userCreationBean.cancel}" immediate="true"
|
||||
ajax="false">
|
||||
</p:commandButton>
|
||||
|
||||
<div class="flex-grow-1"></div>
|
||||
|
||||
<!-- Aide -->
|
||||
<p:commandButton value="Aide" icon="pi pi-question-circle"
|
||||
styleClass="p-button-outlined p-button-help" type="button"
|
||||
onclick="PF('helpDialog').show();">
|
||||
</p:commandButton>
|
||||
onclick="PF('helpDialog').show();" />
|
||||
</div>
|
||||
|
||||
<!-- Message d'information -->
|
||||
<p:messages id="messages" showDetail="true" closable="true" styleClass="mt-3">
|
||||
<p:autoUpdate />
|
||||
</p:messages>
|
||||
@@ -225,17 +224,7 @@
|
||||
</h:form>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG DE CONFIRMATION
|
||||
================================================================ -->
|
||||
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade" responsive="true" width="400">
|
||||
<p:commandButton value="Non" type="button" styleClass="p-button-text" icon="pi pi-times" />
|
||||
<p:commandButton value="Oui" type="button" styleClass="p-button-primary" icon="pi pi-check" />
|
||||
</p:confirmDialog>
|
||||
|
||||
<!-- ================================================================
|
||||
DIALOG D'AIDE
|
||||
================================================================ -->
|
||||
<!-- Dialog d'aide -->
|
||||
<p:dialog header="Aide - Création d'Utilisateur" widgetVar="helpDialog" modal="true" responsive="true"
|
||||
width="600" showEffect="fade" hideEffect="fade">
|
||||
<div class="grid">
|
||||
@@ -247,6 +236,7 @@
|
||||
<ul class="text-700 line-height-3 mb-4">
|
||||
<li><strong>Nom d'utilisateur</strong> : Identifiant unique (3-50 caractères)</li>
|
||||
<li><strong>Email</strong> : Adresse email valide</li>
|
||||
<li><strong>Prénom / Nom</strong> : Au moins 2 caractères chacun</li>
|
||||
<li><strong>Mot de passe</strong> : Au moins 8 caractères</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -7,31 +7,31 @@
|
||||
<ui:define name="title">Liste des Utilisateurs - Lions User Manager</ui:define>
|
||||
|
||||
<ui:define name="content">
|
||||
<h:form id="formUserList">
|
||||
<div class="grid">
|
||||
<!-- ================================================================
|
||||
EN-TÊTE DE LA PAGE
|
||||
================================================================ -->
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="flex align-items-center gap-2">
|
||||
<i class="pi pi-users text-blue-500" style="font-size: 2rem"></i>
|
||||
<div>
|
||||
<h3 class="m-0 mb-1">Gestion des Utilisateurs</h3>
|
||||
<p class="text-600 m-0">Gestion centralisée des utilisateurs Keycloak - Recherche,
|
||||
création, modification et suppression</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- En-tête -->
|
||||
<ui:decorate template="/templates/components/layout/page-header.xhtml">
|
||||
<ui:param name="icon" value="pi pi-users text-blue-500" />
|
||||
<ui:param name="title" value="Gestion des Utilisateurs" />
|
||||
<ui:param name="description"
|
||||
value="Gestion centralisée des utilisateurs Keycloak - Recherche, création, modification et suppression" />
|
||||
<ui:define name="actions">
|
||||
<h:form id="formHeaderActions">
|
||||
<div class="flex gap-2">
|
||||
<p:commandButton value="Rafraîchir" icon="pi pi-refresh" styleClass="p-button-secondary"
|
||||
action="#{userListBean.refreshData}" update=":formUserList" process="@this" />
|
||||
<p:commandButton value="Nouvel Utilisateur" icon="pi pi-user-plus"
|
||||
styleClass="p-button-success" outcome="/pages/user-manager/users/create" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h:form>
|
||||
</ui:define>
|
||||
</ui:decorate>
|
||||
|
||||
<!-- Messages globaux -->
|
||||
<p:growl id="growlMessages" showDetail="true" life="5000">
|
||||
<p:autoUpdate />
|
||||
</p:growl>
|
||||
|
||||
<h:form id="formUserList">
|
||||
<div class="grid">
|
||||
|
||||
<!-- ================================================================
|
||||
STATISTIQUES KPI (4 CARTES)
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="flex align-items-start justify-content-between mb-3">
|
||||
<div>
|
||||
<div class="text-500 font-medium mb-1">Total Utilisateurs</div>
|
||||
<div class="text-900 font-bold text-2xl">#{userListBean.totalRecords}</div>
|
||||
<div class="text-900 font-bold text-2xl">#{userListBean.kpiTotalUsers}</div>
|
||||
</div>
|
||||
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
|
||||
style="width: 2.5rem; height: 2.5rem">
|
||||
@@ -154,9 +154,8 @@
|
||||
<div class="col-12 md:col-6 lg:col-3">
|
||||
<label for="realmSelect" class="block text-900 font-medium mb-2">Realm</label>
|
||||
<p:selectOneMenu id="realmSelect" value="#{userListBean.realmName}" styleClass="w-full">
|
||||
<f:selectItem itemLabel="lions-user-manager" itemValue="lions-user-manager" />
|
||||
<f:selectItem itemLabel="master" itemValue="master" />
|
||||
<p:ajax update=":formUserList:userTable" listener="#{userListBean.search}" />
|
||||
<f:selectItems value="#{userListBean.availableRealms}" />
|
||||
<p:ajax update=":formUserList" listener="#{userListBean.onRealmChange}" />
|
||||
</p:selectOneMenu>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -399,16 +399,6 @@
|
||||
<!-- ================================================================
|
||||
DIALOG DE CONFIRMATION
|
||||
================================================================ -->
|
||||
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade"
|
||||
responsive="true" width="400">
|
||||
<p:commandButton value="Non" type="button"
|
||||
styleClass="p-button-text"
|
||||
icon="pi pi-times" />
|
||||
<p:commandButton value="Oui" type="button"
|
||||
styleClass="p-button-danger"
|
||||
icon="pi pi-check" />
|
||||
</p:confirmDialog>
|
||||
|
||||
<!-- Animation CSS pour le badge "Connecté" -->
|
||||
<style>
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
</div>
|
||||
</ui:fragment>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<p:growl id="growlMessages" showDetail="true" life="5000">
|
||||
<p:autoUpdate />
|
||||
</p:growl>
|
||||
|
||||
</ui:define>
|
||||
|
||||
</ui:composition>
|
||||
@@ -41,7 +41,8 @@
|
||||
<c:set var="showActions" value="#{empty showActions ? true : showActions}" />
|
||||
<c:set var="showDescription" value="#{empty showDescription ? true : showDescription}" />
|
||||
<c:set var="showCompositeInfo" value="#{empty showCompositeInfo ? true : showCompositeInfo}" />
|
||||
<c:set var="clickable" value="#{empty clickable ? true : clickable}" />
|
||||
<c:set var="clickable" value="#{empty clickable ? false : clickable}" />
|
||||
<c:set var="showEdit" value="#{empty showEdit ? false : showEdit}" />
|
||||
<c:set var="editOutcome" value="#{empty editOutcome ? '/pages/user-manager/roles/edit' : editOutcome}" />
|
||||
|
||||
<!-- Détermination des styles selon le type de rôle -->
|
||||
@@ -149,16 +150,18 @@
|
||||
<f:facet name="footer">
|
||||
<c:if test="#{showActions}">
|
||||
<div class="flex gap-2 justify-content-end pt-2 border-top-1 surface-border">
|
||||
<c:if test="#{showEdit}">
|
||||
<p:commandButton icon="pi pi-pencil" title="Modifier"
|
||||
styleClass="p-button-text p-button-sm p-button-warning p-button-rounded"
|
||||
outcome="#{editOutcome}">
|
||||
<f:param name="roleId" value="#{role.id}" />
|
||||
</p:commandButton>
|
||||
</c:if>
|
||||
|
||||
<c:if test="#{not empty deleteAction}">
|
||||
<c:if test="#{not empty deleteBean and not empty deleteMethod}">
|
||||
<p:commandButton icon="pi pi-trash" title="Supprimer"
|
||||
styleClass="p-button-text p-button-sm p-button-danger p-button-rounded"
|
||||
action="#{deleteAction}" update="@form">
|
||||
action="#{deleteBean[deleteMethod](role.name)}" update="@form :growlMessages">
|
||||
<p:confirm header="Confirmation" message="Supprimer le rôle #{role.name} ?"
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:commandButton>
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
<p:outputLabel for="roleName" value="Nom du rôle" styleClass="font-medium" />
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
<p:inputText id="roleName" value="#{role.name}" required="true" readonly="#{readonly}"
|
||||
placeholder="ADMIN, USER, MODERATOR..." styleClass="w-full">
|
||||
placeholder="admin, user_manager, sync_manager..." styleClass="w-full">
|
||||
<f:validateLength minimum="2" maximum="100" />
|
||||
<f:validateRegex pattern="^[A-Z][A-Z0-9_]*$" />
|
||||
<f:validateRegex pattern="^[a-zA-Z][a-zA-Z0-9_-]*$" />
|
||||
</p:inputText>
|
||||
<p:message for="roleName" display="text" styleClass="mt-1" />
|
||||
<small class="text-color-secondary block mt-1">
|
||||
Lettres majuscules, chiffres et underscores uniquement (ex: ADMIN_USER)
|
||||
Lettres, chiffres, underscores et tirets (ex: admin, user_manager)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@
|
||||
<p:selectOneMenu id="realmName" value="#{role.realmName}" required="#{showRealmSelector}"
|
||||
readonly="#{readonly}" styleClass="w-full">
|
||||
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
|
||||
<f:selectItems value="#{roleBean.availableRealms}" />
|
||||
<f:selectItems value="#{roleGestionBean.availableRealms}" />
|
||||
</p:selectOneMenu>
|
||||
<p:message for="realmName" display="text" styleClass="mt-1" />
|
||||
</div>
|
||||
@@ -111,7 +111,7 @@
|
||||
<p:selectOneMenu id="clientId" value="#{role.clientId}" required="#{showClientSelector}"
|
||||
readonly="#{readonly}" styleClass="w-full">
|
||||
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
|
||||
<f:selectItems value="#{roleBean.availableClients}" />
|
||||
<f:selectItems value="#{roleGestionBean.availableClients}" />
|
||||
</p:selectOneMenu>
|
||||
<p:message for="clientId" display="text" styleClass="mt-1" />
|
||||
</div>
|
||||
@@ -142,7 +142,8 @@
|
||||
<c:when test="#{hasSubmitAction == true}">
|
||||
<p:commandButton value="#{mode == 'create' ? 'Créer le rôle' : 'Enregistrer'}"
|
||||
icon="pi pi-check" styleClass="p-button-success" action="#{submitAction}"
|
||||
update="#{not empty update ? update : '@form'}" process="@form" validateClient="true" />
|
||||
update="#{not empty update ? update : '@form'}" process="@form" validateClient="true"
|
||||
oncomplete="if(!args.validationFailed && '#{dialogWidgetVar}' !== '') PF('#{dialogWidgetVar}').hide()" />
|
||||
</c:when>
|
||||
<c:when test="#{not empty submitOutcome}">
|
||||
<p:commandButton value="#{mode == 'create' ? 'Créer le rôle' : 'Enregistrer'}"
|
||||
@@ -155,9 +156,19 @@
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</c:if>
|
||||
<p:commandButton value="Annuler" icon="pi pi-times" styleClass="p-button-secondary p-button-outlined"
|
||||
<c:choose>
|
||||
<c:when test="#{not empty dialogWidgetVar}">
|
||||
<p:commandButton value="Annuler" icon="pi pi-times"
|
||||
styleClass="p-button-secondary p-button-outlined"
|
||||
type="button" onclick="PF('#{dialogWidgetVar}').hide()" />
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<p:commandButton value="Annuler" icon="pi pi-times"
|
||||
styleClass="p-button-secondary p-button-outlined"
|
||||
outcome="#{not empty cancelOutcome ? cancelOutcome : '/pages/user-manager/roles/list'}"
|
||||
immediate="true" />
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</div>
|
||||
</f:facet>
|
||||
</p:panel>
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<ui:param name="submitOutcome" value="#{submitOutcome}" />
|
||||
<ui:param name="update" value="#{update}" />
|
||||
<ui:param name="cancelOutcome" value="#{cancelOutcome}" />
|
||||
<ui:param name="dialogWidgetVar" value="#{dialogWidgetVar}" />
|
||||
</ui:include>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
@@ -83,6 +84,7 @@
|
||||
<ui:param name="submitOutcome" value="#{submitOutcome}" />
|
||||
<ui:param name="update" value="#{update}" />
|
||||
<ui:param name="cancelOutcome" value="#{cancelOutcome}" />
|
||||
<ui:param name="dialogWidgetVar" value="#{dialogWidgetVar}" />
|
||||
</ui:include>
|
||||
</h:form>
|
||||
</c:otherwise>
|
||||
|
||||
@@ -63,12 +63,14 @@
|
||||
</div>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<c:if test="#{empty subtitle}">
|
||||
<div class="text-500 text-xs mb-2">
|
||||
<c:choose>
|
||||
<c:when test="#{not empty statusLabel}">Aucun #{statusLabel}</c:when>
|
||||
<c:otherwise>#{noDataLabel}</c:otherwise>
|
||||
</c:choose>
|
||||
</div>
|
||||
</c:if>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</c:when>
|
||||
@@ -100,7 +102,9 @@
|
||||
</div>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<c:if test="#{empty subtitle}">
|
||||
<div class="text-500 text-xs mb-2">#{noDataLabel}</div>
|
||||
</c:if>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</c:otherwise>
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
<div class="#{colSize}">
|
||||
<c:choose>
|
||||
<c:when test="#{clickable and not empty clickOutcome}">
|
||||
<p:commandLink styleClass="card-link w-full #{styleClass}" outcome="#{clickOutcome}">
|
||||
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200">
|
||||
<h:link outcome="#{clickOutcome}"
|
||||
styleClass="card-link w-full no-underline #{styleClass}">
|
||||
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200 cursor-pointer">
|
||||
<ui:include src="/templates/components/shared/cards/kpi-card-content.xhtml">
|
||||
<ui:param name="title" value="#{title}" />
|
||||
<ui:param name="value" value="#{value}" />
|
||||
@@ -43,30 +44,7 @@
|
||||
<ui:param name="statusValue" value="#{statusValue}" />
|
||||
</ui:include>
|
||||
</div>
|
||||
</p:commandLink>
|
||||
</c:when>
|
||||
<c:when test="#{clickable and not empty clickAction}">
|
||||
<p:commandLink styleClass="card-link w-full #{styleClass}" action="#{clickAction}">
|
||||
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200">
|
||||
<ui:include src="/templates/components/shared/cards/kpi-card-content.xhtml">
|
||||
<ui:param name="title" value="#{title}" />
|
||||
<ui:param name="value" value="#{value}" />
|
||||
<ui:param name="icon" value="#{icon}" />
|
||||
<ui:param name="iconColor" value="#{iconColor}" />
|
||||
<ui:param name="subtitle" value="#{subtitle}" />
|
||||
<ui:param name="growthValue" value="#{growthValue}" />
|
||||
<ui:param name="growthLabel" value="#{growthLabel}" />
|
||||
<ui:param name="growthType" value="#{growthType}" />
|
||||
<ui:param name="showGrowth" value="#{showGrowth}" />
|
||||
<ui:param name="noDataLabel" value="#{noDataLabel}" />
|
||||
<ui:param name="progressValue" value="#{progressValue}" />
|
||||
<ui:param name="showProgress" value="#{showProgress}" />
|
||||
<ui:param name="statusIcon" value="#{statusIcon}" />
|
||||
<ui:param name="statusLabel" value="#{statusLabel}" />
|
||||
<ui:param name="statusValue" value="#{statusValue}" />
|
||||
</ui:include>
|
||||
</div>
|
||||
</p:commandLink>
|
||||
</h:link>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="card surface-0 border-round-lg #{styleClass}">
|
||||
|
||||
@@ -179,8 +179,8 @@
|
||||
|
||||
<!-- Colonne Actions -->
|
||||
<c:if test="#{showActions}">
|
||||
<p:column headerText="Actions" style="width: 80px" exportable="false">
|
||||
<div class="flex justify-content-center align-items-center" style="min-height: 2.5rem;">
|
||||
<p:column headerText="Actions" style="width: 70px" exportable="false">
|
||||
<div class="flex justify-content-center align-items-center" style="min-height: 1.75rem;">
|
||||
<ui:include src="/templates/components/user-management/user-actions.xhtml">
|
||||
<ui:param name="user" value="#{user}" />
|
||||
<ui:param name="layout" value="dropdown" />
|
||||
|
||||
@@ -36,15 +36,16 @@
|
||||
value="#{empty logoutSessionsAction ? targetBean.logoutAllSessions(user.id) : logoutSessionsAction}" />
|
||||
|
||||
<c:choose>
|
||||
<!-- Layout Dropdown -->
|
||||
<!-- Layout Dropdown (compact, pour tableau de liste) -->
|
||||
<c:when test="#{layout == 'dropdown'}">
|
||||
<p:commandButton icon="pi pi-ellipsis-v"
|
||||
styleClass="p-button-text p-button-sm p-button-rounded p-button-plain" type="button" title="Actions"
|
||||
style="width: 2rem; height: 2rem;">
|
||||
<p:menu styleClass="w-12rem">
|
||||
<p:menuButton
|
||||
icon="pi pi-ellipsis-v"
|
||||
styleClass="p-button-text p-button-sm p-button-rounded p-button-plain"
|
||||
title="Actions"
|
||||
style="width: 1.6rem; height: 1.6rem; padding: 0;">
|
||||
<c:if test="#{showView}">
|
||||
<p:menuitem value="Voir le profil" icon="pi pi-eye"
|
||||
outcome="#{not empty viewOutcome ? viewOutcome : '/pages/user-manager/users/profile'}">
|
||||
outcome="#{not empty viewOutcome ? viewOutcome : '/pages/user-manager/users/view'}">
|
||||
<f:param name="userId" value="#{user.id}" />
|
||||
</p:menuitem>
|
||||
</c:if>
|
||||
@@ -79,8 +80,7 @@
|
||||
icon="pi pi-exclamation-triangle" />
|
||||
</p:menuitem>
|
||||
</c:if>
|
||||
</p:menu>
|
||||
</p:commandButton>
|
||||
</p:menuButton>
|
||||
</c:when>
|
||||
|
||||
<!-- Layout Horizontal -->
|
||||
@@ -95,7 +95,7 @@
|
||||
<ui:param name="rounded" value="true" />
|
||||
<ui:param name="hasOutcome" value="true" />
|
||||
<ui:param name="outcome"
|
||||
value="#{not empty viewOutcome ? viewOutcome : '/pages/user-manager/users/profile'}" />
|
||||
value="#{not empty viewOutcome ? viewOutcome : '/pages/user-manager/users/view'}" />
|
||||
<ui:param name="paramUserId" value="#{user.id}" />
|
||||
<ui:param name="paramRealm" value="#{user.realmName}" />
|
||||
</ui:include>
|
||||
@@ -170,9 +170,10 @@
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
|
||||
<!-- Dialog de Reset Password (reste spécifique par user car il contient un form) -->
|
||||
<!-- Dialog de Reset Password (appendTo body pour éviter les h:form imbriqués) -->
|
||||
<p:dialog id="resetPasswordDialog_#{user.id}" widgetVar="resetPasswordDialog_#{user.id}"
|
||||
header="Réinitialiser le mot de passe" modal="true" styleClass="w-full md:w-4">
|
||||
header="Réinitialiser le mot de passe" modal="true" styleClass="w-full md:w-4"
|
||||
appendTo="@(body)">
|
||||
<h:form>
|
||||
<div class="field">
|
||||
<p:outputLabel for="newPass" value="Nouveau mot de passe" />
|
||||
|
||||
@@ -15,11 +15,13 @@ quarkus.http.port=8082
|
||||
# ============================================
|
||||
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.credentials.secret=${KEYCLOAK_CLIENT_SECRET:NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO}
|
||||
quarkus.oidc.tls.verification=none
|
||||
|
||||
# ============================================
|
||||
# Backend REST Client DEV
|
||||
# Le serveur API (lions-user-manager-server-impl-quarkus) doit tourner sur ce port (8081 en dev).
|
||||
# Lancer d'abord: cd lions-user-manager-server-impl-quarkus && mvn quarkus:dev
|
||||
# ============================================
|
||||
lions.user.manager.backend.url=http://localhost:8081
|
||||
quarkus.rest-client."lions-user-manager-api".url=http://localhost:8081
|
||||
@@ -33,6 +35,9 @@ quarkus.log.console.level=DEBUG
|
||||
quarkus.log.category."dev.lions.user.manager".level=DEBUG
|
||||
quarkus.log.category."io.quarkus.oidc".level=INFO
|
||||
quarkus.log.category."io.quarkus.oidc.runtime".level=INFO
|
||||
quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG
|
||||
quarkus.rest-client.logging.scope=request-response
|
||||
quarkus.rest-client.logging.body-limit=100
|
||||
|
||||
# ============================================
|
||||
# Dev Services DEV
|
||||
|
||||
@@ -18,7 +18,7 @@ quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:lions-user-manager-client}
|
||||
quarkus.oidc.token.issuer=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/lions-user-manager}
|
||||
quarkus.oidc.tls.verification=required
|
||||
quarkus.oidc.authentication.cookie-same-site=strict
|
||||
quarkus.oidc.authentication.pkce-required=false
|
||||
quarkus.oidc.authentication.pkce-required=true
|
||||
quarkus.oidc.token-state-manager.encryption-secret=${OIDC_ENCRYPTION_SECRET}
|
||||
|
||||
# ============================================
|
||||
|
||||
@@ -59,6 +59,12 @@ quarkus.http.auth.permission.logout.policy=authenticated
|
||||
quarkus.http.auth.permission.dev-ui.paths=/q/*
|
||||
quarkus.http.auth.permission.dev-ui.policy=permit
|
||||
|
||||
# ============================================
|
||||
# Configuration Lions (COMMUNE)
|
||||
# ============================================
|
||||
# Realm par défaut utilisé par les beans (RoleView, UserView)
|
||||
lions.user.manager.default.realm=lions-user-manager
|
||||
|
||||
# ============================================
|
||||
# Keycloak Dev Services désactivé (COMMUNE)
|
||||
# ============================================
|
||||
|
||||
@@ -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.faces.application.FacesMessage;
|
||||
@@ -29,6 +30,9 @@ class AuditConsultationBeanTest {
|
||||
@Mock
|
||||
AuditServiceClient auditServiceClient;
|
||||
|
||||
@Mock
|
||||
RealmServiceClient realmServiceClient;
|
||||
|
||||
@Mock
|
||||
FacesContext facesContext;
|
||||
|
||||
@@ -41,6 +45,7 @@ class AuditConsultationBeanTest {
|
||||
void setUp() {
|
||||
facesContextMock = mockStatic(FacesContext.class);
|
||||
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
|
||||
// Par défaut, aucun message n'est ajouté, les tests configureront si besoin
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -60,6 +65,7 @@ class AuditConsultationBeanTest {
|
||||
when(auditServiceClient.getUserActivityStatistics(isNull(), isNull())).thenReturn(userStats);
|
||||
when(auditServiceClient.getFailureCount(isNull(), isNull())).thenReturn(failureDto);
|
||||
when(auditServiceClient.getSuccessCount(isNull(), isNull())).thenReturn(successDto);
|
||||
when(realmServiceClient.getAllRealms()).thenReturn(java.util.List.of("master"));
|
||||
|
||||
auditConsultationBean.init();
|
||||
|
||||
@@ -177,12 +183,29 @@ class AuditConsultationBeanTest {
|
||||
when(auditServiceClient.exportLogsToCSV(anyString(), anyString()))
|
||||
.thenReturn(response);
|
||||
|
||||
// Contenu CSV simulé
|
||||
when(response.readEntity(String.class)).thenReturn("col1,col2\nv1,v2");
|
||||
when(response.getHeaderString("Content-Disposition"))
|
||||
.thenReturn("attachment; filename=\"audit-logs-test.csv\"");
|
||||
|
||||
// Mock de l'ExternalContext et du flux de sortie
|
||||
jakarta.faces.context.ExternalContext externalContext = mock(jakarta.faces.context.ExternalContext.class);
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
try {
|
||||
when(externalContext.getResponseOutputStream()).thenReturn(baos);
|
||||
} catch (java.io.IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
when(facesContext.getExternalContext()).thenReturn(externalContext);
|
||||
|
||||
auditConsultationBean.setDateDebut(LocalDateTime.now().minusDays(7));
|
||||
auditConsultationBean.setDateFin(LocalDateTime.now());
|
||||
auditConsultationBean.exportToCSV();
|
||||
|
||||
verify(auditServiceClient).exportLogsToCSV(anyString(), anyString());
|
||||
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
|
||||
verify(externalContext).setResponseContentType(startsWith("text/csv"));
|
||||
verify(externalContext).setResponseHeader(eq("Content-Disposition"), contains("audit-logs-test.csv"));
|
||||
verify(facesContext).responseComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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.role.RoleDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
|
||||
@@ -36,6 +37,9 @@ class DashboardBeanTest {
|
||||
@Mock
|
||||
AuditServiceClient auditServiceClient;
|
||||
|
||||
@Mock
|
||||
UserMetricsServiceClient userMetricsServiceClient;
|
||||
|
||||
@Mock
|
||||
FacesContext facesContext;
|
||||
|
||||
@@ -72,6 +76,16 @@ class DashboardBeanTest {
|
||||
when(auditServiceClient.getSuccessCount(anyString(), anyString())).thenReturn(successDto);
|
||||
when(auditServiceClient.getFailureCount(anyString(), anyString())).thenReturn(failureDto);
|
||||
|
||||
// Mock Metrics Client
|
||||
dev.lions.user.manager.dto.common.UserSessionStatsDTO stats = dev.lions.user.manager.dto.common.UserSessionStatsDTO
|
||||
.builder()
|
||||
.realmName("master")
|
||||
.totalUsers(100L)
|
||||
.activeSessions(80L)
|
||||
.onlineUsers(70L)
|
||||
.build();
|
||||
when(userMetricsServiceClient.getUserSessionStats(anyString())).thenReturn(stats);
|
||||
|
||||
dashboardBean.init();
|
||||
|
||||
assertEquals(100L, dashboardBean.getTotalUsers());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.lions.user.manager.client.view;
|
||||
|
||||
import dev.lions.user.manager.client.service.RoleServiceClient;
|
||||
import dev.lions.user.manager.client.service.RealmServiceClient;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
@@ -34,6 +35,9 @@ class RoleGestionBeanTest {
|
||||
@Mock
|
||||
RoleServiceClient roleServiceClient;
|
||||
|
||||
@Mock
|
||||
RealmServiceClient realmServiceClient;
|
||||
|
||||
@Mock
|
||||
FacesContext facesContext;
|
||||
|
||||
@@ -189,7 +193,8 @@ class RoleGestionBeanTest {
|
||||
roleGestionBean.setClientName("");
|
||||
roleGestionBean.createClientRole();
|
||||
|
||||
verify(roleServiceClient, never()).createClientRole(any(), anyString(), anyString());
|
||||
verify(roleServiceClient, never()).createClientRole(anyString(),
|
||||
any(dev.lions.user.manager.dto.role.RoleDTO.class), anyString());
|
||||
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -24,6 +25,9 @@ class UserCreationBeanTest {
|
||||
@Mock
|
||||
UserServiceClient userServiceClient;
|
||||
|
||||
@Mock
|
||||
RealmServiceClient realmServiceClient;
|
||||
|
||||
@Mock
|
||||
FacesContext facesContext;
|
||||
|
||||
@@ -38,6 +42,7 @@ class UserCreationBeanTest {
|
||||
void setUp() {
|
||||
facesContextMock = mockStatic(FacesContext.class);
|
||||
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
|
||||
when(realmServiceClient.getAllRealms()).thenReturn(java.util.List.of("master"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
||||
Reference in New Issue
Block a user