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:
lionsdev
2026-02-18 03:28:07 +00:00
parent 366654a796
commit 9fce8f1d0a
47 changed files with 1429 additions and 759 deletions

19
BUILD-SYNC-API.md Normal file
View File

@@ -0,0 +1,19 @@
# Build pour éviter 404 sur le dashboard Sync
Le client utilise linterface **SyncResourceApi** du module `lions-user-manager-server-api`.
Si vous lancez uniquement `mvn quarkus:dev` dans le client, une ancienne version de lAPI (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 lAPI) :
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`).

View 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:

View 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:

View 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
View 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

View 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"]

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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}")

View File

@@ -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 {
}

View File

@@ -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)

View File

@@ -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);
totalRecords = auditLogs.size();
auditLogs = auditServiceClient.searchLogs(
null, null, null, null, null, null, 0, pageSize);
if (auditLogs == null) auditLogs = new ArrayList<>();
totalRecords = successCount + failureCount;
if (totalRecords == 0) {
totalRecords = auditLogs.size();
}
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement: " + e.getMessage());
addErrorMessage("Erreur lors du chargement: " + e.getMessage());
}
}
/**
* Charger les logs par realm
*/
public void loadLogsByRealm(String realmName) {
try {
// Méthode supprimée de l'API standardisée
LOGGER.warning("Recherche par realm non supportée dans l'API standardisée");
LOGGER.severe("Erreur lors du chargement initial des logs: " + e.getMessage());
auditLogs = new ArrayList<>();
totalRecords = 0;
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement: " + e.getMessage());
addErrorMessage("Erreur lors du chargement: " + e.getMessage());
}
}
/**
* Charger les statistiques
*/
public void loadStatistics() {
try {
String dateDebutStr = dateDebut != null ? dateDebut.format(DATE_FORMATTER) : null;
String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null;
String dateDebutStr = toIsoString(dateDebut);
String dateFinStr = toIsoString(dateFin);
actionStatistics = auditServiceClient.getActionStatistics(dateDebutStr, dateFinStr);
userActivityStatistics = auditServiceClient.getUserActivityStatistics(dateDebutStr, dateFinStr);
CountDTO failures = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr);
failureCount = failures != null ? failures.getCount() : 0;
dev.lions.user.manager.dto.common.CountDTO failures = auditServiceClient.getFailureCount(dateDebutStr,
dateFinStr);
failureCount = failures != null ? failures.getCount() : 0L;
CountDTO successes = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr);
successCount = successes != null ? successes.getCount() : 0;
dev.lions.user.manager.dto.common.CountDTO successes = auditServiceClient.getSuccessCount(dateDebutStr,
dateFinStr);
successCount = successes != null ? successes.getCount() : 0L;
totalRecords = successCount + failureCount;
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage());
}
}
/**
* Exporter les logs en CSV
*/
public void exportToCSV() {
try {
String dateDebutStr = dateDebut != null ? dateDebut.format(DATE_FORMATTER) : null;
String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null;
try (jakarta.ws.rs.core.Response response = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr)) {
// TODO: Implémenter le téléchargement du fichier CSV
// String csv = response.readEntity(String.class); // Non utilisé pour l'instant
addSuccessMessage("Export CSV généré avec succès");
}
String dateDebutStr = toIsoString(dateDebut);
String dateFinStr = toIsoString(dateFin);
actionStatistics = auditServiceClient.getActionStatistics(dateDebutStr, dateFinStr);
userActivityStatistics = auditServiceClient.getUserActivityStatistics(dateDebutStr, dateFinStr);
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'export: " + e.getMessage());
addErrorMessage("Erreur lors de l'export: " + e.getMessage());
LOGGER.warning("Statistiques détaillées non disponibles: " + e.getMessage());
}
}
public void exportToCSV() {
try {
String dateDebutStr = toIsoString(dateDebut);
String dateFinStr = toIsoString(dateFin);
try (jakarta.ws.rs.core.Response response = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr)) {
String csv = response.readEntity(String.class);
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
String disposition = response.getHeaderString("Content-Disposition");
String fileName = "audit-logs.csv";
if (disposition != null && disposition.contains("filename=")) {
fileName = disposition.substring(disposition.indexOf("filename=") + 9).replace("\"", "");
}
byte[] contentBytes = csv.getBytes(java.nio.charset.StandardCharsets.UTF_8);
externalContext.setResponseContentType("text/csv; charset=UTF-8");
externalContext.setResponseContentLength(contentBytes.length);
externalContext.setResponseHeader("Content-Disposition",
"attachment; filename=\"" + fileName + "\"");
try (OutputStream out = externalContext.getResponseOutputStream()) {
out.write(contentBytes);
out.flush();
}
facesContext.responseComplete();
}
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'export: " + e.getMessage());
addErrorMessage("Erreur lors de l'export : " + e.getMessage());
}
}
/**
* Réinitialiser les filtres
*/
public void resetFilters() {
acteurUsername = null;
dateDebut = null;
@@ -180,12 +181,10 @@ public class AuditConsultationBean implements Serializable {
ressourceType = null;
succes = null;
currentPage = 0;
auditLogs.clear();
loadStatistics();
loadRecentLogs();
}
/**
* Page précédente
*/
public void previousPage() {
if (currentPage > 0) {
currentPage--;
@@ -193,25 +192,32 @@ public class AuditConsultationBean implements Serializable {
}
}
/**
* Page suivante
*/
public void nextPage() {
currentPage++;
searchLogs();
}
/**
* Charger les realms disponibles
*/
private void loadRealms() {
// TODO: Implémenter la récupération des realms depuis Keycloak
// Le realm "master" est le realm d'administration qui permet de gérer tous les
// autres realms
availableRealms = List.of("master", "lions-user-manager", "btpxpress", "unionflow");
public long getSuccessRate() {
if (totalRecords == 0) return 0;
return (successCount * 100) / totalRecords;
}
private void loadRealms() {
try {
List<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));

View File

@@ -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");
}

View File

@@ -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();
}
}

View File

@@ -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";
}

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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>

View File

@@ -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,55 +243,111 @@
<!-- 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">
<div>
<strong>Type d'action:</strong>
<p>#{auditConsultationBean.selectedLog.typeAction}</p>
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>Acteur:</strong>
<p>#{auditConsultationBean.selectedLog.acteurUsername}</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>Ressource:</strong>
<p>#{auditConsultationBean.selectedLog.ressourceType} -
#{auditConsultationBean.selectedLog.ressourceId}</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>Date:</strong>
<p>#{auditConsultationBean.selectedLog.dateAction}</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>
<c:if test="#{not empty auditConsultationBean.selectedLog.details}">
<div>
<strong>Détails:</strong>
<p>#{auditConsultationBean.selectedLog.details}</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>
<c:if test="#{not empty auditConsultationBean.selectedLog.adresseIp}">
<div>
<strong>Adresse IP:</strong>
<p>#{auditConsultationBean.selectedLog.adresseIp}</p>
</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>
</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>
</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>
</h:panelGroup>
</h:form>
</p:dialog>
</ui:define>
</ui:composition>
</ui:composition>

View File

@@ -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>

View File

@@ -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"
template="/templates/main-template.xhtml">
<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="" />
<f:selectItems value="#{roleGestionBean.availableRealms}" />
<p:ajax event="change"
listener="#{roleGestionBean.loadRealmRoles}"
update=":formRealmRoles:realmRolesPanel :formClientRoles:clientRolesPanel" />
</p:selectOneMenu>
<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 :formClientRoles :formFilters:clientFilter :growlMessages" />
</p:selectOneMenu>
</div>
</div>
<p:outputLabel for="clientFilter" value="Client" />
<p:selectOneMenu id="clientFilter"
value="#{roleGestionBean.clientName}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}" />
<p:ajax event="change"
listener="#{roleGestionBean.loadClientRoles}"
update=":formClientRoles:clientRolesPanel" />
</p:selectOneMenu>
<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 un client..." itemValue="" />
<f:selectItems value="#{roleGestionBean.availableClients}" />
<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}"
styleClass="w-full">
<f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItems value="#{roleGestionBean.typeRoleOptions}" />
</p:selectOneMenu>
</p:panelGrid>
<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="#{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>
</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
</div>
</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
<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>
</div>
</div>
</ui:define>
</ui:decorate>
</div>
<!-- ================================================================
FORMULAIRE DE CRÉATION
================================================================ -->
<!-- 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-success p-button-sm">
<i class="pi pi-list mr-2"></i>Voir la liste
</h:link>
</div>
</div>
</h:panelGroup>
<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>
@@ -279,4 +269,4 @@
</p:dialog>
</ui:define>
</ui:composition>
</ui:composition>

View File

@@ -7,31 +7,31 @@
<ui:define name="title">Liste des Utilisateurs - Lions User Manager</ui:define>
<ui:define name="content">
<!-- 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>
</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">
<!-- ================================================================
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>
<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>
<!-- ================================================================
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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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">
<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 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>

View File

@@ -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 &amp;&amp; '#{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"
outcome="#{not empty cancelOutcome ? cancelOutcome : '/pages/user-manager/roles/list'}"
immediate="true" />
<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>

View File

@@ -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>

View File

@@ -63,12 +63,14 @@
</div>
</c:when>
<c:otherwise>
<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 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>
<div class="text-500 text-xs mb-2">#{noDataLabel}</div>
<c:if test="#{empty subtitle}">
<div class="text-500 text-xs mb-2">#{noDataLabel}</div>
</c:if>
</c:otherwise>
</c:choose>
</c:otherwise>

View File

@@ -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}">

View File

@@ -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" />

View File

@@ -36,51 +36,51 @@
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">
<c:if test="#{showView}">
<p:menuitem value="Voir le profil" icon="pi pi-eye"
outcome="#{not empty viewOutcome ? viewOutcome : '/pages/user-manager/users/profile'}">
<f:param name="userId" value="#{user.id}" />
</p:menuitem>
</c:if>
<c:if test="#{showEdit}">
<p:menuitem value="Modifier" icon="pi pi-pencil"
outcome="#{not empty editOutcome ? editOutcome : '/pages/user-manager/users/edit'}">
<f:param name="userId" value="#{user.id}" />
</p:menuitem>
</c:if>
<c:if test="#{showResetPassword}">
<p:menuitem value="Réinitialiser MDP" icon="pi pi-key"
onclick="PF('resetPasswordDialog_#{user.id}').show()" />
</c:if>
<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/view'}">
<f:param name="userId" value="#{user.id}" />
</p:menuitem>
</c:if>
<c:if test="#{showEdit}">
<p:menuitem value="Modifier" icon="pi pi-pencil"
outcome="#{not empty editOutcome ? editOutcome : '/pages/user-manager/users/edit'}">
<f:param name="userId" value="#{user.id}" />
</p:menuitem>
</c:if>
<c:if test="#{showResetPassword}">
<p:menuitem value="Réinitialiser MDP" icon="pi pi-key"
onclick="PF('resetPasswordDialog_#{user.id}').show()" />
</c:if>
<p:separator />
<c:if test="#{showActivate and not user.enabled}">
<p:menuitem value="Activer" icon="pi pi-check" styleClass="text-green-600"
action="#{actActivate}" update="#{update}" />
</c:if>
<c:if test="#{showDeactivate and user.enabled}">
<p:menuitem value="Désactiver" icon="pi pi-times" styleClass="text-orange-600"
action="#{actDeactivate}" update="#{update}" />
</c:if>
<c:if test="#{showLogoutSessions}">
<p:menuitem value="Déconnecter sessions" icon="pi pi-sign-out" styleClass="text-blue-600"
action="#{actLogout}" update="#{update}" />
</c:if>
<c:if test="#{showDelete}">
<p:separator />
<c:if test="#{showActivate and not user.enabled}">
<p:menuitem value="Activer" icon="pi pi-check" styleClass="text-green-600"
action="#{actActivate}" update="#{update}" />
</c:if>
<c:if test="#{showDeactivate and user.enabled}">
<p:menuitem value="Désactiver" icon="pi pi-times" styleClass="text-orange-600"
action="#{actDeactivate}" update="#{update}" />
</c:if>
<c:if test="#{showLogoutSessions}">
<p:menuitem value="Déconnecter sessions" icon="pi pi-sign-out" styleClass="text-blue-600"
action="#{actLogout}" update="#{update}" />
</c:if>
<c:if test="#{showDelete}">
<p:separator />
<p:menuitem value="Supprimer" icon="pi pi-trash" styleClass="text-red-600" action="#{actDelete}"
update="#{update}">
<p:confirm header="Confirmation" message="Supprimer l'utilisateur #{user.username} ?"
icon="pi pi-exclamation-triangle" />
</p:menuitem>
</c:if>
</p:menu>
</p:commandButton>
<p:menuitem value="Supprimer" icon="pi pi-trash" styleClass="text-red-600" action="#{actDelete}"
update="#{update}">
<p:confirm header="Confirmation" message="Supprimer l'utilisateur #{user.username} ?"
icon="pi pi-exclamation-triangle" />
</p:menuitem>
</c:if>
</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" />

View File

@@ -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

View File

@@ -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}
# ============================================

View File

@@ -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)
# ============================================

View File

@@ -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

View File

@@ -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());

View File

@@ -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));
}

View File

@@ -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