Compare commits

..

9 Commits

Author SHA1 Message Date
dahoud
d19ee7cd25 refactoring 2026-02-18 23:15:34 +00:00
root
e7df337276 refactoring 2026-02-18 19:17:52 +01:00
dahoud
21927c5b4f refactoring 2026-02-18 16:17:53 +00:00
dahoud
88f6dfe499 refactoring 2026-02-18 15:50:15 +00:00
lionsdev
9adefdb8b3 refactoring 2026-02-18 14:55:46 +00:00
lionsdev
9fce8f1d0a feat(client): corrections UI/UX pages dashboard, audit, roles, users - fix REST clients, KPI, navigation et formulaires
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 03:28:07 +00:00
lionsdev
366654a796 refactoring 2026-02-15 14:41:13 +00:00
lionsdev
c5cbe61002 feat: Optimisations UX/UI et amélioration import/export CSV
Optimisations majeures de l'interface utilisateur et amélioration du système d'import/export CSV avec rapport d'erreurs détaillé.

## Optimisations UX/UI
- Suppression des blocs Actions Rapides redondants dans les pages list/view
- Consolidation des actions dans les en-têtes de page
- Conversion des filtres en panneau collapsible avec badge Filtres actifs
- Suppression du sous-menu Attribution Rôles (redondant avec /users/edit)
- Amélioration de la navigation et de l'ergonomie générale
- Correction des attributs iconLeft non supportés par fr:fieldInput

## Import/Export CSV
- Ajout de ImportResultDTO avec rapport détaillé des erreurs
- Création de CsvValidationHelper pour validation robuste des données
- Amélioration des messages d'erreur avec numéros de ligne
- Support de colonnes flexibles (username,prenom,nom,email)
- Validation stricte des formats email

## Corrections techniques
- Fix DashboardBeanTest: getRecentActions() → getActionsLast24h()
- Fix UserServiceImplTest: retour ImportResultDTO au lieu de int
- Amélioration de la gestion d'erreurs dans AuditServiceImpl
- Migration Flyway V1.0.0 pour la table audit_logs

## Infrastructure
- Mise à jour .gitignore professionnel (exclusion docs de session)
- Configuration production sécurisée (variables d'environnement)
- Pas de secrets hardcodés dans les fichiers de configuration

Testé et validé en environnement de développement.
2026-01-03 13:53:35 +00:00
lionsdev
564d29a9d2 Migration complète vers PrimeFaces Freya - Corrections des incompatibilités et intégration de primefaces-freya-extension 2025-12-27 00:18:31 +00:00
100 changed files with 14609 additions and 3472 deletions

130
.gitignore vendored
View File

@@ -1,130 +0,0 @@
# ============================================================================
# Lions User Manager - Client Quarkus PrimeFaces Freya - .gitignore
# ============================================================================
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Build artifacts
*.jar
*.war
*.ear
*.class
*.idx
# Eclipse
.project
.classpath
.settings/
.metadata/
bin/
# IntelliJ IDEA
.idea/
*.iml
*.iws
*.ipr
out/
# NetBeans
nbproject/
nbbuild/
nbdist/
.nb-gradle/
# VS Code
.vscode/
*.code-workspace
# Mac
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Logs
logs/
*.log
*.log.*
hs_err_pid*.log
# Quarkus
.quarkus/
quarkus-app/
quarkus-run.jar
quarkus-*.dat
# Temporary files
*.tmp
*.bak
*.swp
*~
*.orig
# Test files and reports
test_output*.txt
surefire-reports/
failsafe-reports/
*.dump
*.dumpstream
# Test coverage
.jacoco/
jacoco.exec
coverage/
target/site/jacoco/
# Application specific
application-local.properties
application-*.local.properties
# Configuration files with sensitive data
*.local.json
# Token and authentication files
token.json
token.txt
*.token
# Generated sources
generated-sources/
generated-test-sources/
# Maven status
maven-status/
# Node modules (si utilisé pour le build frontend)
node_modules/
# Build metrics
build-metrics.json
# Quarkus Dev Services
.devservices/
# IDE specific
*.sublime-project
*.sublime-workspace
# OS specific
.DS_Store?
._*
.Spotlight-V100
.Trashes
# Environment files
.env
.env.local
.env.*.local

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`).

93
Dockerfile.prod Normal file
View File

@@ -0,0 +1,93 @@
####
# Dockerfile de production pour Lions User Manager Client (Frontend)
# Multi-stage build optimisé avec sécurité renforcée
# Basé sur la structure de btpxpress-client
####
## Stage 1 : Build avec Maven
FROM maven:3.9.6-eclipse-temurin-17 AS builder
WORKDIR /app
# Copier pom.xml et télécharger les dépendances (cache Docker)
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copier le code source
COPY src ./src
# Build de l'application avec profil production (fast-jar par défaut)
RUN mvn clean package -DskipTests -B -Dquarkus.profile=prod
## Stage 2 : Image de production optimisée et sécurisée
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
ENV LANGUAGE='fr_FR:fr'
# Variables d'environnement de production
# Ces valeurs peuvent être surchargées via docker-compose ou Kubernetes
ENV QUARKUS_PROFILE=prod
ENV QUARKUS_HTTP_PORT=8080
ENV QUARKUS_HTTP_HOST=0.0.0.0
# Proxy forwarding (SSL termination par l'ingress nginx K8s)
ENV QUARKUS_HTTP_PROXY_PROXY_ADDRESS_FORWARDING=true
ENV QUARKUS_HTTP_PROXY_ALLOW_X_FORWARDED=true
# Configuration Keycloak/OIDC (production)
ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/lions-user-manager
ENV QUARKUS_OIDC_CLIENT_ID=lions-user-manager-client
ENV KEYCLOAK_CLIENT_SECRET=oGCivOdgbNHroNsHS1MRBZJXX8VpRGk3
ENV QUARKUS_OIDC_CREDENTIALS_SECRET=oGCivOdgbNHroNsHS1MRBZJXX8VpRGk3
ENV QUARKUS_OIDC_ENABLED=true
ENV QUARKUS_OIDC_TLS_VERIFICATION=required
ENV QUARKUS_OIDC_AUTHENTICATION_COOKIE_SAME_SITE=lax
ENV OIDC_ENCRYPTION_SECRET=gbztZB3CYpou0vFL2LqOWJQdXnvwVQkhjrHpsZHOJPI=
# Configuration API Backend
ENV LIONS_USER_MANAGER_BACKEND_URL=https://api.lions.dev/lions-user-manager
# Configuration CORS
ENV QUARKUS_HTTP_CORS_ORIGINS=https://users.lions.dev,https://admin.lions.dev
ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true
# Installer curl pour les health checks
USER root
RUN microdnf install -y curl && \
microdnf clean all && \
rm -rf /var/cache/yum
# Créer les répertoires et permissions pour utilisateur non-root
RUN mkdir -p /deployments /app/logs && \
chown -R 185:185 /deployments /app/logs
# Passer à l'utilisateur non-root pour la sécurité
USER 185
# Copier l'application depuis le builder (format fast-jar Quarkus)
COPY --from=builder --chown=185 /app/target/quarkus-app/ /deployments/
# Exposer le port
EXPOSE 8080
# Variables JVM optimisées pour production avec sécurité
ENV JAVA_OPTS="-Xmx768m -Xms256m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-XX:+ParallelRefProcEnabled \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/logs/heapdump.hprof \
-Djava.security.egd=file:/dev/./urandom \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-Dquarkus.profile=${QUARKUS_PROFILE}"
# Health check avec endpoints Quarkus
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1
# Point d'entrée avec profil production (format fast-jar)
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"]

141
README.md Normal file
View File

@@ -0,0 +1,141 @@
# lions-user-manager-client-quarkus-primefaces-freya
> Interface web d'administration — Quarkus + JSF + PrimeFaces Freya
## Dépôt Git
`https://git.lions.dev/lionsdev/lions-user-manager-client-quarkus-primefaces-freya`
---
## Responsabilités
- Interface d'administration des utilisateurs et rôles Keycloak
- Authentification SSO via OIDC (Keycloak)
- Communication avec le backend via MicroProfile REST Client
- Dashboard, gestion CRUD, audit, synchronisation, assignation de realms
---
## Pages
| Page | Bean | Description |
|------|------|-------------|
| `/dashboard.xhtml` | `DashboardBean` | Vue d'ensemble et statistiques |
| `/users/list.xhtml` | `UserListBean` | Liste paginée avec filtres et export CSV |
| `/users/creation.xhtml` | `UserCreationBean` | Formulaire de création |
| `/users/profil.xhtml` | `UserProfilBean` | Édition profil utilisateur |
| `/roles/gestion.xhtml` | `RoleGestionBean` | Gestion des rôles par realm |
| `/audit/consultation.xhtml` | `AuditConsultationBean` | Consultation des logs d'audit |
| `/sync/dashboard.xhtml` | `SyncDashboardBean` | État et pilotage des synchronisations |
| `/realms/assignment.xhtml` | `RealmAssignmentBean` | Assignation utilisateurs/realms |
---
## Stack
| Composant | Technologie |
|-----------|-------------|
| Framework | Quarkus 3.17.8 + Undertow (Servlet) |
| UI | PrimeFaces 14.0.5 (Jakarta) |
| Thème | Freya Enterprise |
| Auth | `quarkus-oidc` (Keycloak) + PKCE |
| REST Client | MicroProfile REST Client (`quarkus-rest-client-jackson`) |
| Token Propagation | `quarkus-rest-client-oidc-token-propagation` |
---
## Développement local
### Prérequis
- Java 17+, Maven 3.9+
- Keycloak sur `localhost:8180` (realm `lions-user-manager` configuré)
- Backend `server-impl` démarré sur `localhost:8081`
### Démarrage
```bash
mvn quarkus:dev
```
Application disponible sur : `http://localhost:8082`
### Configuration dev
Fichier : `src/main/resources/application-dev.properties`
```properties
quarkus.http.port=8082
quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager
lions.user.manager.backend.url=http://localhost:8081
```
---
## Configuration production
Fichier : `src/main/resources/application-prod.properties`
| Variable | Description |
|----------|-------------|
| `KEYCLOAK_AUTH_SERVER_URL` | URL du realm Keycloak |
| `KEYCLOAK_CLIENT_ID` | Client OIDC (défaut : `lions-user-manager-client`) |
| `OIDC_ENCRYPTION_SECRET` | Secret de chiffrement des tokens (32 caractères min) |
| `LIONS_USER_MANAGER_BACKEND_URL` | URL de l'API backend |
---
## Build
```bash
# Build standard (développement)
mvn clean package -DskipTests
# Build production
mvn clean package -P prod -DskipTests
```
---
## Déploiement (lionsctl)
```bash
lionsctl pipeline \
-u https://git.lions.dev/lionsdev/lions-user-manager-client-quarkus-primefaces-freya \
-b main -j 17 -e production -c k1 -p prod
```
**Pipeline** : clone → `mvn package -P prod``docker build -f Dockerfile.prod` → push `registry.lions.dev``kubectl apply` → health check
**URL prod** : `https://users.lions.dev`
---
## Structure
```
src/main/
├── java/dev/lions/user/manager/client/
│ ├── bean/ # Beans JSF (@Named, @ViewScoped / @SessionScoped)
│ └── client/ # REST Clients (UserRestClient, RoleRestClient, ...)
└── resources/
├── META-INF/resources/
│ ├── templates/
│ │ └── components/ # Composants PrimeFaces réutilisables
│ ├── dashboard.xhtml
│ ├── users/
│ ├── roles/
│ ├── audit/
│ ├── sync/
│ └── realms/
├── application.properties
├── application-dev.properties
└── application-prod.properties
```
---
## Licence
Propriétaire — Lions Dev © 2025

32
pom.xml
View File

@@ -16,6 +16,15 @@
<name>Lions User Manager - Client (Quarkus + PrimeFaces Freya)</name>
<description>Client web: UI PrimeFaces Freya, Beans JSF, REST Clients</description>
<repositories>
<repository>
<id>gitea-lionsdev</id>
<url>https://git.lions.dev/api/packages/lionsdev/maven</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<dependencies>
<!-- Module API pour DTOs -->
<dependency>
@@ -40,6 +49,11 @@
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-oidc-token-propagation</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
@@ -96,6 +110,24 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

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

@@ -0,0 +1,55 @@
package dev.lions.user.manager.client.api;
import dev.lions.user.manager.client.filter.AuthHeaderFactory;
import dev.lions.user.manager.dto.audit.AuditLogDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.util.List;
import java.util.Map;
@RegisterRestClient(configKey = "user-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/audit")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface AuditRestClient {
@GET
List<AuditLogDTO> searchLogs(
@QueryParam("acteur") String acteur,
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin,
@QueryParam("typeAction") TypeActionAudit typeAction,
@QueryParam("ressourceType") String ressourceType,
@QueryParam("succes") Boolean succes,
@QueryParam("page") int page,
@QueryParam("pageSize") int pageSize);
@GET
@Path("/stats/actions")
Map<TypeActionAudit, Long> getActionStatistics(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin);
@GET
@Path("/stats/activity")
Map<String, Long> getUserActivityStatistics(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin);
@GET
@Path("/stats/failures")
long getFailureCount(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin);
@GET
@Path("/stats/successes")
long getSuccessCount(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin);
}

View File

@@ -0,0 +1,26 @@
package dev.lions.user.manager.client.api;
import dev.lions.user.manager.client.filter.AuthHeaderFactory;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.util.Map;
@RegisterRestClient(configKey = "user-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/health")
@Produces(MediaType.APPLICATION_JSON)
public interface HealthRestClient {
@GET
@Path("/keycloak")
Map<String, Object> getKeycloakHealth();
@GET
@Path("/status")
Map<String, Object> getServiceStatus();
}

View File

@@ -0,0 +1,60 @@
package dev.lions.user.manager.client.api;
import dev.lions.user.manager.client.filter.AuthHeaderFactory;
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
import dev.lions.user.manager.dto.role.RoleDTO;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.util.List;
@RegisterRestClient(configKey = "user-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/roles")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface RoleRestClient {
@GET
@Path("/realm")
List<RoleDTO> getAllRealmRoles(@QueryParam("realm") String realmName);
@POST
@Path("/realm")
RoleDTO createRealmRole(@QueryParam("realm") String realmName, RoleDTO role);
@GET
@Path("/realm/{roleName}")
RoleDTO getRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName);
@PUT
@Path("/realm/{roleName}")
RoleDTO updateRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName,
RoleDTO role);
@DELETE
@Path("/realm/{roleName}")
void deleteRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName);
@POST
@Path("/users/{userId}/realm-roles")
void assignRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
RoleAssignmentRequest request);
@DELETE
@Path("/users/{userId}/realm-roles")
void revokeRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
RoleAssignmentRequest request);
@GET
@Path("/users/{userId}/realm-roles")
List<RoleDTO> getUserRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName);
// Inner class for role assignment request
class RoleAssignmentRequest {
public List<String> roleNames;
}
}

View File

@@ -0,0 +1,91 @@
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.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.util.List;
@RegisterRestClient(configKey = "user-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface UserRestClient {
@GET
UserSearchResultDTO searchUsers(
@QueryParam("realm") String realmName,
@QueryParam("search") String searchTerm,
@QueryParam("username") String username,
@QueryParam("email") String email,
@QueryParam("prenom") String prenom,
@QueryParam("nom") String nom,
@QueryParam("enabled") Boolean enabled,
@QueryParam("page") int page,
@QueryParam("pageSize") int pageSize);
@GET
@Path("/{id}")
UserDTO getUserById(@PathParam("id") String id, @QueryParam("realm") String realmName);
@POST
Response createUser(@QueryParam("realm") String realmName, UserDTO user);
@PUT
@Path("/{id}")
UserDTO updateUser(@PathParam("id") String id, @QueryParam("realm") String realmName, UserDTO user);
@DELETE
@Path("/{id}")
void deleteUser(@PathParam("id") String id, @QueryParam("realm") String realmName,
@QueryParam("hard") boolean hardDelete);
// Correction : @POST (était @PUT — mismatch avec le serveur → 405)
@POST
@Path("/{id}/activate")
void activateUser(@PathParam("id") String id, @QueryParam("realm") String realmName);
// 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);
@POST
@Path("/{id}/reset-password")
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-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 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

@@ -0,0 +1,59 @@
package dev.lions.user.manager.client.filter;
import io.quarkus.oidc.AccessTokenCredential;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import java.util.logging.Logger;
/**
* Factory to automatically add the OIDC access token (Bearer)
* to REST Client request headers.
*/
@RequestScoped
public class AuthHeaderFactory implements ClientHeadersFactory {
private static final Logger LOGGER = Logger.getLogger(AuthHeaderFactory.class.getName());
@Inject
AccessTokenCredential accessTokenCredential;
@Override
public MultivaluedMap<String, String> update(
MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
// 1. Log incoming and outgoing headers for debugging
if (incomingHeaders != null) {
LOGGER.fine("Incoming Headers: " + incomingHeaders.keySet());
}
if (clientOutgoingHeaders != null) {
LOGGER.fine("Client Outgoing Headers: " + clientOutgoingHeaders.keySet());
}
try {
if (accessTokenCredential != null) {
// 2. Check if the token is available
String accessToken = accessTokenCredential.getToken();
if (accessToken != null && !accessToken.isEmpty()) {
result.add("Authorization", "Bearer " + accessToken);
LOGGER.info("Access token added to Authorization header. Token length: " + accessToken.length());
} else {
LOGGER.warning("Access token is empty or null in AccessTokenCredential.");
}
} else {
LOGGER.warning("AccessTokenCredential is unavailable - user might not be authenticated.");
}
} catch (Exception e) {
LOGGER.severe("Error adding Bearer token: " + e.getMessage());
e.printStackTrace();
}
return result;
}
}

View File

@@ -1,105 +1,26 @@
package dev.lions.user.manager.client.service;
import dev.lions.user.manager.dto.audit.AuditLogDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import 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;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* REST Client pour le service d'audit
* 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")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface AuditServiceClient {
@RegisterClientHeaders(AuthHeaderFactory.class)
public interface AuditServiceClient extends AuditResourceApi {
@POST
@Path("/search")
List<AuditLogDTO> searchLogs(
@QueryParam("acteur") String acteurUsername,
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin,
@QueryParam("typeAction") TypeActionAudit typeAction,
@QueryParam("ressourceType") String ressourceType,
@QueryParam("succes") Boolean succes,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("pageSize") @DefaultValue("50") int pageSize
);
// Méthodes héritées de AuditResourceApi
// Note: getLogsByActeur (FR) a été remplacé par getLogsByActor (EN) dans l'API
// commune.
@GET
@Path("/acteur/{acteurUsername}")
List<AuditLogDTO> getLogsByActeur(
@PathParam("acteurUsername") String acteurUsername,
@QueryParam("limit") @DefaultValue("100") int limit
);
@GET
@Path("/realm/{realmName}")
List<AuditLogDTO> getLogsByRealm(
@PathParam("realmName") String realmName,
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("pageSize") @DefaultValue("50") int pageSize
);
@GET
@Path("/ressource/{ressourceType}/{ressourceId}")
List<AuditLogDTO> getLogsByRessource(
@PathParam("ressourceType") String ressourceType,
@PathParam("ressourceId") String ressourceId,
@QueryParam("limit") @DefaultValue("100") int limit
);
@GET
@Path("/action/{typeAction}")
List<AuditLogDTO> getLogsByAction(
@PathParam("typeAction") TypeActionAudit typeAction,
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin,
@QueryParam("limit") @DefaultValue("100") int limit
);
@GET
@Path("/statistics/actions")
Map<TypeActionAudit, Long> getActionStatistics(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin
);
@GET
@Path("/statistics/users")
Map<String, Long> getUserActivityStatistics(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin
);
@GET
@Path("/statistics/failures")
Long getFailureCount(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin
);
@GET
@Path("/statistics/successes")
Long getSuccessCount(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin
);
@GET
@Path("/export/csv")
@Produces("text/csv")
String exportLogsToCSV(
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin
);
// Si des méthodes spécifiques au client (non présentes sur le serveur)
// existaient, elles devraient être ici.
// L'ancienne méthode getLogsByRealm n'existait pas sur le serveur, donc
// supprimée.
}

View File

@@ -0,0 +1,21 @@
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;
/**
* 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 {
// Méthodes héritées de RealmAssignmentResourceApi
// Les classes internes CheckResponse et AuthorizedRealmsResponse ont été
// remplacées par des DTOs dans server-api.
}

View File

@@ -0,0 +1,21 @@
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;
/**
* 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)
public interface RealmServiceClient extends RealmResourceApi {
// Méthode getAllRealms héritée de RealmResourceApi
}

View File

@@ -0,0 +1,112 @@
package dev.lions.user.manager.client.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
/**
* Mapper d'exceptions pour les clients REST
* Convertit les réponses HTTP d'erreur en exceptions appropriées
*/
public class RestClientExceptionMapper implements ResponseExceptionMapper<RuntimeException> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public RuntimeException toThrowable(Response response) {
int status = response.getStatus();
String reasonPhrase = response.getStatusInfo().getReasonPhrase();
// Lire le corps de la réponse pour plus de détails
String errorMessage = reasonPhrase;
try {
if (response.hasEntity()) {
String body = response.readEntity(String.class);
if (body != null && !body.isEmpty()) {
// Essayer de parser le JSON pour extraire le message
try {
JsonNode jsonNode = objectMapper.readTree(body);
if (jsonNode.has("message")) {
errorMessage = jsonNode.get("message").asText();
} else {
errorMessage = body;
}
} catch (Exception e) {
// Si ce n'est pas du JSON, utiliser le body tel quel
errorMessage = body;
}
}
}
} catch (Exception e) {
// Ignorer les erreurs de lecture du body
}
return switch (status) {
case 400 -> new BadRequestException("Requête invalide: " + errorMessage);
case 401 -> new UnauthorizedException("Non autorisé: " + errorMessage);
case 403 -> new ForbiddenException("Accès interdit: " + errorMessage);
case 404 -> new NotFoundException(errorMessage);
case 409 -> new ConflictException("Conflit: " + errorMessage);
case 422 -> new UnprocessableEntityException("Données non valides: " + errorMessage);
case 500 -> new InternalServerErrorException("Erreur serveur interne: " + errorMessage);
case 502 -> new BadGatewayException("Erreur de passerelle: " + errorMessage);
case 503 -> new ServiceUnavailableException("Service indisponible: " + errorMessage);
case 504 -> new GatewayTimeoutException("Timeout de passerelle: " + errorMessage);
default -> new UnknownHttpStatusException("Erreur HTTP " + status + ": " + errorMessage);
};
}
@Override
public boolean handles(int status, MultivaluedMap<String, Object> headers) {
// Gérer tous les codes d'erreur HTTP (>= 400)
return status >= 400;
}
// Classes d'exception personnalisées
public static class BadRequestException extends RuntimeException {
public BadRequestException(String message) { super(message); }
}
public static class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) { super(message); }
}
public static class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) { super(message); }
}
public static class NotFoundException extends RuntimeException {
public NotFoundException(String message) { super(message); }
}
public static class ConflictException extends RuntimeException {
public ConflictException(String message) { super(message); }
}
public static class UnprocessableEntityException extends RuntimeException {
public UnprocessableEntityException(String message) { super(message); }
}
public static class InternalServerErrorException extends RuntimeException {
public InternalServerErrorException(String message) { super(message); }
}
public static class BadGatewayException extends RuntimeException {
public BadGatewayException(String message) { super(message); }
}
public static class ServiceUnavailableException extends RuntimeException {
public ServiceUnavailableException(String message) { super(message); }
}
public static class GatewayTimeoutException extends RuntimeException {
public GatewayTimeoutException(String message) { super(message); }
}
public static class UnknownHttpStatusException extends RuntimeException {
public UnknownHttpStatusException(String message) { super(message); }
}
}

View File

@@ -1,135 +1,22 @@
package dev.lions.user.manager.client.service;
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
import dev.lions.user.manager.dto.role.RoleDTO;
import dev.lions.user.manager.enums.role.TypeRole;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import 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;
import java.util.List;
/**
* REST Client pour le service de gestion des rôles
* 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")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface RoleServiceClient {
@RegisterClientHeaders(AuthHeaderFactory.class)
public interface RoleServiceClient extends RoleResourceApi {
// ==================== Realm Roles ====================
@POST
@Path("/realm")
RoleDTO createRealmRole(
RoleDTO role,
@QueryParam("realm") String realmName
);
@GET
@Path("/realm/{roleName}")
RoleDTO getRealmRoleByName(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName
);
@GET
@Path("/realm")
List<RoleDTO> getAllRealmRoles(
@QueryParam("realm") String realmName
);
@PUT
@Path("/realm/{roleName}")
RoleDTO updateRealmRole(
@PathParam("roleName") String roleName,
RoleDTO role,
@QueryParam("realm") String realmName
);
@DELETE
@Path("/realm/{roleName}")
void deleteRealmRole(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName
);
// ==================== Client Roles ====================
@POST
@Path("/client")
RoleDTO createClientRole(
RoleDTO role,
@QueryParam("realm") String realmName,
@QueryParam("clientName") String clientName
);
@GET
@Path("/client")
List<RoleDTO> getAllClientRoles(
@QueryParam("realm") String realmName,
@QueryParam("clientName") String clientName
);
@GET
@Path("/client/{roleName}")
RoleDTO getClientRoleByName(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName,
@QueryParam("clientName") String clientName
);
@DELETE
@Path("/client/{roleName}")
void deleteClientRole(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName,
@QueryParam("clientName") String clientName
);
// ==================== Role Assignment ====================
@POST
@Path("/assign")
void assignRoleToUser(RoleAssignmentDTO assignment);
@POST
@Path("/revoke")
void revokeRoleFromUser(RoleAssignmentDTO assignment);
@GET
@Path("/user/{userId}")
List<RoleDTO> getUserRoles(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
// ==================== Composite Roles ====================
@GET
@Path("/composite/{roleName}")
List<RoleDTO> getCompositeRoles(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName,
@QueryParam("typeRole") TypeRole typeRole,
@QueryParam("clientName") String clientName
);
@POST
@Path("/composite/{roleName}/add")
void addCompositeRole(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName,
@QueryParam("compositeRoleName") String compositeRoleName
);
@DELETE
@Path("/composite/{roleName}/remove")
void removeCompositeRole(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName,
@QueryParam("compositeRoleName") String compositeRoleName
);
// Méthodes héritées de RoleResourceApi
// Note: Certaines méthodes de l'ancien client ont été restructurées (ex:
// assignRoleToUser -> assignRealmRoles/assignClientRoles)
// pour correspondre à l'implémentation serveur existante.
}

View File

@@ -1,53 +1,34 @@
package dev.lions.user.manager.client.service;
import dev.lions.user.manager.dto.sync.HealthStatusDTO;
import dev.lions.user.manager.dto.sync.SyncResultDTO;
import dev.lions.user.manager.api.SyncResourceApi;
import dev.lions.user.manager.client.filter.AuthHeaderFactory;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* REST Client pour le service de synchronisation
* 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")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface SyncServiceClient {
@RegisterClientHeaders(AuthHeaderFactory.class)
public interface SyncServiceClient extends SyncResourceApi {
@GET
@Path("/health")
HealthStatusDTO checkHealth(@QueryParam("realm") String realmName);
@GET
@Path("/health/keycloak")
HealthStatusDTO checkKeycloakHealth();
@POST
@Path("/users")
SyncResultDTO syncUsers(@QueryParam("realm") String realmName);
@POST
@Path("/roles")
SyncResultDTO syncRoles(
@QueryParam("realm") String realmName,
@QueryParam("clientName") String clientName
);
// checkKeycloakHealth() hérité de SyncResourceApi → GET /api/sync/health/keycloak
@GET
@Path("/exists/user/{username}")
Boolean userExists(
@PathParam("username") String username,
@QueryParam("realm") String realmName
);
@PathParam("username") String username,
@QueryParam("realm") String realmName);
@GET
@Path("/exists/role/{roleName}")
Boolean roleExists(
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName,
@QueryParam("typeRole") String typeRole,
@QueryParam("clientName") String clientName
);
@PathParam("roleName") String roleName,
@QueryParam("realm") String realmName,
@QueryParam("typeRole") String typeRole,
@QueryParam("clientName") String clientName);
}

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

@@ -1,140 +1,21 @@
package dev.lions.user.manager.client.service;
import dev.lions.user.manager.dto.user.UserDTO;
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import 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;
import java.util.List;
/**
* REST Client pour le service de gestion des utilisateurs
* Interface pour communiquer avec l'API backend
* Étend maintenant l'interface API commune définie dans server-api.
*/
@Path("/api/users")
@RegisterRestClient(configKey = "lions-user-manager-api")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface UserServiceClient {
@RegisterClientHeaders(AuthHeaderFactory.class)
@RegisterProvider(RestClientExceptionMapper.class)
public interface UserServiceClient extends UserResourceApi {
/**
* Rechercher des utilisateurs selon des critères
*/
@POST
@Path("/search")
UserSearchResultDTO searchUsers(UserSearchCriteriaDTO criteria);
/**
* Récupérer un utilisateur par ID
*/
@GET
@Path("/{userId}")
UserDTO getUserById(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
/**
* Lister tous les utilisateurs (paginé)
*/
@GET
UserSearchResultDTO getAllUsers(
@QueryParam("realm") String realmName,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("pageSize") @DefaultValue("20") int pageSize
);
/**
* Créer un nouvel utilisateur
*/
@POST
UserDTO createUser(
UserDTO user,
@QueryParam("realm") String realmName
);
/**
* Mettre à jour un utilisateur
*/
@PUT
@Path("/{userId}")
UserDTO updateUser(
@PathParam("userId") String userId,
UserDTO user,
@QueryParam("realm") String realmName
);
/**
* Supprimer un utilisateur
*/
@DELETE
@Path("/{userId}")
void deleteUser(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
/**
* Activer un utilisateur
*/
@POST
@Path("/{userId}/activate")
void activateUser(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
/**
* Désactiver un utilisateur
*/
@POST
@Path("/{userId}/deactivate")
void deactivateUser(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
/**
* Réinitialiser le mot de passe
*/
@POST
@Path("/{userId}/reset-password")
void resetPassword(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName,
@QueryParam("newPassword") String newPassword
);
/**
* Envoyer un email de vérification
*/
@POST
@Path("/{userId}/send-verification-email")
void sendVerificationEmail(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
/**
* Déconnecter toutes les sessions d'un utilisateur
*/
@POST
@Path("/{userId}/logout-sessions")
void logoutAllSessions(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
/**
* Récupérer les sessions actives d'un utilisateur
*/
@GET
@Path("/{userId}/sessions")
List<String> getActiveSessions(
@PathParam("userId") String userId,
@QueryParam("realm") String realmName
);
// Méthodes héritées de UserResourceApi
}

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,146 +33,146 @@ 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,
dateDebutStr,
dateFinStr,
selectedTypeAction,
ressourceType,
succes,
currentPage,
pageSize
);
acteurUsername,
dateDebutStr,
dateFinStr,
selectedTypeAction,
ressourceType,
succes,
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.getLogsByActeur(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());
LOGGER.severe("Erreur lors du chargement initial des logs: " + e.getMessage());
auditLogs = new ArrayList<>();
}
}
/**
* Charger les logs par realm
*/
public void loadLogsByRealm(String realmName) {
try {
String dateDebutStr = dateDebut != null ? dateDebut.format(DATE_FORMATTER) : null;
String dateFinStr = dateFin != null ? dateFin.format(DATE_FORMATTER) : null;
auditLogs = auditServiceClient.getLogsByRealm(
realmName,
dateDebutStr,
dateFinStr,
currentPage,
pageSize
);
totalRecords = auditLogs.size();
} 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);
failureCount = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr);
successCount = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr);
CountDTO failures = auditServiceClient.getFailureCount(dateDebutStr, dateFinStr);
failureCount = failures != null ? failures.getCount() : 0;
CountDTO successes = auditServiceClient.getSuccessCount(dateDebutStr, dateFinStr);
successCount = successes != null ? successes.getCount() : 0;
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;
String csv = auditServiceClient.exportLogsToCSV(dateDebutStr, dateFinStr);
// TODO: Implémenter le téléchargement du fichier CSV
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;
@@ -181,26 +181,50 @@ public class AuditConsultationBean implements Serializable {
ressourceType = null;
succes = null;
currentPage = 0;
auditLogs.clear();
loadStatistics();
loadRecentLogs();
}
public void previousPage() {
if (currentPage > 0) {
currentPage--;
searchLogs();
}
}
public void nextPage() {
currentPage++;
searchLogs();
}
public long getSuccessRate() {
if (totalRecords == 0) return 0;
return (successCount * 100) / totalRecords;
}
/**
* Charger les realms disponibles
*/
private void loadRealms() {
// TODO: Implémenter la récupération des realms depuis Keycloak
availableRealms = List.of("master", "btpxpress", "unionflow");
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));
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
}

View File

@@ -0,0 +1,233 @@
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;
import jakarta.annotation.PostConstruct;
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;
import jakarta.faces.context.FacesContext;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.logging.Logger;
/**
* Bean JSF pour le tableau de bord
*
* @author Lions User Manager
* @version 1.0.0
*/
@Named("dashboardBean")
@ViewScoped
@Data
public class DashboardBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(DashboardBean.class.getName());
@Inject
@RestClient
private UserServiceClient userServiceClient;
@Inject
@RestClient
private RoleServiceClient roleServiceClient;
@Inject
@RestClient
private AuditServiceClient auditServiceClient;
@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;
private Long recentActions = 0L;
private Long activeSessions = 0L;
private Long onlineUsers = 0L;
// Indicateur de chargement
private boolean loading = false;
// Méthodes pour obtenir les valeurs formatées pour l'affichage
public String getTotalUsersDisplay() {
if (loading)
return "...";
return totalUsers != null ? String.valueOf(totalUsers) : "0";
}
public String getTotalRolesDisplay() {
if (loading)
return "...";
return totalRoles != null ? String.valueOf(totalRoles) : "0";
}
public String getRecentActionsDisplay() {
if (loading)
return "...";
return recentActions != null ? String.valueOf(recentActions) : "0";
}
public 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 (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() {
this.realmName = defaultRealm;
LOGGER.info("Initialisation DashboardBean pour realm: " + realmName);
loadStatistics();
}
/**
* Charger toutes les statistiques
*/
public void loadStatistics() {
loading = true;
try {
loadTotalUsers();
loadTotalRoles();
loadRecentActions();
loadSessionStats();
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des statistiques: " + e.getMessage());
} finally {
loading = false;
}
}
/**
* Charger le nombre total d'utilisateurs
*/
private void loadTotalUsers() {
try {
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.page(0)
.pageSize(1)
.build();
UserSearchResultDTO result = userServiceClient.searchUsers(criteria);
totalUsers = (result != null && result.getTotalCount() != null) ? result.getTotalCount() : 0L;
} catch (Exception e) {
LOGGER.severe("Erreur chargement total utilisateurs: " + e.getMessage());
totalUsers = 0L;
addErrorMessage("Impossible de charger le nombre d'utilisateurs: " + e.getMessage());
}
}
/**
* Charger le nombre total de rôles Realm
*/
private void loadTotalRoles() {
try {
List<?> roles = roleServiceClient.getAllRealmRoles(realmName);
totalRoles = (roles != null) ? (long) roles.size() : 0L;
} catch (Exception e) {
LOGGER.severe("Erreur chargement total rôles: " + e.getMessage());
totalRoles = 0L;
addErrorMessage("Impossible de charger le nombre de rôles: " + e.getMessage());
}
}
/**
* Charger le nombre d'actions récentes (dernières 24h)
*/
private void loadRecentActions() {
try {
String dateDebutStr = LocalDateTime.now().minusDays(1).format(DATE_FORMATTER);
String dateFinStr = LocalDateTime.now().format(DATE_FORMATTER);
try {
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;
recentActions = successCount + failureCount;
} catch (Exception e2) {
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 chargement actions récentes: " + e.getMessage());
recentActions = 0L;
}
}
/**
* 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;
}
}
/**
* Rafraîchir les statistiques
*/
public void refreshStatistics() {
loadStatistics();
addSuccessMessage("Statistiques rafraîchies avec succès");
}
// Méthodes utilitaires pour les messages
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
}

View File

@@ -0,0 +1,142 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.api.AuditRestClient;
import dev.lions.user.manager.client.api.HealthRestClient;
import dev.lions.user.manager.client.api.UserRestClient;
import dev.lions.user.manager.enums.audit.TypeActionAudit;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.primefaces.model.charts.ChartData;
import org.primefaces.model.charts.axes.cartesian.CartesianScales;
import org.primefaces.model.charts.axes.cartesian.linear.CartesianLinearAxes;
import org.primefaces.model.charts.bar.BarChartDataSet;
import org.primefaces.model.charts.bar.BarChartModel;
import org.primefaces.model.charts.bar.BarChartOptions;
import org.primefaces.model.charts.optionconfig.title.Title;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Named
@ViewScoped
@Slf4j
@SuppressWarnings("deprecation") // ChartData API dépréciée - migration vers JSON prévue
public class DashboardView implements Serializable {
@Inject
@RestClient
AuditRestClient auditRestClient;
@Inject
@RestClient
HealthRestClient healthRestClient;
@Inject
@RestClient
UserRestClient userRestClient;
@ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "test-realm")
String defaultRealm;
@Getter
private long totalSuccesses;
@Getter
private long totalFailures;
@Getter
private long activeUsers;
@Getter
private boolean systemHealthy;
@Getter
private BarChartModel barModel;
@PostConstruct
public void init() {
loadStats();
createBarModel();
}
public void loadStats() {
try {
totalSuccesses = auditRestClient.getSuccessCount(null, null);
totalFailures = auditRestClient.getFailureCount(null, null);
// Assuming we display active users for default realm
// Ideally we would have an endpoint for global stats
activeUsers = 0; // Placeholder until we have count endpoint in UserRestClient or general stats
try {
Map<String, Object> health = healthRestClient.getServiceStatus();
systemHealthy = "UP".equals(health.get("status"));
} catch (Exception e) {
systemHealthy = false;
}
} catch (Exception e) {
log.error("Error loading stats", e);
}
}
@SuppressWarnings("deprecation") // ChartData sera remplacé par une approche JSON moderne dans une version future
public void createBarModel() {
barModel = new BarChartModel();
ChartData data = new ChartData();
BarChartDataSet barDataSet = new BarChartDataSet();
barDataSet.setLabel("Activités par type");
List<Object> values = new ArrayList<>();
List<String> labels = new ArrayList<>();
List<String> bgColor = new ArrayList<>();
List<String> borderColor = new ArrayList<>();
try {
Map<TypeActionAudit, Long> stats = auditRestClient.getActionStatistics(null, null);
for (Map.Entry<TypeActionAudit, Long> entry : stats.entrySet()) {
labels.add(entry.getKey().name());
values.add(entry.getValue());
bgColor.add("rgba(75, 192, 192, 0.2)");
borderColor.add("rgb(75, 192, 192)");
}
} catch (Exception e) {
log.error("Error loading chart data", e);
}
barDataSet.setData(values);
barDataSet.setBackgroundColor(bgColor);
barDataSet.setBorderColor(borderColor);
barDataSet.setBorderWidth(1);
data.addChartDataSet(barDataSet);
data.setLabels(labels);
barModel.setData(data);
// Options
BarChartOptions options = new BarChartOptions();
CartesianScales cScales = new CartesianScales();
CartesianLinearAxes linearAxes = new CartesianLinearAxes();
linearAxes.setOffset(true);
cScales.addYAxesData(linearAxes);
options.setScales(cScales);
Title title = new Title();
title.setDisplay(true);
title.setText("Audit Actions");
options.setTitle(title);
barModel.setOptions(options);
}
}

View File

@@ -0,0 +1,285 @@
package dev.lions.user.manager.client.view;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import lombok.Data;
import org.primefaces.model.TreeNode;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.*;
import java.util.logging.Logger;
/**
* Bean JSF pour la showcase Freya Extension
* Démonstration complète des 46 composants personnalisés
*
* @author Lions User Manager
* @version 1.0.0
*/
@Named("demoBean")
@ViewScoped
@Data
public class FreyaShowcaseBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(FreyaShowcaseBean.class.getName());
// ============ DONNÉES UTILISATEUR EXEMPLE ============
private User user = new User();
private List<SampleUser> sampleUsers;
private TreeNode<NodeData> treeRoot;
// ============ DIALOGUES ============
private FormDialogData dialogData = new FormDialogData();
// ============ COMPOSANTS ============
private String requiredField;
private String inplaceText = "Texte éditable";
private String chartData;
private Integer progressValue = 0;
@PostConstruct
public void init() {
LOGGER.info("=== Initialisation du FreyaShowcaseBean ===");
// Initialiser les données d'exemple
initSampleUsers();
initTreeData();
initChartData();
// Valeurs par défaut pour les composants
user.setVolume(50);
user.setRating(4);
user.setQuantite(10);
}
/**
* Initialiser les utilisateurs d'exemple pour dataTable et dataView
*/
private void initSampleUsers() {
sampleUsers = new ArrayList<>();
sampleUsers.add(new SampleUser("Jean Dupont", "jean.dupont@example.com", true));
sampleUsers.add(new SampleUser("Marie Martin", "marie.martin@example.com", true));
sampleUsers.add(new SampleUser("Pierre Durand", "pierre.durand@example.com", false));
sampleUsers.add(new SampleUser("Sophie Bernard", "sophie.bernard@example.com", true));
sampleUsers.add(new SampleUser("Luc Petit", "luc.petit@example.com", false));
sampleUsers.add(new SampleUser("Anne Dubois", "anne.dubois@example.com", true));
sampleUsers.add(new SampleUser("Paul Thomas", "paul.thomas@example.com", true));
sampleUsers.add(new SampleUser("Claire Robert", "claire.robert@example.com", false));
}
/**
* Initialiser l'arborescence pour tree et treeTable
*/
private void initTreeData() {
treeRoot = new org.primefaces.model.DefaultTreeNode<>(new NodeData("Racine", "Dossier"), null);
TreeNode<NodeData> documents = new org.primefaces.model.DefaultTreeNode<>(
new NodeData("Documents", "Dossier"), treeRoot);
TreeNode<NodeData> images = new org.primefaces.model.DefaultTreeNode<>(
new NodeData("Images", "Dossier"), treeRoot);
new org.primefaces.model.DefaultTreeNode<>(new NodeData("Rapport.pdf", "Fichier"), documents);
new org.primefaces.model.DefaultTreeNode<>(new NodeData("Facture.xlsx", "Fichier"), documents);
new org.primefaces.model.DefaultTreeNode<>(new NodeData("Photo1.jpg", "Fichier"), images);
new org.primefaces.model.DefaultTreeNode<>(new NodeData("Photo2.png", "Fichier"), images);
}
/**
* Initialiser les données du graphique Chart.js (format JSON moderne)
*/
private void initChartData() {
// Utilisation de JSON direct pour Chart.js (API moderne, non dépréciée)
chartData = """
{
"type": "bar",
"data": {
"labels": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin"],
"datasets": [{
"label": "Statistiques Mensuelles",
"data": [65, 59, 80, 81, 56, 55],
"backgroundColor": [
"rgba(54, 162, 235, 0.2)",
"rgba(75, 192, 192, 0.2)",
"rgba(255, 206, 86, 0.2)",
"rgba(153, 102, 255, 0.2)",
"rgba(255, 99, 132, 0.2)",
"rgba(255, 159, 64, 0.2)"
],
"borderColor": [
"rgba(54, 162, 235, 1)",
"rgba(75, 192, 192, 1)",
"rgba(255, 206, 86, 1)",
"rgba(153, 102, 255, 1)",
"rgba(255, 99, 132, 1)",
"rgba(255, 159, 64, 1)"
],
"borderWidth": 1
}]
},
"options": {
"responsive": true,
"maintainAspectRatio": false,
"plugins": {
"legend": {
"display": true,
"position": "top"
}
},
"scales": {
"y": {
"beginAtZero": true
}
}
}
}
""";
}
/**
* Autocomplétion pour fieldAutoComplete
*/
public List<String> completeCities(String query) {
List<String> allCities = Arrays.asList(
"Paris", "Lyon", "Marseille", "Toulouse", "Nice", "Nantes",
"Strasbourg", "Montpellier", "Bordeaux", "Lille", "Rennes", "Reims"
);
return allCities.stream()
.filter(city -> city.toLowerCase().startsWith(query.toLowerCase()))
.toList();
}
// ============ ACTIONS ============
public void saveAction() {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", "Données sauvegardées avec succès"));
LOGGER.info("Action Save executée");
}
public void cancelAction() {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Annulé", "Action annulée"));
LOGGER.info("Action Cancel executée");
}
public void refreshAction() {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Rafraîchi", "Données rafraîchies"));
LOGGER.info("Action Refresh executée");
}
public void confirmAction() {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Confirmé", "Action confirmée avec succès"));
LOGGER.info("Action Confirm executée (actionDialog)");
}
public void createUser() {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Utilisateur créé",
"Utilisateur " + dialogData.getNom() + " créé avec succès"));
LOGGER.info("Utilisateur créé: " + dialogData.getNom() + " - " + dialogData.getEmail());
dialogData = new FormDialogData(); // Reset
}
public void showSuccessMessage() {
FacesContext.getCurrentInstance().addMessage("growlDemo",
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", "Opération réussie avec succès"));
}
public void showWarningMessage() {
FacesContext.getCurrentInstance().addMessage("growlDemo",
new FacesMessage(FacesMessage.SEVERITY_WARN, "Attention", "Ceci est un avertissement"));
}
public void startProgress() {
progressValue = 0;
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
progressValue = Math.min(100, progressValue + 10);
if (progressValue >= 100) {
timer.cancel();
}
}
}, 0, 500);
}
public void resetProgress() {
progressValue = 0;
}
// ============ CLASSES INTERNES ============
/**
* Classe User pour démonstration des champs de formulaire
*/
@Data
public static class User implements Serializable {
private String nom;
private String password;
private String description;
private Integer age;
private LocalDate dateNaissance;
private String pays;
private List<String> competences;
private Boolean actif;
private String genre;
private Boolean newsletter;
private Boolean modeNuit;
private Integer volume;
private Integer rating;
private List<String> tags;
private String couleur;
private String bio;
private String telephone;
private String ville;
private Integer quantite;
}
/**
* Classe SampleUser pour dataTable et dataView
*/
@Data
public static class SampleUser implements Serializable {
private String nom;
private String email;
private Boolean actif;
public SampleUser(String nom, String email, Boolean actif) {
this.nom = nom;
this.email = email;
this.actif = actif;
}
}
/**
* Classe NodeData pour tree et treeTable
*/
@Data
public static class NodeData implements Serializable {
private String label;
private String type;
public NodeData(String label, String type) {
this.label = label;
this.type = type;
}
}
/**
* Classe FormDialogData pour formDialog
*/
@Data
public static class FormDialogData implements Serializable {
private String nom;
private String email;
}
}

View File

@@ -0,0 +1,401 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.service.RealmAssignmentServiceClient;
import dev.lions.user.manager.client.service.RealmServiceClient;
import dev.lions.user.manager.client.service.UserServiceClient;
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
import dev.lions.user.manager.dto.user.UserDTO;
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Data;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* Bean JSF pour la gestion des affectations de realms
* Permet d'assigner des realms aux utilisateurs pour le contrôle d'accès
* multi-tenant
*
* @author Lions User Manager
* @version 1.0.0
*/
@Named("realmAssignmentBean")
@ViewScoped
@Data
public class RealmAssignmentBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(RealmAssignmentBean.class.getName());
@Inject
@RestClient
private RealmAssignmentServiceClient realmAssignmentServiceClient;
@Inject
@RestClient
private UserServiceClient userServiceClient;
@Inject
@RestClient
private RealmServiceClient realmServiceClient;
@Inject
private UserSessionBean userSessionBean;
// Listes
private List<RealmAssignmentDTO> assignments = new ArrayList<>();
private List<UserDTO> availableUsers = new ArrayList<>();
private List<String> availableRealms = new ArrayList<>();
private RealmAssignmentDTO selectedAssignment;
// Pour la création/édition
private RealmAssignmentDTO newAssignment = RealmAssignmentDTO.builder()
.active(true)
.temporaire(false)
.build();
private String selectedUserId;
private String selectedRealmName;
// Filtres
private String filterUserName;
private String filterRealmName;
@PostConstruct
public void init() {
LOGGER.info("Initialisation de RealmAssignmentBean");
// Vérifier si l'utilisateur est admin
if (!userSessionBean.hasRole("admin")) {
addErrorMessage("Accès refusé: Cette fonctionnalité est réservée aux administrateurs");
return;
}
loadAssignments();
loadAvailableUsers();
loadAvailableRealms();
}
/**
* Charger toutes les affectations
*/
public void loadAssignments() {
try {
LOGGER.info("Chargement de toutes les affectations de realms");
assignments = realmAssignmentServiceClient.getAllAssignments();
LOGGER.info("Chargement réussi: " + assignments.size() + " affectation(s) trouvée(s)");
if (assignments.isEmpty()) {
addInfoMessage("Aucune affectation de realm configurée");
}
} catch (Exception e) {
String errorMsg = "Erreur lors du chargement des affectations: " + e.getMessage();
LOGGER.severe(errorMsg);
LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e);
addErrorMessage(errorMsg);
assignments = new ArrayList<>();
}
}
/**
* Charger les utilisateurs disponibles
*/
public void loadAvailableUsers() {
try {
LOGGER.info("Chargement des utilisateurs disponibles");
// Charger les utilisateurs du realm lions-user-manager (page 0, 100
// utilisateurs max)
UserSearchResultDTO result = userServiceClient.getAllUsers("lions-user-manager", 0, 100);
availableUsers = result != null && result.getUsers() != null ? result.getUsers() : new ArrayList<>();
LOGGER.info("Chargement réussi: " + availableUsers.size() + " utilisateur(s) disponible(s)");
} catch (Exception e) {
String errorMsg = "Erreur lors du chargement des utilisateurs: " + e.getMessage();
LOGGER.severe(errorMsg);
addErrorMessage(errorMsg);
availableUsers = new ArrayList<>();
}
}
/**
* Charger les realms disponibles depuis Keycloak
*/
public void loadAvailableRealms() {
try {
LOGGER.info("Chargement des realms disponibles depuis Keycloak");
List<String> realms = realmServiceClient.getAllRealms();
if (realms == null || realms.isEmpty()) {
LOGGER.warning("Aucun realm trouvé dans Keycloak");
availableRealms = Collections.emptyList();
addInfoMessage("Aucun realm disponible dans Keycloak");
} else {
availableRealms = new ArrayList<>(realms);
LOGGER.info("Realms disponibles chargés depuis Keycloak: " + availableRealms.size());
}
} catch (Exception e) {
String errorMsg = "Erreur lors du chargement des realms depuis Keycloak: " + e.getMessage();
LOGGER.severe(errorMsg);
LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e);
addErrorMessage(errorMsg);
// Fallback: liste vide plutôt que des données fictives
availableRealms = Collections.emptyList();
}
}
/**
* Assigner un realm à un utilisateur
*/
public void assignRealm() {
try {
if (selectedUserId == null || selectedUserId.isEmpty()) {
addErrorMessage("Veuillez sélectionner un utilisateur");
return;
}
if (selectedRealmName == null || selectedRealmName.isEmpty()) {
addErrorMessage("Veuillez sélectionner un realm");
return;
}
// Trouver l'utilisateur sélectionné
UserDTO selectedUser = availableUsers.stream()
.filter(u -> u.getId().equals(selectedUserId))
.findFirst()
.orElse(null);
if (selectedUser == null) {
addErrorMessage("Utilisateur introuvable");
return;
}
// Construire l'assignation
RealmAssignmentDTO assignment = RealmAssignmentDTO.builder()
.userId(selectedUserId)
.username(selectedUser.getUsername())
.email(selectedUser.getEmail())
.realmName(selectedRealmName)
.isSuperAdmin(false)
.assignedAt(LocalDateTime.now())
.assignedBy(userSessionBean.getUsername())
.raison(newAssignment.getRaison())
.commentaires(newAssignment.getCommentaires())
.temporaire(newAssignment.getTemporaire() != null && newAssignment.getTemporaire())
.dateExpiration(newAssignment.getDateExpiration())
.active(true)
.build();
LOGGER.info("Assignation du realm " + selectedRealmName + " à l'utilisateur " + selectedUser.getUsername());
jakarta.ws.rs.core.Response response = realmAssignmentServiceClient.assignRealmToUser(assignment);
response.readEntity(RealmAssignmentDTO.class);
addSuccessMessage("Realm '" + selectedRealmName + "' assigné avec succès à " + selectedUser.getUsername());
resetForm();
loadAssignments();
} catch (Exception e) {
String errorMsg = "Erreur lors de l'assignation: " + e.getMessage();
LOGGER.severe(errorMsg);
LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e);
addErrorMessage(errorMsg);
}
}
/**
* Révoquer l'accès d'un utilisateur à un realm
*/
public void revokeAssignment(RealmAssignmentDTO assignment) {
try {
if (assignment == null) {
addErrorMessage("Assignation invalide");
return;
}
LOGGER.info("Révocation du realm " + assignment.getRealmName() + " pour l'utilisateur "
+ assignment.getUsername());
realmAssignmentServiceClient.revokeRealmFromUser(assignment.getUserId(), assignment.getRealmName());
addSuccessMessage(
"Accès révoqué pour " + assignment.getUsername() + " au realm '" + assignment.getRealmName() + "'");
loadAssignments();
} catch (Exception e) {
String errorMsg = "Erreur lors de la révocation: " + e.getMessage();
LOGGER.severe(errorMsg);
LOGGER.log(java.util.logging.Level.SEVERE, "Exception complète", e);
addErrorMessage(errorMsg);
}
}
/**
* Désactiver une assignation
*/
public void deactivateAssignment(RealmAssignmentDTO assignment) {
try {
if (assignment == null || assignment.getId() == null) {
addErrorMessage("Assignation invalide");
return;
}
LOGGER.info("Désactivation de l'assignation " + assignment.getId());
realmAssignmentServiceClient.deactivateAssignment(assignment.getId());
addSuccessMessage("Assignation désactivée");
loadAssignments();
} catch (Exception e) {
String errorMsg = "Erreur lors de la désactivation: " + e.getMessage();
LOGGER.severe(errorMsg);
addErrorMessage(errorMsg);
}
}
/**
* Activer une assignation
*/
public void activateAssignment(RealmAssignmentDTO assignment) {
try {
if (assignment == null || assignment.getId() == null) {
addErrorMessage("Assignation invalide");
return;
}
LOGGER.info("Activation de l'assignation " + assignment.getId());
realmAssignmentServiceClient.activateAssignment(assignment.getId());
addSuccessMessage("Assignation activée");
loadAssignments();
} catch (Exception e) {
String errorMsg = "Erreur lors de l'activation: " + e.getMessage();
LOGGER.severe(errorMsg);
addErrorMessage(errorMsg);
}
}
/**
* Définir un utilisateur comme super admin
*/
public void setSuperAdmin(String userId, boolean superAdmin) {
try {
if (userId == null || userId.isEmpty()) {
addErrorMessage("Utilisateur invalide");
return;
}
UserDTO user = availableUsers.stream()
.filter(u -> u.getId().equals(userId))
.findFirst()
.orElse(null);
String username = user != null ? user.getUsername() : userId;
LOGGER.info("Définition de " + username + " comme super admin: " + superAdmin);
realmAssignmentServiceClient.setSuperAdmin(userId, superAdmin);
if (superAdmin) {
addSuccessMessage(username + " est maintenant super admin (peut gérer tous les realms)");
} else {
addSuccessMessage("Privilèges super admin retirés pour " + username);
}
loadAssignments();
} catch (Exception e) {
String errorMsg = "Erreur lors de la modification du statut super admin: " + e.getMessage();
LOGGER.severe(errorMsg);
addErrorMessage(errorMsg);
}
}
/**
* Réinitialiser le formulaire
*/
public void resetForm() {
newAssignment = RealmAssignmentDTO.builder()
.active(true)
.temporaire(false)
.build();
selectedUserId = null;
selectedRealmName = null;
}
/**
* Obtenir les assignations filtrées
*/
public List<RealmAssignmentDTO> getFilteredAssignments() {
if (filterUserName == null && filterRealmName == null) {
return assignments;
}
return assignments.stream()
.filter(a -> {
boolean matchUser = filterUserName == null || filterUserName.isEmpty() ||
(a.getUsername() != null
&& a.getUsername().toLowerCase().contains(filterUserName.toLowerCase()));
boolean matchRealm = filterRealmName == null || filterRealmName.isEmpty() ||
(a.getRealmName() != null
&& a.getRealmName().toLowerCase().contains(filterRealmName.toLowerCase()));
return matchUser && matchRealm;
})
.collect(Collectors.toList());
}
/**
* Obtenir le nombre total d'assignations
*/
public int getTotalAssignments() {
return assignments != null ? assignments.size() : 0;
}
/**
* Obtenir le nombre d'assignations actives
*/
public long getActiveAssignmentsCount() {
return assignments.stream()
.filter(RealmAssignmentDTO::isActive)
.count();
}
/**
* Obtenir le nombre de super admins
*/
public long getSuperAdminsCount() {
return assignments.stream()
.filter(RealmAssignmentDTO::isSuperAdmin)
.count();
}
// Méthodes utilitaires pour les messages
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
private void addInfoMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Information", message));
}
}

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;
@@ -18,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
@@ -37,18 +36,23 @@ public class RoleGestionBean implements Serializable {
@RestClient
private RoleServiceClient roleServiceClient;
@Inject
@RestClient
private RealmServiceClient realmServiceClient;
// Liste des rôles
private List<RoleDTO> realmRoles = new ArrayList<>();
private List<RoleDTO> clientRoles = new ArrayList<>();
private List<RoleDTO> allRoles = new ArrayList<>();
private RoleDTO selectedRole;
private String selectedRoleName;
// Pour la création/édition
private RoleDTO newRole = RoleDTO.builder().build();
private boolean editMode = false;
// Filtres
private String realmName = "master";
private String realmName = "lions-user-manager";
private String clientName;
private TypeRole selectedTypeRole;
private String roleSearchText;
@@ -73,21 +77,23 @@ public class RoleGestionBean implements Serializable {
addErrorMessage("Veuillez sélectionner un realm");
return;
}
try {
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);
}
} catch (Exception e) {
String errorMsg = "Erreur lors du chargement des rôles Realm pour le realm '" + realmName + "': " + e.getMessage();
String errorMsg = "Erreur lors du chargement des rôles Realm pour le realm '" + realmName + "': "
+ e.getMessage();
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();
}
}
@@ -97,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) {
@@ -119,12 +127,45 @@ 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
*/
public void createRealmRole() {
try {
RoleDTO created = roleServiceClient.createRealmRole(newRole, realmName);
jakarta.ws.rs.core.Response response = roleServiceClient.createRealmRole(newRole, realmName);
RoleDTO created = response.readEntity(RoleDTO.class);
addSuccessMessage("Rôle Realm créé avec succès: " + created.getName());
resetForm();
loadRealmRoles();
@@ -144,7 +185,8 @@ public class RoleGestionBean implements Serializable {
}
try {
RoleDTO created = roleServiceClient.createClientRole(newRole, realmName, clientName);
jakarta.ws.rs.core.Response response = roleServiceClient.createClientRole(clientName, newRole, realmName);
RoleDTO created = response.readEntity(RoleDTO.class);
addSuccessMessage("Rôle Client créé avec succès: " + created.getName());
resetForm();
loadClientRoles();
@@ -160,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());
@@ -178,8 +220,8 @@ public class RoleGestionBean implements Serializable {
}
try {
roleServiceClient.deleteClientRole(roleName, realmName, clientName);
addSuccessMessage("Rôle Client supprimé avec succès");
roleServiceClient.deleteClientRole(clientName, roleName, realmName);
addSuccessMessage("Rôle Client supprimé avec succès: " + roleName);
loadClientRoles();
} catch (Exception e) {
LOGGER.severe("Erreur lors de la suppression: " + e.getMessage());
@@ -192,14 +234,10 @@ public class RoleGestionBean implements Serializable {
*/
public void assignRoleToUser(String userId, String roleName) {
try {
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
.userId(userId)
.roleNames(List.of(roleName))
.typeRole(TypeRole.REALM_ROLE)
.realmName(realmName)
.build();
dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO request = new dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO(
List.of(roleName));
roleServiceClient.assignRoleToUser(assignment);
roleServiceClient.assignRealmRoles(userId, realmName, request);
addSuccessMessage("Rôle attribué avec succès");
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'attribution: " + e.getMessage());
@@ -212,14 +250,10 @@ public class RoleGestionBean implements Serializable {
*/
public void revokeRoleFromUser(String userId, String roleName) {
try {
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
.userId(userId)
.roleNames(List.of(roleName))
.typeRole(TypeRole.REALM_ROLE)
.realmName(realmName)
.build();
dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO request = new dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO(
List.of(roleName));
roleServiceClient.revokeRoleFromUser(assignment);
roleServiceClient.revokeRealmRoles(userId, realmName, request);
addSuccessMessage("Rôle révoqué avec succès");
} catch (Exception e) {
LOGGER.severe("Erreur lors de la révocation: " + e.getMessage());
@@ -234,7 +268,7 @@ public class RoleGestionBean implements Serializable {
FacesContext context = FacesContext.getCurrentInstance();
String userId = context.getExternalContext().getRequestParameterMap().get("userId");
String roleName = context.getExternalContext().getRequestParameterMap().get("roleName");
if (userId != null && roleName != null) {
assignRoleToUser(userId, roleName);
} else {
@@ -249,7 +283,7 @@ public class RoleGestionBean implements Serializable {
FacesContext context = FacesContext.getCurrentInstance();
String userId = context.getExternalContext().getRequestParameterMap().get("userId");
String roleName = context.getExternalContext().getRequestParameterMap().get("roleName");
if (userId != null && roleName != null) {
revokeRoleFromUser(userId, roleName);
} else {
@@ -264,14 +298,14 @@ public class RoleGestionBean implements Serializable {
if (user == null || user.getRealmRoles() == null || user.getRealmRoles().isEmpty()) {
return new ArrayList<>();
}
if (allRoles == null || allRoles.isEmpty()) {
return new ArrayList<>();
}
return allRoles.stream()
.filter(role -> user.getRealmRoles().contains(role.getName()))
.collect(java.util.stream.Collectors.toList());
.filter(role -> user.getRealmRoles().contains(role.getName()))
.collect(Collectors.toList());
}
/**
@@ -283,30 +317,55 @@ public class RoleGestionBean implements Serializable {
}
/**
* Charger les realms disponibles
* Charger les realms disponibles depuis Keycloak
*/
private void loadRealms() {
try {
// Pour l'instant, utiliser les realms de la configuration
// TODO: Implémenter la récupération des realms depuis Keycloak via un endpoint API
availableRealms = List.of("master", "lions-user-manager", "btpxpress", "test-realm");
LOGGER.info("Realms disponibles chargés: " + availableRealms.size());
availableRealms = realmServiceClient.getAllRealms();
LOGGER.info("Realms chargés: " + availableRealms);
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des realms: " + e.getMessage());
// Fallback sur une liste par défaut
availableRealms = List.of("master");
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é.
*/
public void loadRolesForUser(String realm) {
if (realm != null && !realm.isEmpty()) {
this.realmName = realm;
loadRealmRoles();
} else {
loadRealmRoles();
}
}
// Méthodes utilitaires
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
}

View File

@@ -0,0 +1,98 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.api.RoleRestClient;
import dev.lions.user.manager.dto.role.RoleDTO;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Named
@ViewScoped
@Slf4j
public class RoleView implements Serializable {
@Inject
@RestClient
RoleRestClient roleRestClient;
@ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "test-realm")
String defaultRealm;
@Getter
@Setter
private List<RoleDTO> roles;
@Getter
@Setter
private RoleDTO selectedRole;
@Getter
@Setter
private String selectedRealm;
@PostConstruct
public void init() {
this.selectedRealm = defaultRealm;
this.selectedRole = new RoleDTO();
loadRoles();
}
public void loadRoles() {
try {
roles = roleRestClient.getAllRealmRoles(selectedRealm);
} catch (Exception e) {
log.error("Error loading roles", e);
roles = new ArrayList<>();
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", "Impossible de charger les rôles"));
}
}
public void openNew() {
this.selectedRole = new RoleDTO();
}
public void saveRole() {
try {
if (this.selectedRole.getId() == null) {
// Create
roleRestClient.createRealmRole(selectedRealm, this.selectedRole);
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Rôle créé"));
} else {
// Update
roleRestClient.updateRealmRole(this.selectedRole.getName(), selectedRealm, this.selectedRole);
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Rôle mis à jour"));
}
loadRoles();
} catch (Exception e) {
log.error("Error saving role", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage()));
}
}
public void deleteRole() {
try {
roleRestClient.deleteRealmRole(this.selectedRole.getName(), selectedRealm);
this.selectedRole = null;
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Rôle supprimé"));
loadRoles();
} catch (Exception e) {
log.error("Error deleting role", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage()));
}
}
}

View File

@@ -0,0 +1,156 @@
package dev.lions.user.manager.client.view;
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.time.Instant;
import java.time.Duration;
import java.util.logging.Logger;
/**
* Bean de monitoring de session utilisateur en temps réel
* Calcule le temps restant avant expiration du token JWT
*
* @author Lions User Manager Team
* @version 1.0
*/
@Named("sessionMonitor")
@SessionScoped
public class SessionMonitorBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(SessionMonitorBean.class.getName());
// Temps d'inactivité maximum en secondes (30 minutes par défaut)
private static final long DEFAULT_INACTIVITY_TIMEOUT = 1800;
private Instant lastActivityTime;
private long inactivityTimeout = DEFAULT_INACTIVITY_TIMEOUT;
public SessionMonitorBean() {
this.lastActivityTime = Instant.now();
}
/**
* Met à jour le timestamp de la dernière activité
*/
public void updateActivity() {
this.lastActivityTime = Instant.now();
}
/**
* Calcule le temps d'inactivité en secondes
*/
public long getInactivitySeconds() {
if (lastActivityTime == null) {
lastActivityTime = Instant.now();
return 0;
}
return Duration.between(lastActivityTime, Instant.now()).getSeconds();
}
/**
* Calcule le temps restant avant expiration en minutes
*/
public long getRemainingMinutes() {
long inactivitySeconds = getInactivitySeconds();
long remainingSeconds = inactivityTimeout - inactivitySeconds;
if (remainingSeconds < 0) {
return 0;
}
return remainingSeconds / 60;
}
/**
* Calcule le temps restant avant expiration en secondes (pour le timer)
*/
public long getRemainingSeconds() {
long inactivitySeconds = getInactivitySeconds();
long remainingSeconds = inactivityTimeout - inactivitySeconds;
return Math.max(0, remainingSeconds);
}
/**
* Formate le temps restant en format mm:ss
*/
public String getFormattedRemainingTime() {
long totalSeconds = getRemainingSeconds();
long minutes = totalSeconds / 60;
long seconds = totalSeconds % 60;
return String.format("%02d:%02d", minutes, seconds);
}
/**
* Retourne le pourcentage de temps écoulé (pour une barre de progression)
*/
public int getSessionProgressPercent() {
long inactivitySeconds = getInactivitySeconds();
if (inactivityTimeout == 0)
return 0;
int percent = (int) ((inactivitySeconds * 100) / inactivityTimeout);
return Math.min(100, Math.max(0, percent));
}
/**
* Vérifie si la session est proche de l'expiration (moins de 5 minutes)
*/
public boolean isSessionExpiringSoon() {
return getRemainingMinutes() <= 5;
}
/**
* Vérifie si la session est expirée
*/
public boolean isSessionExpired() {
return getRemainingSeconds() == 0;
}
/**
* Retourne la classe CSS pour l'indicateur de temps (couleur)
*/
public String getTimeIndicatorClass() {
long minutes = getRemainingMinutes();
if (minutes <= 3) {
return "text-red-600 font-bold"; // Rouge critique
} else if (minutes <= 5) {
return "text-orange-600 font-semibold"; // Orange warning
} else if (minutes <= 10) {
return "text-yellow-600"; // Jaune attention
} else {
return "text-green-600"; // Vert OK
}
}
/**
* Retourne l'icône appropriée selon le temps restant
*/
public String getTimeIndicatorIcon() {
long minutes = getRemainingMinutes();
if (minutes <= 3) {
return "pi pi-exclamation-triangle";
} else if (minutes <= 5) {
return "pi pi-clock";
} else {
return "pi pi-check-circle";
}
}
// Getters et Setters
public long getInactivityTimeout() {
return inactivityTimeout;
}
public void setInactivityTimeout(long inactivityTimeout) {
this.inactivityTimeout = inactivityTimeout;
}
public Instant getLastActivityTime() {
return lastActivityTime;
}
}

View File

@@ -0,0 +1,63 @@
package dev.lions.user.manager.client.view;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Data;
import java.io.Serializable;
import java.util.logging.Logger;
/**
* Bean JSF pour la page de paramètres
*
* @author Lions User Manager
* @version 1.0.0
*/
@Named("settingsBean")
@ViewScoped
@Data
public class SettingsBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(SettingsBean.class.getName());
@Inject
private UserSessionBean userSessionBean;
@Inject
private GuestPreferences guestPreferences;
@PostConstruct
public void init() {
LOGGER.info("Initialisation de SettingsBean");
}
/**
* Sauvegarder les préférences
*/
public void savePreferences() {
try {
// Les préférences sont déjà sauvegardées dans GuestPreferences (SessionScoped)
addSuccessMessage("Préférences sauvegardées avec succès");
} catch (Exception e) {
LOGGER.severe("Erreur lors de la sauvegarde: " + e.getMessage());
addErrorMessage("Erreur lors de la sauvegarde: " + e.getMessage());
}
}
// Méthodes utilitaires
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
}

View File

@@ -0,0 +1,215 @@
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;
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
@ViewScoped
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;
@PostConstruct
public void init() {
this.targetRealm = defaultRealm;
checkKeycloakStatus();
loadLastSyncStatus();
}
public void checkKeycloakStatus() {
try {
this.keycloakStatus = syncService.checkKeycloakHealth();
} catch (Exception e) {
LOGGER.severe("Erreur lors de la vérification de l'état de Keycloak: " + e.getMessage());
this.keycloakStatus = new HealthStatusDTO();
this.keycloakStatus.setOverallHealthy(false);
this.keycloakStatus.setErrorMessage("Erreur de connexion: " + e.getMessage());
}
}
public void syncUsers() {
try {
SyncResultDTO result = syncService.syncUsers(targetRealm);
handleSyncResult("Utilisateurs", result);
} catch (Exception e) {
LOGGER.severe("Erreur lors de la synchronisation des utilisateurs: " + e.getMessage());
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Echec de la synchronisation des utilisateurs: " + e.getMessage());
}
}
public void syncRoles() {
try {
// Sychronize realm roles
SyncResultDTO result = syncService.syncRoles(targetRealm, null);
handleSyncResult("Rôles", result);
} catch (Exception e) {
LOGGER.severe("Erreur lors de la synchronisation des rôles: " + e.getMessage());
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Echec de la synchronisation des rôles: " + e.getMessage());
}
}
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 : "
+ ("Utilisateurs".equals(type) ? result.getUsersCount() : result.getRealmRolesCount())
: result.getErrorMessage();
if (result.isSuccess()) {
addMessage(FacesMessage.SEVERITY_INFO, "Succès", msg);
} else {
addMessage(FacesMessage.SEVERITY_WARN, "Attention", type + ": " + msg);
}
this.syncMessage = msg;
}
private void addMessage(FacesMessage.Severity severity, String summary, String detail) {
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(severity, summary, detail));
}
// Getters and Setters
public HealthStatusDTO getKeycloakStatus() { return keycloakStatus; }
public void setKeycloakStatus(HealthStatusDTO keycloakStatus) { this.keycloakStatus = keycloakStatus; }
public SyncConsistencyDTO getConsistencyResult() { return consistencyResult; }
public SyncHistoryDTO getLastSyncStatusDTO() { return lastSyncStatus; }
public String getKeycloakStatusLabel() {
return (keycloakStatus != null && keycloakStatus.isOverallHealthy()) ? "UP" : "DOWN";
}
public String getKeycloakStatusMessage() {
if (keycloakStatus == null)
return "Unknown status";
if (keycloakStatus.isOverallHealthy())
return "Keycloak est accessible.";
return keycloakStatus.getErrorMessage() != null ? keycloakStatus.getErrorMessage() : "Erreur inconnue";
}
public String getKeycloakVersion() {
return (keycloakStatus != null) ? keycloakStatus.getKeycloakVersion() : "N/A";
}
public String getSyncMessage() {
return syncMessage;
}
public void setSyncMessage(String syncMessage) {
this.syncMessage = syncMessage;
}
public String getTargetRealm() {
return targetRealm;
}
public void setTargetRealm(String targetRealm) {
this.targetRealm = targetRealm;
}
}

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,10 +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();
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());
@@ -46,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);
@@ -56,37 +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
UserDTO createdUser = userServiceClient.createUser(newUser, realmName);
jakarta.ws.rs.core.Response response = userServiceClient.createUser(newUser, realmName);
UserDTO createdUser = response.readEntity(UserDTO.class);
// Définir le mot de passe
userServiceClient.resetPassword(createdUser.getId(), realmName, password);
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());
}
}
@@ -97,36 +107,55 @@ 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
availableRealms = List.of("master", "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
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
}

View File

@@ -1,24 +1,40 @@
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.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
*
@@ -42,15 +58,22 @@ public class UserListBean implements Serializable {
@RestClient
private UserServiceClient userServiceClient;
@Inject
@RestClient
private RealmServiceClient realmServiceClient;
@ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "lions-user-manager")
String defaultRealm;
// Propriétés pour la liste
private List<UserDTO> users = new ArrayList<>();
private LazyDataModel<UserDTO> users;
private UserDTO selectedUser;
private List<UserDTO> selectedUsers = new ArrayList<>();
// Propriétés pour la recherche
private UserSearchCriteriaDTO searchCriteria = UserSearchCriteriaDTO.builder().build();
private String searchText;
private String realmName = "master";
private String realmName;
private StatutUser selectedStatut;
// Propriétés pour la pagination
@@ -59,64 +82,100 @@ 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<>();
// Champs pour réinitialisation mot de passe
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");
loadUsers();
this.realmName = defaultRealm;
LOGGER.info("Initialisation de UserListBean - realm: " + realmName);
loadRealms();
loadStats();
users = new LazyDataModel<UserDTO>() {
@Override
public String getRowKey(UserDTO user) {
return user.getId();
}
@Override
public UserDTO getRowData(String rowKey) {
List<UserDTO> list = (List<UserDTO>) getWrappedData();
if (list != null) {
for (UserDTO user : list) {
if (user.getId().equals(rowKey)) {
return user;
}
}
}
return null;
}
@Override
public int count(Map<String, FilterMeta> filterBy) {
return (int) totalRecords;
}
@Override
public List<UserDTO> load(int first, int pageSize, Map<String, SortMeta> sortBy,
Map<String, FilterMeta> filterBy) {
try {
int page = first / pageSize;
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.searchTerm(searchText)
.statut(selectedStatut)
.page(page)
.pageSize(pageSize)
.includeRoles(true)
.build();
UserSearchResultDTO result = userServiceClient.searchUsers(criteria);
List<UserDTO> data = result.getUsers() != null ? result.getUsers() : new ArrayList<>();
long total = result.getTotalCount() != null ? result.getTotalCount() : 0;
this.setRowCount((int) total);
totalRecords = total;
return data;
} catch (Exception e) {
LOGGER.severe("Erreur loading: " + e.getMessage());
return new ArrayList<>();
}
}
};
}
/**
* Charger la liste des utilisateurs
* (Deprecated: Utiliser LazyDataModel)
*/
public void loadUsers() {
try {
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.page(currentPage)
.pageSize(pageSize)
.build();
UserSearchResultDTO result = userServiceClient.searchUsers(criteria);
this.users = result.getUsers() != null ? result.getUsers() : new ArrayList<>();
this.totalRecords = result.getTotalCount() != null ? result.getTotalCount() : 0;
this.totalPages = (int) Math.ceil((double) totalRecords / pageSize);
LOGGER.info("Chargement de " + users.size() + " utilisateurs");
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des utilisateurs: " + e.getMessage());
addErrorMessage("Erreur lors du chargement des utilisateurs: " + e.getMessage());
}
// La méthode load du LazyDataModel est appelée automatiquement par le composant
}
/**
* Rechercher des utilisateurs
*/
public void search() {
try {
currentPage = 0; // Réinitialiser à la première page
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.searchTerm(searchText)
.statut(selectedStatut)
.page(currentPage)
.pageSize(pageSize)
.build();
UserSearchResultDTO result = userServiceClient.searchUsers(criteria);
this.users = result.getUsers() != null ? result.getUsers() : new ArrayList<>();
this.totalRecords = result.getTotalCount() != null ? result.getTotalCount() : 0;
this.totalPages = (int) Math.ceil((double) totalRecords / pageSize);
addSuccessMessage("Recherche effectuée: " + totalRecords + " résultat(s) trouvé(s)");
} catch (Exception e) {
LOGGER.severe("Erreur lors de la recherche: " + e.getMessage());
addErrorMessage("Erreur lors de la recherche: " + e.getMessage());
currentPage = 0;
if (PrimeFaces.current().isAjaxRequest()) {
PrimeFaces.current().executeScript("PF('userTableWidget').getPaginator().setPage(0);");
}
}
@@ -127,7 +186,62 @@ public class UserListBean implements Serializable {
searchText = null;
selectedStatut = null;
currentPage = 0;
loadUsers();
if (PrimeFaces.current().isAjaxRequest()) {
PrimeFaces.current().executeScript("PF('userTableWidget').getPaginator().setPage(0);");
}
}
/**
* Réinitialiser le mot de passe
*/
public void resetPassword(String userId) {
if (newPassword != null && !newPassword.isEmpty() && newPassword.equals(newPasswordConfirm)) {
try {
// Utilisation du DTO pour la demande de réinitialisation
dev.lions.user.manager.dto.user.PasswordResetRequestDTO request = new dev.lions.user.manager.dto.user.PasswordResetRequestDTO(
newPassword, false);
userServiceClient.resetPassword(userId, realmName, request);
addSuccessMessage("Mot de passe réinitialisé avec succès");
// Clear fields
newPassword = null;
newPasswordConfirm = null;
} catch (Exception e) {
LOGGER.severe("Erreur lors de la réinitialisation du mot de passe: " + e.getMessage());
addErrorMessage("Erreur lors de la réinitialisation: " + e.getMessage());
}
} else {
addErrorMessage("Les mots de passe sont invalides ou ne correspondent pas");
}
}
/**
* Action pour activer un utilisateur (utilisé par le composant composite)
*/
public void activateUserAction(ActionEvent event) {
String userId = (String) event.getComponent().getAttributes().get("userId");
if (userId != null) {
activateUser(userId);
}
}
/**
* Action pour désactiver un utilisateur (utilisé par le composant composite)
*/
public void deactivateUserAction(ActionEvent event) {
String userId = (String) event.getComponent().getAttributes().get("userId");
if (userId != null) {
deactivateUser(userId);
}
}
/**
* Action pour supprimer un utilisateur (utilisé par le composant composite)
*/
public void deleteUserAction(ActionEvent event) {
String userId = (String) event.getComponent().getAttributes().get("userId");
if (userId != null) {
deleteUser(userId);
}
}
/**
@@ -136,8 +250,9 @@ 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");
loadUsers();
} catch (Exception e) {
LOGGER.severe("Erreur lors de l'activation: " + e.getMessage());
addErrorMessage("Erreur lors de l'activation: " + e.getMessage());
@@ -149,9 +264,10 @@ public class UserListBean implements Serializable {
*/
public void deactivateUser(String userId) {
try {
userServiceClient.deactivateUser(userId, realmName);
userServiceClient.deactivateUser(userId, realmName, "Désactivé par l'administrateur");
activeUsersCount = Math.max(0, activeUsersCount - 1);
disabledUsersCount++;
addSuccessMessage("Utilisateur désactivé avec succès");
loadUsers();
} catch (Exception e) {
LOGGER.severe("Erreur lors de la désactivation: " + e.getMessage());
addErrorMessage("Erreur lors de la désactivation: " + e.getMessage());
@@ -163,9 +279,9 @@ public class UserListBean implements Serializable {
*/
public void deleteUser(String userId) {
try {
userServiceClient.deleteUser(userId, realmName);
userServiceClient.deleteUser(userId, realmName, false);
kpiTotalUsers = Math.max(0, kpiTotalUsers - 1);
addSuccessMessage("Utilisateur supprimé avec succès");
loadUsers();
} catch (Exception e) {
LOGGER.severe("Erreur lors de la suppression: " + e.getMessage());
addErrorMessage("Erreur lors de la suppression: " + e.getMessage());
@@ -186,55 +302,136 @@ public class UserListBean implements Serializable {
}
/**
* Obtenir le nombre d'utilisateurs actifs
*/
public long getActiveUsersCount() {
if (users == null || users.isEmpty()) {
return 0;
}
return users.stream()
.filter(user -> user.getEnabled() != null && user.getEnabled())
.count();
}
/**
* Obtenir le nombre d'utilisateurs désactivés
*/
public long getDisabledUsersCount() {
if (users == null || users.isEmpty()) {
return 0;
}
return users.stream()
.filter(user -> user.getEnabled() != null && !user.getEnabled())
.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);
}
/**
* Charger les realms disponibles
* Rafraîchir les données et les KPIs
*/
public void refreshData() {
loadStats();
addSuccessMessage("Données rafraîchies");
}
/**
* 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() {
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 depuis un fichier CSV
*/
public void importUsers() {
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 depuis Keycloak
*/
private void loadRealms() {
// TODO: Implémenter la récupération des realms depuis Keycloak
availableRealms = List.of("master", "btpxpress", "unionflow");
try {
availableRealms = realmServiceClient.getAllRealms();
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");
}
}
/**
@@ -261,12 +458,11 @@ public class UserListBean implements Serializable {
// Méthodes utilitaires pour les messages
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
}

View File

@@ -34,7 +34,8 @@ public class UserProfilBean implements Serializable {
private UserDTO user;
private String userId;
private String realmName = "master";
// Le realm "lions-user-manager" est le realm par défaut
private String realmName = "lions-user-manager";
private boolean editMode = false;
// Pour la réinitialisation de mot de passe
@@ -45,7 +46,14 @@ public class UserProfilBean implements Serializable {
public void init() {
// Récupérer l'ID depuis les paramètres de requête
userId = FacesContext.getCurrentInstance().getExternalContext()
.getRequestParameterMap().get("userId");
.getRequestParameterMap().get("userId");
// Récupérer le realm depuis les paramètres de requête (si présent)
String realmParam = FacesContext.getCurrentInstance().getExternalContext()
.getRequestParameterMap().get("realm");
if (realmParam != null && !realmParam.isEmpty()) {
this.realmName = realmParam;
}
if (userId != null && !userId.isEmpty()) {
loadUser();
@@ -60,7 +68,13 @@ public class UserProfilBean implements Serializable {
public void loadUser() {
try {
user = userServiceClient.getUserById(userId, realmName);
LOGGER.info("Utilisateur chargé: " + user.getUsername());
if (user != null) {
LOGGER.info("Utilisateur chargé: " + user.getUsername());
}
} catch (dev.lions.user.manager.client.service.RestClientExceptionMapper.NotFoundException e) {
LOGGER.warning("Utilisateur non trouvé: " + userId);
addErrorMessage("Utilisateur non trouvé dans le realm " + realmName);
user = null;
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement de l'utilisateur: " + e.getMessage());
addErrorMessage("Erreur lors du chargement de l'utilisateur: " + e.getMessage());
@@ -111,7 +125,9 @@ public class UserProfilBean implements Serializable {
}
try {
userServiceClient.resetPassword(userId, realmName, newPassword);
dev.lions.user.manager.dto.user.PasswordResetRequestDTO request = new dev.lions.user.manager.dto.user.PasswordResetRequestDTO(
newPassword, false);
userServiceClient.resetPassword(userId, realmName, request);
newPassword = null;
newPasswordConfirm = null;
addSuccessMessage("Mot de passe réinitialisé avec succès");
@@ -140,7 +156,7 @@ public class UserProfilBean implements Serializable {
*/
public void deactivateUser() {
try {
userServiceClient.deactivateUser(userId, realmName);
userServiceClient.deactivateUser(userId, realmName, "Désactivé depuis le profil utilisateur");
loadUser(); // Recharger pour mettre à jour le statut
addSuccessMessage("Utilisateur désactivé avec succès");
} catch (Exception e) {
@@ -162,6 +178,38 @@ public class UserProfilBean implements Serializable {
}
}
/**
* Supprimer l'utilisateur
*/
public void deleteUser() {
try {
userServiceClient.deleteUser(userId, realmName, false);
addSuccessMessage("Utilisateur supprimé avec succès");
FacesContext.getCurrentInstance().getExternalContext().redirect("list.xhtml");
} catch (Exception e) {
LOGGER.severe("Erreur lors de la suppression: " + e.getMessage());
addErrorMessage("Erreur lors de la suppression: " + e.getMessage());
}
}
// Méthodes compatibles avec user-actions.xhtml (qui passe l'ID en paramètre)
public void activateUser(String userId) {
activateUser();
}
public void deactivateUser(String userId) {
deactivateUser();
}
public void resetPassword(String userId) {
resetPassword();
}
public void deleteUser(String userId) {
deleteUser();
}
/**
* Déconnecter toutes les sessions
*/
@@ -175,15 +223,18 @@ public class UserProfilBean implements Serializable {
}
}
public void logoutAllSessions(String userId) {
logoutAllSessions();
}
// Méthodes utilitaires
private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
}
private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
}
}

View File

@@ -0,0 +1,361 @@
package dev.lions.user.manager.client.view;
import io.quarkus.oidc.IdToken;
import io.quarkus.oidc.OidcSession;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.SessionScoped;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Data;
import org.eclipse.microprofile.jwt.JsonWebToken;
import java.io.Serializable;
import java.util.logging.Logger;
/**
* Bean de session pour gérer les informations de l'utilisateur connecté
*
* @author Lions User Manager
* @version 1.0.0
*/
@Named("userSessionBean")
@SessionScoped
@Data
public class UserSessionBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = Logger.getLogger(UserSessionBean.class.getName());
@Inject
SecurityIdentity securityIdentity;
@Inject
@IdToken
JsonWebToken idToken;
@Inject
OidcSession oidcSession;
// Informations utilisateur
private String username;
private String email;
private String firstName;
private String lastName;
private String fullName;
private String initials;
@PostConstruct
public void init() {
loadUserInfo();
}
/**
* Charger les informations utilisateur depuis le token OIDC
*/
public void loadUserInfo() {
try {
if (idToken != null && securityIdentity != null && !securityIdentity.isAnonymous()) {
// Username
username = idToken.getClaim("preferred_username");
if (username == null || username.trim().isEmpty()) {
username = securityIdentity.getPrincipal().getName();
}
// Email
email = idToken.getClaim("email");
if (email == null || email.trim().isEmpty()) {
email = username + "@lions.dev";
}
// Prénom et nom
firstName = idToken.getClaim("given_name");
lastName = idToken.getClaim("family_name");
// Nom complet
fullName = idToken.getClaim("name");
if (fullName == null || fullName.trim().isEmpty()) {
if (firstName != null && lastName != null) {
fullName = firstName + " " + lastName;
} else if (firstName != null) {
fullName = firstName;
} else if (lastName != null) {
fullName = lastName;
} else {
fullName = username;
}
}
// Initiales pour l'avatar
initials = generateInitials(fullName);
LOGGER.info("Informations utilisateur chargées: " + fullName + " (" + email + ")");
} else {
// Valeurs par défaut si non authentifié
username = "Utilisateur";
email = "utilisateur@lions.dev";
fullName = "Utilisateur";
initials = "U";
}
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement des informations utilisateur: " + e.getMessage());
username = "Utilisateur";
email = "utilisateur@lions.dev";
fullName = "Utilisateur";
initials = "U";
}
}
/**
* Générer les initiales depuis le nom complet
*/
private String generateInitials(String name) {
if (name == null || name.trim().isEmpty()) {
return "U";
}
String[] parts = name.trim().split("\\s+");
if (parts.length >= 2) {
return String.valueOf(parts[0].charAt(0)).toUpperCase() +
String.valueOf(parts[1].charAt(0)).toUpperCase();
} else if (parts.length == 1) {
String part = parts[0];
if (part.length() >= 2) {
return part.substring(0, 2).toUpperCase();
} else {
return part.substring(0, 1).toUpperCase();
}
}
return "U";
}
// Rôles
private java.util.Set<String> roles;
private String primaryRole;
/**
* Obtenir le rôle principal de l'utilisateur
*/
public String getPrimaryRole() {
if (primaryRole == null) {
primaryRole = getMainRole();
}
return primaryRole;
}
/**
* Obtenir tous les rôles de l'utilisateur
*/
public java.util.Set<String> getRoles() {
if (roles == null) {
roles = new java.util.HashSet<>();
try {
if (securityIdentity != null && securityIdentity.getRoles() != null) {
roles.addAll(securityIdentity.getRoles());
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération des rôles: " + e.getMessage());
}
if (roles.isEmpty()) {
roles.add("Utilisateur");
}
}
return roles;
}
/**
* Obtenir le rôle principal de l'utilisateur (méthode interne)
*/
private String getMainRole() {
try {
if (securityIdentity != null && securityIdentity.getRoles() != null
&& !securityIdentity.getRoles().isEmpty()) {
// Prioriser certains rôles
java.util.Set<String> roleSet = securityIdentity.getRoles();
if (roleSet.contains("admin")) {
return "Administrateur";
} else if (roleSet.contains("user_manager")) {
return "Gestionnaire";
} else if (roleSet.contains("user_viewer")) {
return "Consultant";
} else {
return roleSet.iterator().next();
}
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération du rôle: " + e.getMessage());
}
return "Utilisateur";
}
/**
* Vérifier si l'utilisateur est authentifié
*/
public boolean isAuthenticated() {
return securityIdentity != null && !securityIdentity.isAnonymous();
}
/**
* Vérifier si l'utilisateur a un rôle spécifique
*/
public boolean hasRole(String role) {
try {
if (securityIdentity != null && securityIdentity.getRoles() != null) {
return securityIdentity.getRoles().contains(role);
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la vérification du rôle: " + e.getMessage());
}
return false;
}
/**
* Obtenir l'issuer du token OIDC
*/
public String getIssuer() {
try {
if (idToken != null) {
return idToken.getIssuer();
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération de l'issuer: " + e.getMessage());
}
return "Non disponible";
}
/**
* Obtenir le subject du token OIDC
*/
public String getSubject() {
try {
if (idToken != null) {
return idToken.getSubject();
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération du subject: " + e.getMessage());
}
return "Non disponible";
}
/**
* Obtenir le session ID
*/
public String getSessionId() {
try {
if (idToken != null) {
Object sid = idToken.getClaim("sid");
if (sid != null) {
return sid.toString();
}
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération du session ID: " + e.getMessage());
}
return "Non disponible";
}
/**
* Obtenir le temps d'expiration du token
*/
public java.util.Date getExpirationTime() {
try {
if (idToken != null && idToken.getExpirationTime() > 0) {
return new java.util.Date(idToken.getExpirationTime() * 1000L);
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération de l'expiration: " + e.getMessage());
}
return null;
}
/**
* Obtenir le temps d'émission du token
*/
public java.util.Date getIssuedAt() {
try {
if (idToken != null && idToken.getIssuedAtTime() > 0) {
return new java.util.Date(idToken.getIssuedAtTime() * 1000L);
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération de l'émission: " + e.getMessage());
}
return null;
}
/**
* Obtenir l'audience du token
*/
public String getAudience() {
try {
if (idToken != null && idToken.getAudience() != null && !idToken.getAudience().isEmpty()) {
return String.join(", ", idToken.getAudience());
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération de l'audience: " + e.getMessage());
}
return "Non disponible";
}
/**
* Obtenir l'authorized party (azp)
*/
public String getAuthorizedParty() {
try {
if (idToken != null) {
Object azp = idToken.getClaim("azp");
if (azp != null) {
return azp.toString();
}
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la récupération de l'authorized party: " + e.getMessage());
}
return "Non disponible";
}
/**
* Vérifier si l'email est vérifié
*/
public boolean isEmailVerified() {
try {
if (idToken != null) {
Boolean emailVerified = idToken.getClaim("email_verified");
return emailVerified != null && emailVerified;
}
} catch (Exception e) {
LOGGER.warning("Erreur lors de la vérification de l'email: " + e.getMessage());
}
return false;
}
/**
* Déconnexion OIDC
* Redirige vers l'endpoint de logout Quarkus qui gère la déconnexion Keycloak
*/
public String logout() {
try {
LOGGER.info("Déconnexion de l'utilisateur: " + fullName);
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
// Invalider la session HTTP locale
externalContext.invalidateSession();
// Rediriger vers l'endpoint de logout OIDC de Quarkus
// Quarkus gère la déconnexion Keycloak (end_session_endpoint) + redirection
// post-logout
String contextPath = externalContext.getRequestContextPath();
externalContext.redirect(contextPath + "/auth/logout");
facesContext.responseComplete();
return null;
} catch (Exception e) {
LOGGER.severe("Erreur lors de la déconnexion: " + e.getMessage());
// En cas d'erreur, rediriger vers la page d'accueil
return "/?faces-redirect=true";
}
}
}

View File

@@ -0,0 +1,177 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.api.UserRestClient;
import dev.lions.user.manager.dto.user.UserDTO;
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.primefaces.model.FilterMeta;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortMeta;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
@Named
@ViewScoped
@Slf4j
public class UserView implements Serializable {
@Inject
@RestClient
UserRestClient userRestClient;
@ConfigProperty(name = "lions.user.manager.default.realm", defaultValue = "test-realm")
String defaultRealm;
@Getter
@Setter
private LazyDataModel<UserDTO> users;
@Getter
@Setter
private UserDTO selectedUser;
@Getter
@Setter
private String selectedRealm;
@Getter
@Setter
private String searchTerm;
@PostConstruct
public void init() {
this.selectedRealm = defaultRealm;
this.selectedUser = new UserDTO(); // Initialize to avoid NPE in dialogs before selection
users = new LazyDataModel<UserDTO>() {
@Override
public int count(Map<String, FilterMeta> filterBy) {
// Simplified count logic reusing search API
try {
return (int) userRestClient
.searchUsers(selectedRealm, searchTerm, null, null, null, null, null, 0, 1).getTotalCount()
.intValue();
} catch (Exception e) {
log.error("Error counting users", e);
return 0;
}
}
@Override
public List<UserDTO> load(int first, int pageSize, Map<String, SortMeta> sortBy,
Map<String, FilterMeta> filterBy) {
try {
int page = first / pageSize;
UserSearchResultDTO result = userRestClient.searchUsers(selectedRealm, searchTerm, null, null, null,
null, null, page, pageSize);
setRowCount(result.getTotalCount().intValue());
return result.getUsers();
} catch (Exception e) {
log.error("Error loading users", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur de chargement", e.getMessage()));
return List.of();
}
}
@Override
public UserDTO getRowData(String rowKey) {
// Not ideal for lazy model, but needed for selection sometimes if not using
// rowDataWrapper
// Assuming ID is rowKey
try {
return userRestClient.getUserById(rowKey, selectedRealm);
} catch (Exception e) {
return null;
}
}
@Override
public String getRowKey(UserDTO user) {
return user.getId();
}
};
}
public void openNew() {
this.selectedUser = new UserDTO();
this.selectedUser.setEnabled(true);
}
public void saveUser() {
try {
if (this.selectedUser.getId() == null) {
// Create
// Password handling: assume temporary password is set in UI
if (this.selectedUser.getTemporaryPassword() == null
|| this.selectedUser.getTemporaryPassword().isBlank()) {
// Generate or require password logic here. For now, let's assume UI requires
// it.
}
userRestClient.createUser(selectedRealm, this.selectedUser);
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Succès", "Utilisateur créé"));
} else {
// Update
userRestClient.updateUser(this.selectedUser.getId(), selectedRealm, this.selectedUser);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Succès", "Utilisateur mis à jour"));
}
// PrimeFaces.current().executeScript("PF('manageUserDialog').hide()"); //
// Handled in xhtml via oncomplete
} catch (Exception e) {
log.error("Error saving user", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage()));
}
}
public void deleteUser() {
try {
userRestClient.deleteUser(this.selectedUser.getId(), selectedRealm, false);
this.selectedUser = null;
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Succès", "Utilisateur supprimé (soft delete)"));
} catch (Exception e) {
log.error("Error deleting user", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage()));
}
}
public void downloadCSV() {
try {
String csvContent = userRestClient.exportUsersToCSV(selectedRealm);
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
externalContext.setResponseContentType("text/csv");
externalContext.setResponseHeader("Content-Disposition", "attachment; filename=\"users_export.csv\"");
OutputStream output = externalContext.getResponseOutputStream();
output.write(csvContent.getBytes(StandardCharsets.UTF_8));
facesContext.responseComplete();
} catch (IOException e) {
log.error("Error exporting CSV", e);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur Export", e.getMessage()));
}
}
}

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>
@@ -18,7 +22,9 @@
<navigation-rule>
<from-view-id>*</from-view-id>
<!-- Dashboard -->
<!-- ================================================================
DASHBOARD & ACCUEIL
================================================================ -->
<navigation-case>
<description>Page d'accueil / Dashboard</description>
<from-outcome>userManagerDashboardPage</from-outcome>
@@ -26,7 +32,16 @@
<redirect />
</navigation-case>
<!-- Users -->
<navigation-case>
<description>Navigation directe vers dashboard</description>
<from-outcome>/pages/user-manager/dashboard</from-outcome>
<to-view-id>/pages/user-manager/dashboard.xhtml</to-view-id>
<redirect />
</navigation-case>
<!-- ================================================================
GESTION DES UTILISATEURS
================================================================ -->
<navigation-case>
<description>Page de liste des utilisateurs</description>
<from-outcome>userListPage</from-outcome>
@@ -34,6 +49,13 @@
<redirect />
</navigation-case>
<navigation-case>
<description>Navigation directe vers liste utilisateurs</description>
<from-outcome>/pages/user-manager/users/list</from-outcome>
<to-view-id>/pages/user-manager/users/list.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<description>Page de création d'utilisateur</description>
<from-outcome>userCreatePage</from-outcome>
@@ -41,6 +63,13 @@
<redirect />
</navigation-case>
<navigation-case>
<description>Navigation directe vers création utilisateur</description>
<from-outcome>/pages/user-manager/users/create</from-outcome>
<to-view-id>/pages/user-manager/users/create.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<description>Page de profil utilisateur</description>
<from-outcome>userProfilePage</from-outcome>
@@ -48,6 +77,13 @@
<redirect />
</navigation-case>
<navigation-case>
<description>Navigation directe vers profil utilisateur</description>
<from-outcome>/pages/user-manager/users/profile</from-outcome>
<to-view-id>/pages/user-manager/users/profile.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<description>Page d'édition utilisateur</description>
<from-outcome>userEditPage</from-outcome>
@@ -55,7 +91,16 @@
<redirect />
</navigation-case>
<!-- Roles -->
<navigation-case>
<description>Navigation directe vers édition utilisateur</description>
<from-outcome>/pages/user-manager/users/edit</from-outcome>
<to-view-id>/pages/user-manager/users/edit.xhtml</to-view-id>
<redirect />
</navigation-case>
<!-- ================================================================
GESTION DES RÔLES
================================================================ -->
<navigation-case>
<description>Page de liste des rôles</description>
<from-outcome>roleListPage</from-outcome>
@@ -63,6 +108,13 @@
<redirect />
</navigation-case>
<navigation-case>
<description>Navigation directe vers liste rôles</description>
<from-outcome>/pages/user-manager/roles/list</from-outcome>
<to-view-id>/pages/user-manager/roles/list.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<description>Page d'attribution de rôles</description>
<from-outcome>roleAssignPage</from-outcome>
@@ -70,7 +122,16 @@
<redirect />
</navigation-case>
<!-- Audit -->
<navigation-case>
<description>Navigation directe vers attribution rôles</description>
<from-outcome>/pages/user-manager/roles/assign</from-outcome>
<to-view-id>/pages/user-manager/roles/assign.xhtml</to-view-id>
<redirect />
</navigation-case>
<!-- ================================================================
AUDIT
================================================================ -->
<navigation-case>
<description>Page de journal d'audit</description>
<from-outcome>auditLogsPage</from-outcome>
@@ -78,7 +139,16 @@
<redirect />
</navigation-case>
<!-- Sync -->
<navigation-case>
<description>Navigation directe vers journal d'audit</description>
<from-outcome>/pages/user-manager/audit/logs</from-outcome>
<to-view-id>/pages/user-manager/audit/logs.xhtml</to-view-id>
<redirect />
</navigation-case>
<!-- ================================================================
SYNCHRONISATION
================================================================ -->
<navigation-case>
<description>Page de dashboard synchronisation</description>
<from-outcome>syncDashboardPage</from-outcome>
@@ -86,6 +156,30 @@
<redirect />
</navigation-case>
<navigation-case>
<description>Navigation directe vers dashboard synchronisation</description>
<from-outcome>/pages/user-manager/sync/dashboard</from-outcome>
<to-view-id>/pages/user-manager/sync/dashboard.xhtml</to-view-id>
<redirect />
</navigation-case>
<!-- ================================================================
PARAMÈTRES & PROFIL
================================================================ -->
<navigation-case>
<description>Page de paramètres utilisateur</description>
<from-outcome>settingsPage</from-outcome>
<to-view-id>/pages/user-manager/settings.xhtml</to-view-id>
<redirect />
</navigation-case>
<navigation-case>
<description>Navigation directe vers paramètres</description>
<from-outcome>/pages/user-manager/settings</from-outcome>
<to-view-id>/pages/user-manager/settings.xhtml</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
</faces-config>

View File

@@ -0,0 +1,982 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lions User Manager - Plateforme de Gestion IAM Centralisée</title>
<!-- PrimeIcons -->
<link rel="stylesheet" href="https://unpkg.com/primeicons@7.0.0/primeicons.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
/* ============================================
FREYA BLUE - Couleurs officielles du template
============================================ */
/* Primary Blue - Freya Template */
--primary-color: #4F8EEC;
--primary-50: #EBF3FE;
--primary-100: #D7E7FD;
--primary-200: #AECFFB;
--primary-300: #86B7F9;
--primary-400: #5D9FF6;
--primary-500: #4F8EEC; /* Base */
--primary-600: #387FE9; /* Hover */
--primary-700: #2C6DCC; /* Active */
--primary-800: #2159A8;
--primary-900: #164684;
/* Surface - Freya Neutral Colors */
--surface-0: #ffffff;
--surface-50: #FAFAFA;
--surface-100: #F5F5F5;
--surface-200: #EEEEEE;
--surface-300: #E0E0E0;
--surface-400: #BDBDBD;
--surface-500: #9E9E9E;
--surface-600: #757575;
--surface-700: #616161;
--surface-800: #424242;
--surface-900: #212121;
/* Text Colors - Freya */
--text-color: #495057;
--text-color-secondary: #6c757d;
/* Semantic Colors - Freya */
--blue-500: #4F8EEC;
--green-500: #34D399;
--red-500: #EF4444;
--yellow-500: #F59E0B;
/* Border & Shadows */
--border-radius: 12px;
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--surface-0);
color: var(--text-color);
overflow-x: hidden;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ==================== NAVBAR ==================== */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--surface-200);
padding: 1rem 0;
transition: var(--transition);
}
.navbar.scrolled {
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
background: rgba(255, 255, 255, 0.98);
}
.navbar-container {
max-width: 1280px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 800;
font-size: 1.25rem;
color: var(--text-color);
text-decoration: none;
}
.logo-icon {
width: 42px;
height: 42px;
background: linear-gradient(135deg, var(--primary-500), var(--primary-700));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 14px rgba(79, 142, 236, 0.35);
}
.logo-icon i {
color: white;
font-size: 1.25rem;
}
.nav-cta {
background: linear-gradient(135deg, var(--primary-500), var(--primary-700));
color: white;
padding: 0.75rem 1.75rem;
border-radius: var(--border-radius);
text-decoration: none;
font-weight: 600;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 4px 14px rgba(79, 142, 236, 0.35);
transition: var(--transition);
}
.nav-cta:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 142, 236, 0.45);
background: linear-gradient(135deg, var(--primary-600), var(--primary-800));
}
/* ==================== HERO SECTION ==================== */
.hero {
margin-top: 80px;
padding: 6rem 2rem 4rem;
background: linear-gradient(180deg, var(--surface-0) 0%, var(--primary-50) 100%);
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(79, 142, 236, 0.12) 0%, transparent 70%);
border-radius: 50%;
animation: float 20s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-30px, -30px) scale(1.1); }
}
.hero-container {
max-width: 1280px;
margin: 0 auto;
position: relative;
z-index: 1;
}
.hero-content {
max-width: 720px;
margin: 0 auto;
text-align: center;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: white;
padding: 0.5rem 1.25rem;
border-radius: 50px;
font-size: 0.875rem;
font-weight: 600;
color: var(--primary-600);
box-shadow: 0 2px 12px rgba(79, 142, 236, 0.15);
margin-bottom: 2rem;
animation: slideDown 0.8s ease-out;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-badge i {
font-size: 1rem;
color: var(--primary-500);
}
.hero h1 {
font-size: 3.75rem;
font-weight: 900;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, var(--text-color), var(--primary-700));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: fadeInUp 0.8s ease-out 0.2s both;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-subtitle {
font-size: 1.375rem;
color: var(--text-color-secondary);
margin-bottom: 3rem;
line-height: 1.7;
font-weight: 400;
animation: fadeInUp 0.8s ease-out 0.4s both;
}
.hero-cta-group {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
animation: fadeInUp 0.8s ease-out 0.6s both;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-500), var(--primary-700));
color: white;
padding: 1rem 2.5rem;
border-radius: var(--border-radius);
text-decoration: none;
font-weight: 700;
font-size: 1.125rem;
display: inline-flex;
align-items: center;
gap: 0.75rem;
box-shadow: 0 8px 24px rgba(79, 142, 236, 0.4);
transition: var(--transition);
border: none;
cursor: pointer;
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(79, 142, 236, 0.5);
background: linear-gradient(135deg, var(--primary-600), var(--primary-800));
}
.btn-secondary {
background: white;
color: var(--text-color);
padding: 1rem 2.5rem;
border-radius: var(--border-radius);
text-decoration: none;
font-weight: 700;
font-size: 1.125rem;
display: inline-flex;
align-items: center;
gap: 0.75rem;
border: 2px solid var(--surface-300);
transition: var(--transition);
cursor: pointer;
}
.btn-secondary:hover {
border-color: var(--primary-500);
color: var(--primary-600);
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(79, 142, 236, 0.15);
}
/* ==================== SESSION EXPIRED ALERT ==================== */
.session-expired-alert {
max-width: 600px;
margin: 0 auto 3rem;
background: linear-gradient(135deg, #FEE2E2, #FECACA);
border: 2px solid #FCA5A5;
border-radius: var(--border-radius);
padding: 1.25rem 1.5rem;
display: none;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.15);
animation: slideDown 0.6s ease-out;
}
.session-expired-alert.show {
display: flex;
}
.session-expired-alert i {
color: var(--red-500);
font-size: 2rem;
flex-shrink: 0;
}
.session-expired-alert .message {
color: #7F1D1D;
font-weight: 600;
line-height: 1.6;
}
/* ==================== STATS SECTION ==================== */
.stats-section {
padding: 4rem 2rem;
background: white;
}
.stats-container {
max-width: 1280px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.stat-card {
text-align: center;
padding: 2rem 1.5rem;
border-radius: var(--border-radius);
background: var(--surface-50);
transition: var(--transition);
border: 1px solid var(--surface-200);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
border-color: var(--primary-200);
}
.stat-number {
font-size: 3rem;
font-weight: 900;
background: linear-gradient(135deg, var(--primary-500), var(--primary-700));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 1rem;
color: var(--text-color-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ==================== FEATURES SECTION ==================== */
.features-section {
padding: 6rem 2rem;
background: linear-gradient(180deg, white 0%, var(--surface-50) 100%);
}
.features-header {
max-width: 720px;
margin: 0 auto 4rem;
text-align: center;
}
.section-badge {
display: inline-block;
background: var(--primary-100);
color: var(--primary-700);
padding: 0.5rem 1rem;
border-radius: 50px;
font-size: 0.875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 1.5rem;
}
.features-header h2 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 1.25rem;
color: var(--text-color);
}
.features-header p {
font-size: 1.25rem;
color: var(--text-color-secondary);
line-height: 1.7;
}
.features-grid {
max-width: 1280px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2.5rem;
}
.feature-card {
background: white;
padding: 2.5rem;
border-radius: 16px;
border: 1px solid var(--surface-200);
transition: var(--transition);
position: relative;
overflow: hidden;
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary-500), var(--primary-700));
transform: scaleX(0);
transition: var(--transition);
}
.feature-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.12);
border-color: var(--primary-300);
}
.feature-card:hover::before {
transform: scaleX(1);
}
.feature-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--primary-100), var(--primary-200));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
transition: var(--transition);
}
.feature-card:hover .feature-icon {
background: linear-gradient(135deg, var(--primary-500), var(--primary-700));
}
.feature-icon i {
font-size: 2rem;
color: var(--primary-600);
transition: var(--transition);
}
.feature-card:hover .feature-icon i {
color: white;
transform: scale(1.1);
}
.feature-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 1rem;
color: var(--text-color);
}
.feature-description {
color: var(--text-color-secondary);
line-height: 1.7;
font-size: 1rem;
}
.feature-list {
margin-top: 1.25rem;
list-style: none;
}
.feature-list li {
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--text-color-secondary);
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.feature-list li i {
color: var(--primary-500);
font-size: 1rem;
}
/* ==================== CTA SECTION ==================== */
.cta-section {
padding: 6rem 2rem;
background: linear-gradient(135deg, var(--primary-600), var(--primary-800));
position: relative;
overflow: hidden;
}
.cta-section::before {
content: '';
position: absolute;
top: -50%;
left: -10%;
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
border-radius: 50%;
}
.cta-container {
max-width: 800px;
margin: 0 auto;
text-align: center;
position: relative;
z-index: 1;
}
.cta-container h2 {
font-size: 3rem;
font-weight: 900;
color: white;
margin-bottom: 1.5rem;
line-height: 1.2;
}
.cta-container p {
font-size: 1.375rem;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 3rem;
line-height: 1.7;
}
.btn-cta-white {
background: white;
color: var(--primary-600);
padding: 1.25rem 3rem;
border-radius: var(--border-radius);
text-decoration: none;
font-weight: 800;
font-size: 1.25rem;
display: inline-flex;
align-items: center;
gap: 0.75rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
transition: var(--transition);
}
.btn-cta-white:hover {
transform: translateY(-4px) scale(1.05);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
color: var(--primary-700);
}
/* ==================== FOOTER ==================== */
.footer {
background: var(--surface-900);
color: var(--surface-400);
padding: 3rem 2rem 2rem;
}
.footer-container {
max-width: 1280px;
margin: 0 auto;
text-align: center;
}
.footer-logo {
display: inline-flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.footer-logo-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--primary-500), var(--primary-700));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.footer-logo-icon i {
color: white;
font-size: 1.5rem;
}
.footer-logo-text {
font-size: 1.5rem;
font-weight: 800;
color: white;
}
.footer-text {
font-size: 1rem;
margin-bottom: 2rem;
color: var(--surface-500);
}
.footer-divider {
height: 1px;
background: var(--surface-800);
margin: 2rem 0;
}
.footer-bottom {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.875rem;
}
.footer-bottom i {
color: var(--primary-500);
}
/* ==================== RESPONSIVE ==================== */
@media (max-width: 768px) {
.hero {
padding: 4rem 1.5rem 3rem;
}
.hero h1 {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.125rem;
}
.hero-cta-group {
flex-direction: column;
}
.btn-primary, .btn-secondary {
width: 100%;
justify-content: center;
}
.features-header h2,
.cta-container h2 {
font-size: 2rem;
}
.features-grid {
grid-template-columns: 1fr;
}
.stats-container {
grid-template-columns: 1fr;
}
.navbar-container {
padding: 0 1rem;
}
.nav-cta {
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
}
}
</style>
</head>
<body>
<!-- ==================== NAVBAR ==================== -->
<nav class="navbar" id="navbar">
<div class="navbar-container">
<a href="/" class="logo">
<div class="logo-icon">
<i class="pi pi-users"></i>
</div>
<span>Lions User Manager</span>
</a>
<a href="/pages/user-manager/dashboard.xhtml" class="nav-cta">
Accéder à la console
<i class="pi pi-arrow-right"></i>
</a>
</div>
</nav>
<!-- ==================== HERO SECTION ==================== -->
<section class="hero">
<div class="hero-container">
<!-- Session Expired Alert -->
<div id="sessionExpiredAlert" class="session-expired-alert">
<i class="pi pi-exclamation-triangle"></i>
<div class="message">
Votre session a expiré pour des raisons de sécurité. Veuillez vous reconnecter pour accéder à la plateforme.
</div>
</div>
<div class="hero-content">
<div class="hero-badge">
<i class="pi pi-shield"></i>
Plateforme IAM Centralisée
</div>
<h1>Gérez vos utilisateurs Keycloak en toute simplicité</h1>
<p class="hero-subtitle">
Une interface moderne et intuitive pour administrer vos identités, rôles et permissions à travers tous vos royaumes Keycloak. Sécurisé, performant, professionnel.
</p>
<div class="hero-cta-group">
<a href="/pages/user-manager/dashboard.xhtml" class="btn-primary">
<i class="pi pi-sign-in"></i>
Se connecter avec Keycloak
</a>
<a href="#features" class="btn-secondary">
<i class="pi pi-info-circle"></i>
Découvrir les fonctionnalités
</a>
</div>
</div>
</div>
</section>
<!-- ==================== STATS SECTION ==================== -->
<section class="stats-section">
<div class="stats-container">
<div class="stat-card">
<div class="stat-number" data-target="10000">0</div>
<div class="stat-label">Utilisateurs gérés</div>
</div>
<div class="stat-card">
<div class="stat-number" data-target="50">0</div>
<div class="stat-label">Royaumes actifs</div>
</div>
<div class="stat-card">
<div class="stat-number">99.9%</div>
<div class="stat-label">Disponibilité</div>
</div>
<div class="stat-card">
<div class="stat-number" data-target="24">0</div>
<div class="stat-label">Support 24/7</div>
</div>
</div>
</section>
<!-- ==================== FEATURES SECTION ==================== -->
<section class="features-section" id="features">
<div class="features-header">
<span class="section-badge">Fonctionnalités Métier</span>
<h2>Tout ce dont vous avez besoin pour gérer vos identités</h2>
<p>Une suite complète d'outils pour simplifier l'administration de votre infrastructure IAM.</p>
</div>
<div class="features-grid">
<!-- Feature 1 -->
<div class="feature-card">
<div class="feature-icon">
<i class="pi pi-user-plus"></i>
</div>
<h3 class="feature-title">Gestion des utilisateurs</h3>
<p class="feature-description">
Créez, modifiez et supprimez des utilisateurs en quelques clics. Interface intuitive avec recherche avancée et filtrage en temps réel.
</p>
<ul class="feature-list">
<li><i class="pi pi-check-circle"></i> Import/Export CSV massif</li>
<li><i class="pi pi-check-circle"></i> Recherche multi-critères</li>
<li><i class="pi pi-check-circle"></i> Modification par lot</li>
</ul>
</div>
<!-- Feature 2 -->
<div class="feature-card">
<div class="feature-icon">
<i class="pi pi-shield"></i>
</div>
<h3 class="feature-title">Attribution des rôles</h3>
<p class="feature-description">
Gérez les permissions de manière granulaire avec un système de rôles flexible et sécurisé conforme aux standards RBAC.
</p>
<ul class="feature-list">
<li><i class="pi pi-check-circle"></i> Gestion RBAC complète</li>
<li><i class="pi pi-check-circle"></i> Hiérarchie de rôles</li>
<li><i class="pi pi-check-circle"></i> Permissions dynamiques</li>
</ul>
</div>
<!-- Feature 3 -->
<div class="feature-card">
<div class="feature-icon">
<i class="pi pi-chart-line"></i>
</div>
<h3 class="feature-title">Audit & Analytics</h3>
<p class="feature-description">
Suivez l'activité de vos utilisateurs avec des tableaux de bord interactifs et des rapports détaillés en temps réel.
</p>
<ul class="feature-list">
<li><i class="pi pi-check-circle"></i> Logs d'authentification</li>
<li><i class="pi pi-check-circle"></i> Rapports personnalisés</li>
<li><i class="pi pi-check-circle"></i> Alertes de sécurité</li>
</ul>
</div>
<!-- Feature 4 -->
<div class="feature-card">
<div class="feature-icon">
<i class="pi pi-sync"></i>
</div>
<h3 class="feature-title">Synchronisation</h3>
<p class="feature-description">
Intégration transparente avec vos systèmes existants via API RESTful sécurisée et webhooks en temps réel.
</p>
<ul class="feature-list">
<li><i class="pi pi-check-circle"></i> API REST complète</li>
<li><i class="pi pi-check-circle"></i> Webhooks événementiels</li>
<li><i class="pi pi-check-circle"></i> Connecteurs pré-configurés</li>
</ul>
</div>
<!-- Feature 5 -->
<div class="feature-card">
<div class="feature-icon">
<i class="pi pi-lock"></i>
</div>
<h3 class="feature-title">Sécurité avancée</h3>
<p class="feature-description">
Protection multi-niveaux avec chiffrement end-to-end, authentification multi-facteurs et audit de sécurité complet.
</p>
<ul class="feature-list">
<li><i class="pi pi-check-circle"></i> MFA/2FA obligatoire</li>
<li><i class="pi pi-check-circle"></i> Chiffrement AES-256</li>
<li><i class="pi pi-check-circle"></i> SOC 2 Type II conforme</li>
</ul>
</div>
<!-- Feature 6 -->
<div class="feature-card">
<div class="feature-icon">
<i class="pi pi-cog"></i>
</div>
<h3 class="feature-title">Multi-tenant</h3>
<p class="feature-description">
Gérez plusieurs organisations et royaumes depuis une seule interface avec isolation complète des données.
</p>
<ul class="feature-list">
<li><i class="pi pi-check-circle"></i> Isolation par royaume</li>
<li><i class="pi pi-check-circle"></i> Personnalisation par org</li>
<li><i class="pi pi-check-circle"></i> Délégation d'administration</li>
</ul>
</div>
</div>
</section>
<!-- ==================== CTA SECTION ==================== -->
<section class="cta-section">
<div class="cta-container">
<h2>Prêt à transformer votre gestion IAM ?</h2>
<p>
Rejoignez des centaines d'entreprises qui font confiance à Lions User Manager pour sécuriser et simplifier leur infrastructure d'identité.
</p>
<a href="/pages/user-manager/dashboard.xhtml" class="btn-cta-white">
<i class="pi pi-sign-in"></i>
Accéder à la plateforme maintenant
</a>
</div>
</section>
<!-- ==================== FOOTER ==================== -->
<footer class="footer">
<div class="footer-container">
<div class="footer-logo">
<div class="footer-logo-icon">
<i class="pi pi-users"></i>
</div>
<span class="footer-logo-text">Lions User Manager</span>
</div>
<p class="footer-text">
Plateforme professionnelle de gestion IAM propulsée par Keycloak Admin API
</p>
<div class="footer-divider"></div>
<div class="footer-bottom">
<span>© 2025 Lions User Manager</span>
<span></span>
<span><i class="pi pi-shield"></i> Sécurisé par OpenID Connect</span>
<span></span>
<span><i class="pi pi-server"></i> Powered by Quarkus & PrimeFaces Freya</span>
</div>
</div>
</footer>
<!-- ==================== SCRIPTS ==================== -->
<script>
// Session expired alert
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('expired') === 'true') {
document.getElementById('sessionExpiredAlert').classList.add('show');
}
// Navbar scroll effect
window.addEventListener('scroll', () => {
const navbar = document.getElementById('navbar');
if (window.scrollY > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
});
// Animated counters
const animateCounter = (element) => {
const target = parseInt(element.getAttribute('data-target'));
const duration = 2000;
const step = target / (duration / 16);
let current = 0;
const timer = setInterval(() => {
current += step;
if (current >= target) {
element.textContent = target.toLocaleString();
clearInterval(timer);
} else {
element.textContent = Math.floor(current).toLocaleString();
}
}, 16);
};
// Intersection Observer for counter animation
const observerOptions = {
threshold: 0.5,
rootMargin: '0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && entry.target.getAttribute('data-target')) {
animateCounter(entry.target);
observer.unobserve(entry.target);
}
});
}, observerOptions);
document.querySelectorAll('.stat-number[data-target]').forEach(el => {
observer.observe(el);
});
// Smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,351 @@
<!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:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml">
<ui:define name="title">Affectation des Realms - Lions User Manager</ui:define>
<ui:define name="content">
<h:form id="formRealmAssignments">
<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-sitemap text-purple-500" style="font-size: 2rem"></i>
<div>
<h3 class="m-0 mb-1">Affectation des Realms</h3>
<p class="text-600 m-0">Gérer les permissions d'administration par realm (contrôle multi-tenant)</p>
</div>
</div>
<fr:commandButton value="Nouvelle Affectation"
icon="pi pi-plus"
severity="success"
onclick="PF('assignRealmDialog').show();"
type="button" />
</div>
</div>
</div>
<!-- ================================================================
STATISTIQUES
================================================================ -->
<div class="col-12 md:col-6 lg:col-4">
<div class="card">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Total Affectations</div>
<div class="text-900 font-bold text-2xl">#{realmAssignmentBean.totalAssignments}</div>
</div>
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-sitemap text-blue-600 text-xl"></i>
</div>
</div>
<small class="text-500">Assignations configurées</small>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="card">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Affectations Actives</div>
<div class="text-900 font-bold text-2xl">#{realmAssignmentBean.activeAssignmentsCount}</div>
</div>
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-check-circle text-green-600 text-xl"></i>
</div>
</div>
<small class="text-500">En cours de validité</small>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="card">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Super Admins</div>
<div class="text-900 font-bold text-2xl">#{realmAssignmentBean.superAdminsCount}</div>
</div>
<div class="flex align-items-center justify-content-center bg-orange-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-star text-orange-600 text-xl"></i>
</div>
</div>
<small class="text-500">Peuvent gérer tous les realms</small>
</div>
</div>
<!-- ================================================================
TABLEAU DES AFFECTATIONS
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between mb-4">
<h5 class="m-0">Affectations Actuelles</h5>
<fr:commandButton value="Rafraîchir"
icon="pi pi-refresh"
outlined="true"
size="small"
action="#{realmAssignmentBean.loadAssignments}"
update=":formRealmAssignments" />
</div>
<fr:message id="messages" showDetail="true" closable="true">
<p:autoUpdate />
</fr:message>
<p:dataTable id="assignmentsTable"
value="#{realmAssignmentBean.assignments}"
var="assignment"
paginator="true"
rows="25"
paginatorPosition="bottom"
paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
rowsPerPageTemplate="10,25,50,100"
emptyMessage="Aucune affectation configurée"
responsiveLayout="scroll"
styleClass="p-datatable-sm">
<!-- Colonne Utilisateur -->
<p:column headerText="Utilisateur" sortBy="#{assignment.username}" filterBy="#{assignment.username}" filterMatchMode="contains" priority="1">
<div class="flex align-items-center gap-2">
<div style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600)); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: bold; color: white;">
<h:outputText value="#{assignment.username != null and assignment.username.length() >= 2 ? assignment.username.substring(0,2).toUpperCase() : 'U'}" />
</div>
<div>
<div class="text-900 font-semibold">#{assignment.username}</div>
<small class="text-500">#{assignment.email}</small>
</div>
</div>
</p:column>
<!-- Colonne Realm -->
<p:column headerText="Realm" sortBy="#{assignment.realmName}" filterBy="#{assignment.realmName}" filterMatchMode="contains" priority="2">
<fr:tag value="#{assignment.realmName}"
severity="info"
icon="pi pi-globe" />
</p:column>
<!-- Colonne Type -->
<p:column headerText="Type" style="width: 150px" priority="3">
<fr:tag value="Super Admin"
severity="danger"
icon="pi pi-star"
rendered="#{assignment.isSuperAdmin()}" />
<fr:tag value="Realm Admin"
severity="success"
icon="pi pi-shield"
rendered="#{!assignment.isSuperAdmin()}" />
</p:column>
<!-- Colonne Statut -->
<p:column headerText="Statut" style="width: 120px" priority="4">
<fr:tag value="Actif"
severity="success"
icon="pi pi-check-circle"
rendered="#{assignment.active and !assignment.isExpired()}" />
<fr:tag value="Inactif"
severity="warning"
icon="pi pi-times-circle"
rendered="#{!assignment.active}" />
<fr:tag value="Expiré"
severity="danger"
icon="pi pi-exclamation-circle"
rendered="#{assignment.isExpired()}" />
</p:column>
<!-- Colonne Assigné le -->
<p:column headerText="Assigné le" sortBy="#{assignment.assignedAt}" style="width: 180px" priority="5">
<h:outputText value="#{assignment.assignedAt}">
<f:convertDateTime pattern="dd/MM/yyyy HH:mm" />
</h:outputText>
</p:column>
<!-- Colonne Par -->
<p:column headerText="Par" sortBy="#{assignment.assignedBy}" style="width: 150px" priority="6">
<h:outputText value="#{assignment.assignedBy}" />
</p:column>
<!-- Colonne Actions -->
<p:column headerText="Actions" style="width: 120px; text-align: center" priority="1">
<div class="flex gap-1 justify-content-center flex-wrap">
<!-- Bouton Désactiver -->
<fr:commandButton icon="pi pi-ban"
rounded="true"
text="true"
size="small"
severity="warning"
title="Désactiver"
action="#{realmAssignmentBean.deactivateAssignment(assignment)}"
update=":formRealmAssignments"
process="@this"
rendered="#{assignment.active}">
<p:confirm header="Confirmation"
message="Désactiver cette affectation ?"
icon="pi pi-exclamation-triangle" />
</fr:commandButton>
<!-- Bouton Activer -->
<fr:commandButton icon="pi pi-check"
rounded="true"
text="true"
size="small"
severity="success"
title="Activer"
action="#{realmAssignmentBean.activateAssignment(assignment)}"
update=":formRealmAssignments"
process="@this"
rendered="#{!assignment.active}">
<p:confirm header="Confirmation"
message="Activer cette affectation ?"
icon="pi pi-question-circle" />
</fr:commandButton>
<!-- Bouton Supprimer -->
<fr:commandButton icon="pi pi-trash"
rounded="true"
text="true"
size="small"
severity="danger"
title="Supprimer"
action="#{realmAssignmentBean.revokeAssignment(assignment)}"
update=":formRealmAssignments"
process="@this">
<p:confirm header="Confirmation"
message="Révoquer l'accès de #{assignment.username} au realm #{assignment.realmName} ?"
icon="pi pi-exclamation-triangle" />
</fr:commandButton>
</div>
</p:column>
</p:dataTable>
</div>
</div>
</div>
</h:form>
<!-- ================================================================
DIALOG D'AFFECTATION
================================================================ -->
<p:dialog header="Assigner un Realm à un Utilisateur"
widgetVar="assignRealmDialog"
modal="true"
responsive="true"
width="600"
showEffect="fade"
hideEffect="fade">
<h:form id="formAssignRealm">
<div class="grid">
<div class="col-12">
<fr:fieldSelect id="userId"
label="Utilisateur *"
value="#{realmAssignmentBean.selectedUserId}"
filter="true"
filterMatchMode="contains"
iconLeft="pi pi-user">
<f:selectItem itemLabel="Sélectionner un utilisateur" itemValue="" noSelectionOption="true" />
<f:selectItems value="#{realmAssignmentBean.availableUsers}"
var="user"
itemValue="#{user.id}"
itemLabel="#{user.username} (#{user.email})" />
</fr:fieldSelect>
</div>
<div class="col-12">
<fr:fieldSelect id="realmName"
label="Realm *"
value="#{realmAssignmentBean.selectedRealmName}"
iconLeft="pi pi-globe">
<f:selectItem itemLabel="Sélectionner un realm" itemValue="" noSelectionOption="true" />
<f:selectItems value="#{realmAssignmentBean.availableRealms}" />
</fr:fieldSelect>
</div>
<div class="col-12">
<fr:fieldInput id="raison"
label="Raison"
value="#{realmAssignmentBean.newAssignment.raison}"
placeholder="Ex: Nouveau gestionnaire du realm client"
iconLeft="pi pi-comment" />
</div>
<div class="col-12">
<fr:fieldTextarea id="commentaires"
label="Commentaires"
value="#{realmAssignmentBean.newAssignment.commentaires}"
rows="3"
placeholder="Commentaires administratifs (optionnel)"
iconLeft="pi pi-file-edit" />
</div>
<div class="col-12">
<fr:fieldCheckbox id="temporaire"
label="Affectation temporaire"
value="#{realmAssignmentBean.newAssignment.temporaire}" />
</div>
<div class="col-12" rendered="#{realmAssignmentBean.newAssignment.temporaire}">
<fr:fieldCalendar id="dateExpiration"
label="Date d'expiration"
value="#{realmAssignmentBean.newAssignment.dateExpiration}"
pattern="dd/MM/yyyy HH:mm"
showTime="true" />
</div>
<div class="col-12">
<div class="surface-100 border-round p-3">
<div class="flex align-items-center gap-2">
<i class="pi pi-info-circle text-blue-500"></i>
<div>
<div class="text-700 font-semibold text-sm">Information</div>
<small class="text-600">
L'utilisateur pourra administrer uniquement le realm assigné.
Pour accorder l'accès à tous les realms, utilisez le statut Super Admin.
</small>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="flex gap-2">
<fr:commandButton value="Annuler"
icon="pi pi-times"
text="true"
styleClass="flex-1"
onclick="PF('assignRealmDialog').hide();"
type="button"
action="#{realmAssignmentBean.resetForm}" />
<fr:commandButton value="Assigner"
icon="pi pi-check"
severity="success"
styleClass="flex-1"
action="#{realmAssignmentBean.assignRealm}"
update=":formRealmAssignments :formAssignRealm"
oncomplete="if (args.validationFailed == false) PF('assignRealmDialog').hide();" />
</div>
</div>
</div>
</h:form>
</p:dialog>
<!-- ================================================================
DIALOG DE CONFIRMATION (Freya Extension)
================================================================ -->
<!-- Le confirmDialog est géré par p:confirm dans les boutons d'action -->
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade" responsive="true" width="400">
<fr:commandButton value="Non" type="button" text="true" icon="pi pi-times" />
<fr:commandButton value="Oui" type="button" severity="danger" icon="pi pi-check" />
</p:confirmDialog>
</ui:define>
</ui:composition>

View File

@@ -1,179 +1,353 @@
<!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="#{auditConsultationBean}"/>
<ui:param name="page" value="#{auditConsultationBean}" />
<ui:define name="title">Journal d'Audit - Lions User Manager</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-history text-orange-500" />
<ui:param name="title" value="Journal d'Audit" />
<ui:param name="description" value="Consultation des logs d'audit et statistiques" />
<ui:define name="actions">
<h:form id="formActionsAudit">
<div class="flex gap-2">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Exporter CSV" />
<ui:param name="icon" value="pi pi-download" />
<ui:param name="action" value="#{auditConsultationBean.exportToCSV}" />
<ui:param name="severity" value="success" />
<div class="grid">
<!-- En-tête -->
<div class="col-12">
<ui:decorate template="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-history text-orange-500" />
<ui:param name="title" value="Journal d'Audit" />
<ui:param name="description" value="Consultation des logs d'audit et statistiques" />
<ui:define name="actions">
<h:form id="formActionsAudit">
<div class="flex gap-2">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Exporter CSV" />
<ui:param name="icon" value="pi pi-download" />
<ui:param name="hasAction" value="true" />
<ui:param name="action" value="#{auditConsultationBean.exportToCSV}" />
<ui:param name="severity" value="success" />
</ui:include>
</div>
</h:form>
</ui:define>
</ui:decorate>
</div>
<!-- Statistiques -->
<div class="col-12">
<ui:decorate template="/templates/components/shared/dashboard/kpi-group.xhtml">
<ui:param name="title" value="Statistiques d'Audit" />
<ui:param name="columns" value="4" />
<ui:define name="kpi-content">
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Actions" />
<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>
</div>
</h:form>
</ui:define>
</ui:include>
<!-- Statistiques -->
<div class="grid mb-4">
<div class="col-12 md:col-3">
<ui:include src="/templates/components/audit/audit-stats-card.xhtml">
<ui:param name="title" value="Total Actions" />
<ui:param name="value" value="#{auditConsultationBean.totalRecords}" />
<ui:param name="icon" value="pi-history" />
<ui:param name="iconColor" value="blue-600" />
</ui:include>
</div>
<div class="col-12 md:col-3">
<ui:include src="/templates/components/audit/audit-stats-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:include>
</div>
<div class="col-12 md:col-3">
<ui:include src="/templates/components/audit/audit-stats-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:include>
</div>
<div class="col-12 md:col-3">
<ui:include src="/templates/components/audit/audit-stats-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="icon" value="pi pi-percentage" />
<ui:param name="iconColor" value="purple-600" />
</ui:include>
</div>
</div>
<!-- Filtres de recherche -->
<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="acteurFilter" value="Acteur" />
<p:inputText id="acteurFilter"
value="#{auditConsultationBean.acteurUsername}"
placeholder="Nom d'utilisateur..."
styleClass="w-full" />
<p:outputLabel for="typeActionFilter" value="Type d'action" />
<p:selectOneMenu id="typeActionFilter"
value="#{auditConsultationBean.selectedTypeAction}"
styleClass="w-full">
<f:selectItem itemLabel="Tous les types" itemValue="" />
<f:selectItems value="#{auditConsultationBean.typeActionOptions}" />
</p:selectOneMenu>
<p:outputLabel for="succesFilter" value="Résultat" />
<p:selectOneMenu id="succesFilter"
value="#{auditConsultationBean.succes}"
styleClass="w-full">
<f:selectItem itemLabel="Tous" itemValue="" />
<f:selectItem itemLabel="Succès" itemValue="true" />
<f:selectItem itemLabel="Échec" itemValue="false" />
</p:selectOneMenu>
<p:outputLabel for="dateDebutFilter" value="Date début" />
<p:calendar id="dateDebutFilter"
value="#{auditConsultationBean.dateDebut}"
pattern="dd/MM/yyyy"
styleClass="w-full" />
<p:outputLabel for="dateFinFilter" value="Date fin" />
<p:calendar id="dateFinFilter"
value="#{auditConsultationBean.dateFin}"
pattern="dd/MM/yyyy"
styleClass="w-full" />
<p:outputLabel for="ressourceFilter" value="Type ressource" />
<p:inputText id="ressourceFilter"
value="#{auditConsultationBean.ressourceType}"
placeholder="USER, ROLE..."
styleClass="w-full" />
</p:panelGrid>
<div class="flex gap-2 justify-content-end mt-3">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Rechercher" />
<ui:param name="icon" value="pi pi-search" />
<ui:param name="action" value="#{auditConsultationBean.searchLogs}" />
<ui:param name="update" value="auditLogsList" />
<ui:param name="severity" value="primary" />
</ui:include>
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Réinitialiser" />
<ui:param name="icon" value="pi pi-refresh" />
<ui:param name="action" value="#{auditConsultationBean.resetFilters}" />
<ui:param name="update" value="auditLogsList formFilters" />
<ui:param name="severity" value="secondary" />
</ui:include>
</div>
</h:form>
</div>
<!-- Liste des logs -->
<div class="card">
<h:form id="formAuditLogs">
<h5>Logs d'Audit</h5>
<div id="auditLogsList" class="flex flex-column gap-2">
<c:forEach var="log" items="#{auditConsultationBean.auditLogs}">
<ui:include src="/templates/components/audit/audit-log-row.xhtml">
<ui:param name="auditLog" value="#{log}" />
<ui:param name="showDetails" value="true" />
<ui:param name="showActions" value="false" />
<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>
</c:forEach>
<c:if test="#{empty auditConsultationBean.auditLogs}">
<p class="text-center text-color-secondary">Aucun log d'audit trouvé</p>
</c:if>
</div>
<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.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>
</div>
<!-- Pagination -->
<div class="flex justify-content-between align-items-center mt-3">
<span class="text-600">
Affichage de #{auditConsultationBean.currentPage * auditConsultationBean.pageSize + 1}
à #{auditConsultationBean.currentPage * auditConsultationBean.pageSize + auditConsultationBean.auditLogs.size()}
sur #{auditConsultationBean.totalRecords}
</span>
<div class="flex gap-2">
<p:commandButton
value="Précédent"
icon="pi pi-arrow-left"
disabled="#{auditConsultationBean.currentPage == 0}"
action="#{auditConsultationBean.currentPage = auditConsultationBean.currentPage - 1; auditConsultationBean.searchLogs()}"
update="auditLogsList" />
<p:commandButton
value="Suivant"
icon="pi pi-arrow-right"
iconPos="right"
disabled="#{(auditConsultationBean.currentPage + 1) * auditConsultationBean.pageSize >= auditConsultationBean.totalRecords}"
action="#{auditConsultationBean.currentPage = auditConsultationBean.currentPage + 1; auditConsultationBean.searchLogs()}"
update="auditLogsList" />
</div>
<!-- Filtres de recherche -->
<div class="col-12">
<div class="card">
<h:form id="formFilters">
<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" />
<p:inputText id="acteurFilter" value="#{auditConsultationBean.acteurUsername}"
placeholder="Nom d'utilisateur..." />
</div>
<div class="col-12 md:col-4 field">
<p:outputLabel for="typeActionFilter" value="Type d'action" />
<p:selectOneMenu id="typeActionFilter"
value="#{auditConsultationBean.selectedTypeAction}">
<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="#{null}" />
<f:selectItem itemLabel="Succès" itemValue="true" />
<f:selectItem itemLabel="Échec" itemValue="false" />
</p:selectOneMenu>
</div>
<div class="col-12 md:col-4 field">
<p:outputLabel for="dateDebutFilter" value="Date début" />
<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: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">
<p:outputLabel for="ressourceFilter" value="Type ressource" />
<p:inputText id="ressourceFilter" value="#{auditConsultationBean.ressourceType}"
placeholder="USER, ROLE..." />
</div>
</div>
<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 :growlMessages" />
<p:commandButton value="Réinitialiser" icon="pi pi-refresh"
styleClass="p-button-secondary p-button-outlined"
action="#{auditConsultationBean.resetFilters}"
update=":formAuditLogs:auditLogsTable @form :growlMessages" />
</div>
</h:form>
</div>
</h:form>
</div>
<!-- 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">
<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"
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: 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}" filterBy="#{log.typeAction}"
filterMatchMode="contains" style="width: 15%">
<span class="font-semibold text-900">#{log.typeAction}</span>
</p:column>
<!-- Colonne Acteur -->
<p:column headerText="Acteur" sortBy="#{log.acteurUsername}" style="width: 15%">
<div class="flex align-items-center gap-2">
<i class="pi pi-user text-color-secondary"></i>
<span>#{log.acteurUsername}</span>
</div>
</p:column>
<!-- Colonne Ressource -->
<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: 14%">
<div class="flex align-items-center gap-2">
<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 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>
</h:panelGroup>
</p:column>
<!-- Colonne IP -->
<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>
</h:panelGroup>
</p:column>
<!-- Colonne Actions -->
<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}" />
</p:commandButton>
</p:column>
</p:dataTable>
</h:form>
</div>
</div>
</div>
<!-- Dialog de détails -->
<p:dialog id="auditLogDetailsDialog" widgetVar="auditLogDetailsDialog" header="Détails du Log d'Audit"
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>
<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>
<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>
<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>
<!-- 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>
</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>
</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>

View File

@@ -1,160 +1,146 @@
<!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"
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" template="/templates/main-template.xhtml">
<ui:define name="title">Tableau de Bord - Lions User Manager</ui:define>
<ui:define name="content">
<div class="grid">
<!-- En-tête -->
<div class="col-12">
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-home text-blue-500" />
<ui:param name="title" value="Tableau de Bord" />
<ui:param name="description" value="Vue d'ensemble de la gestion des utilisateurs Keycloak" />
</ui:include>
</div>
<!-- KPIs Principaux -->
<div class="col-12">
<div class="grid">
<!-- KPI 1: Utilisateurs Actifs -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Utilisateurs Actifs" />
<ui:param name="value" value="#{empty dashboardBean ? '-' : dashboardBean.totalUsers}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="subtitle" value="Total utilisateurs" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/users/list" />
</ui:include>
<!-- KPI 2: Rôles Realm -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Rôles Realm" />
<ui:param name="value" value="#{empty dashboardBean ? '-' : dashboardBean.totalRoles}" />
<ui:param name="icon" value="pi-shield" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="subtitle" value="Rôles configurés" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/roles/list" />
</ui:include>
<!-- KPI 3: Actions Récentes -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Actions Récentes" />
<ui:param name="value" value="#{empty dashboardBean ? '-' : dashboardBean.recentActions}" />
<ui:param name="icon" value="pi-history" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="subtitle" value="Dernières 24h" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/audit/logs" />
</ui:include>
<!-- KPI 4: Sessions Actives -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Sessions Actives" />
<ui:param name="value" value="#{empty dashboardBean ? '-' : dashboardBean.activeSessions}" />
<ui:param name="icon" value="pi-sign-in" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="subtitle" value="Utilisateurs connectés" />
<ui:param name="statusIcon" value="pi-check-circle" />
<ui:param name="statusLabel" value="En ligne" />
<ui:param name="statusValue" value="#{empty dashboardBean ? '0' : dashboardBean.onlineUsers} actifs" />
</ui:include>
<p:outputPanel id="dashboardPanel">
<div class="grid">
<!-- En-tête -->
<div class="col-12">
<ui:decorate template="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-home text-blue-500" />
<ui:param name="title" value="Tableau de Bord" />
<ui:param name="description" value="Vue d'ensemble de la gestion des utilisateurs Keycloak" />
<ui:define name="actions">
<h:form id="formRefresh">
<p:commandButton value="Rafraîchir" icon="pi pi-refresh" styleClass="p-button-secondary"
action="#{dashboardBean.refreshStatistics}" update=":dashboardPanel" />
</h:form>
</ui:define>
</ui:decorate>
</div>
</div>
<!-- Actions Rapides -->
<ui:include src="/templates/components/shared/dashboard/dashboard-section.xhtml">
<ui:param name="title" value="Actions Rapides" />
<ui:param name="icon" value="pi-bolt" />
<ui:param name="colSize" value="col-12 lg:col-6" />
<ui:define name="section-content">
<div class="grid">
<div class="col-12 md:col-6">
<h:form>
<p:commandButton
value="Nouvel Utilisateur"
icon="pi pi-user-plus"
styleClass="w-full p-button-success"
outcome="/pages/user-manager/users/create" />
</h:form>
</div>
<div class="col-12 md:col-6">
<h:form>
<p:commandButton
value="Liste des Utilisateurs"
icon="pi pi-users"
styleClass="w-full p-button-primary"
outcome="/pages/user-manager/users/list" />
</h:form>
</div>
<div class="col-12 md:col-6">
<h:form>
<p:commandButton
value="Gestion des Rôles"
icon="pi pi-shield"
styleClass="w-full p-button-info"
outcome="/pages/user-manager/roles/list" />
</h:form>
</div>
<div class="col-12 md:col-6">
<h:form>
<p:commandButton
value="Journal d'Audit"
icon="pi pi-history"
styleClass="w-full p-button-help"
<!-- KPIs Principaux avec composant réutilisable -->
<div class="col-12">
<ui:decorate template="/templates/components/shared/dashboard/kpi-group.xhtml">
<ui:param name="title" value="Statistiques Principales" />
<ui:param name="columns" value="4" />
<ui:define name="kpi-content">
<!-- KPI 1: Utilisateurs Actifs -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Utilisateurs Actifs" />
<ui:param name="value" value="#{dashboardBean.totalUsersDisplay}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="subtitle" value="Total utilisateurs" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/users/list" />
</ui:include>
<!-- KPI 2: Rôles Realm -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Rôles Realm" />
<ui:param name="value" value="#{dashboardBean.totalRolesDisplay}" />
<ui:param name="icon" value="pi-shield" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="subtitle" value="Rôles configurés" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/roles/list" />
</ui:include>
<!-- KPI 3: Actions Récentes -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Actions Récentes" />
<ui:param name="value" value="#{dashboardBean.recentActionsDisplay}" />
<ui:param name="icon" value="pi-history" />
<ui:param name="iconColor" value="orange-600" />
<ui:param name="subtitle" value="Dernières 24h" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/audit/logs" />
</ui:include>
<!-- KPI 4: Sessions Actives -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<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="#{dashboardBean.onlineUsersDisplay} utilisateur(s) en ligne" />
</ui:include>
</ui:define>
</ui:decorate>
</div>
<!-- Actions Rapides -->
<ui:decorate template="/templates/components/shared/dashboard/dashboard-section.xhtml">
<ui:param name="title" value="Actions Rapides" />
<ui:param name="icon" value="pi-bolt" />
<ui:param name="colSize" value="col-12 lg:col-6" />
<ui:define name="section-content">
<div class="grid">
<div class="col-12 md:col-6">
<p:button value="Nouvel Utilisateur" icon="pi pi-user-plus"
styleClass="w-full p-button-success" outcome="/pages/user-manager/users/create" />
</div>
<div class="col-12 md:col-6">
<p:button value="Liste des Utilisateurs" icon="pi pi-users"
styleClass="w-full p-button-primary" outcome="/pages/user-manager/users/list" />
</div>
<div class="col-12 md:col-6">
<p:button value="Gestion des Rôles" icon="pi pi-shield"
styleClass="w-full p-button-info" outcome="/pages/user-manager/roles/list" />
</div>
<div class="col-12 md:col-6">
<p:button value="Journal d'Audit" icon="pi pi-history" styleClass="w-full p-button-help"
outcome="/pages/user-manager/audit/logs" />
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:include>
<!-- Informations Système -->
<ui:include src="/templates/components/shared/dashboard/dashboard-section.xhtml">
<ui:param name="title" value="Informations Système" />
<ui:param name="icon" value="pi-info-circle" />
<ui:param name="colSize" value="col-12 lg:col-6" />
<ui:define name="section-content">
<div class="flex flex-column gap-2">
<div class="flex align-items-center justify-content-between">
<span class="text-600">Version</span>
<span class="font-semibold">1.0.0</span>
</ui:define>
</ui:decorate>
<!-- Informations Système -->
<ui:decorate template="/templates/components/shared/dashboard/dashboard-section.xhtml">
<ui:param name="title" value="Informations Système" />
<ui:param name="icon" value="pi-info-circle" />
<ui:param name="colSize" value="col-12 lg:col-6" />
<ui:define name="section-content">
<div class="flex flex-column gap-2">
<div class="flex align-items-center justify-content-between">
<span class="text-600">Version</span>
<span class="font-semibold">1.0.0</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Realm Keycloak</span>
<span class="font-semibold">#{dashboardBean.realmName}</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Statut</span>
<p:tag value="Opérationnel" severity="success" />
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Application</span>
<span class="font-semibold">Lions User Manager</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Environnement</span>
<span class="font-semibold">Développement</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Base de données</span>
<span class="font-semibold">Keycloak Admin API</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Framework</span>
<span class="font-semibold">Quarkus, PrimeFaces Freya</span>
</div>
</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>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Statut</span>
<p:tag value="Opérationnel" severity="success" />
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Application</span>
<span class="font-semibold">Lions User Manager</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Environnement</span>
<span class="font-semibold">Développement</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Base de données</span>
<span class="font-semibold">Keycloak Admin API</span>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Framework</span>
<span class="font-semibold">Quarkus, PrimeFaces Freya</span>
</div>
</div>
</ui:define>
</ui:include>
</div>
</ui:define>
</ui:decorate>
</div>
</p:outputPanel>
</ui:define>
</ui:composition>
</ui:composition>

View File

@@ -0,0 +1,918 @@
<!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:fr="http://primefaces.org/freya"
template="/templates/main-template.xhtml">
<ui:define name="title">Freya Extension Showcase - Lions User Manager</ui:define>
<ui:define name="content">
<h:form id="formFreyaShowcase">
<div class="grid">
<!-- ================================================================
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-3">
<i class="pi pi-palette text-purple-500" style="font-size: 3rem"></i>
<div>
<h1 class="m-0 mb-2">PrimeFaces Freya Extension Showcase</h1>
<p class="text-600 m-0 mb-1">Démonstration complète des 46 composants personnalisés</p>
<div class="flex gap-2 align-items-center">
<fr:tag value="46 Composants" severity="success" icon="pi pi-check" />
<fr:tag value="Version 1.0.0-SNAPSHOT" severity="info" />
<fr:tag value="Intégration Lions User Manager" severity="warning" />
</div>
</div>
</div>
<fr:commandButton
value="Retour Dashboard"
icon="pi pi-home"
severity="secondary"
outcome="/pages/user-manager/dashboard" />
</div>
</div>
</div>
<!-- ================================================================
SECTION: COMPOSANTS DE FORMULAIRE (FIELD PATTERN) - 20 composants
================================================================ -->
<div class="col-12">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-inbox text-blue-500"></i>
Composants de Formulaire (Field Pattern)
<fr:tag value="20 composants" severity="info" />
</h2>
</div>
<!-- fieldInput -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldInput</h5>
<p class="text-600 mb-3">Champ de saisie texte avec label et message de validation</p>
<fr:fieldInput id="nomUtilisateur"
label="Nom complet"
value="#{demoBean.user.nom}"
required="true"
placeholder="Ex: Jean Dupont" />
</div>
</div>
<!-- fieldPassword -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldPassword</h5>
<p class="text-600 mb-3">Champ mot de passe avec validation de force</p>
<fr:fieldPassword id="motDePasse"
label="Mot de passe"
value="#{demoBean.user.password}"
required="true"
feedback="true"
promptLabel="Entrez un mot de passe"
weakLabel="Faible"
goodLabel="Moyen"
strongLabel="Fort" />
</div>
</div>
<!-- fieldTextarea -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldTextarea</h5>
<p class="text-600 mb-3">Zone de texte multiligne pour descriptions</p>
<fr:fieldTextarea id="description"
label="Description"
value="#{demoBean.user.description}"
rows="4"
maxlength="500"
placeholder="Entrez une description détaillée..." />
</div>
</div>
<!-- fieldNumber -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldNumber</h5>
<p class="text-600 mb-3">Champ numérique avec contrôles + / -</p>
<fr:fieldNumber id="age"
label="Âge"
value="#{demoBean.user.age}"
minValue="0"
maxValue="150"
showButtons="true" />
</div>
</div>
<!-- fieldCalendar -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldCalendar</h5>
<p class="text-600 mb-3">Sélecteur de date avec calendrier popup</p>
<fr:fieldCalendar id="dateNaissance"
label="Date de naissance"
value="#{demoBean.user.dateNaissance}"
showIcon="true"
yearRange="1900:2024"
navigator="true" />
</div>
</div>
<!-- fieldSelect -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldSelect</h5>
<p class="text-600 mb-3">Liste déroulante simple sélection</p>
<fr:fieldSelect id="pays"
label="Pays"
value="#{demoBean.user.pays}">
<f:selectItem itemLabel="Sélectionnez un pays" itemValue="" />
<f:selectItem itemLabel="France" itemValue="FR" />
<f:selectItem itemLabel="Belgique" itemValue="BE" />
<f:selectItem itemLabel="Canada" itemValue="CA" />
<f:selectItem itemLabel="Suisse" itemValue="CH" />
</fr:fieldSelect>
</div>
</div>
<!-- fieldMultiSelect -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldMultiSelect</h5>
<p class="text-600 mb-3">Sélection multiple avec chips</p>
<fr:fieldMultiSelect id="competences"
label="Compétences"
value="#{demoBean.user.competences}">
<f:selectItem itemLabel="Java" itemValue="java" />
<f:selectItem itemLabel="JavaScript" itemValue="js" />
<f:selectItem itemLabel="Python" itemValue="python" />
<f:selectItem itemLabel="React" itemValue="react" />
<f:selectItem itemLabel="Angular" itemValue="angular" />
</fr:fieldMultiSelect>
</div>
</div>
<!-- fieldCheckbox -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldCheckbox</h5>
<p class="text-600 mb-3">Case à cocher pour valeurs booléennes</p>
<fr:fieldCheckbox id="actif"
label="Utilisateur actif"
value="#{demoBean.user.actif}"
checkboxLabel="Compte activé" />
</div>
</div>
<!-- fieldRadio -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldRadio</h5>
<p class="text-600 mb-3">Boutons radio pour choix exclusif</p>
<fr:fieldRadio id="genre"
label="Genre"
value="#{demoBean.user.genre}"
layout="grid">
<f:selectItem itemLabel="Homme" itemValue="M" />
<f:selectItem itemLabel="Femme" itemValue="F" />
<f:selectItem itemLabel="Autre" itemValue="O" />
</fr:fieldRadio>
</div>
</div>
<!-- fieldSwitch -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldSwitch</h5>
<p class="text-600 mb-3">Interrupteur on/off moderne</p>
<fr:fieldSwitch id="newsletter"
label="Notifications"
value="#{demoBean.user.newsletter}"
switchLabel="Recevoir la newsletter" />
</div>
</div>
<!-- fieldToggle -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldToggle</h5>
<p class="text-600 mb-3">Bouton toggle avec états on/off</p>
<fr:fieldToggle id="modeNuit"
label="Préférences d'affichage"
value="#{demoBean.user.modeNuit}"
onLabel="Mode Nuit"
offLabel="Mode Jour"
onIcon="pi pi-moon"
offIcon="pi pi-sun" />
</div>
</div>
<!-- fieldSlider -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldSlider</h5>
<p class="text-600 mb-3">Curseur pour sélection de valeur numérique</p>
<fr:fieldSlider id="volume"
label="Volume"
value="#{demoBean.user.volume}"
minValue="0"
maxValue="100"
step="5" />
<p class="text-600 text-sm mt-2">Valeur sélectionnée: #{demoBean.user.volume != null ? demoBean.user.volume : 50}%</p>
</div>
</div>
<!-- fieldRating -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldRating</h5>
<p class="text-600 mb-3">Évaluation par étoiles</p>
<fr:fieldRating id="satisfaction"
label="Satisfaction"
value="#{demoBean.user.rating}"
stars="5"
cancel="true" />
</div>
</div>
<!-- fieldChips -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldChips</h5>
<p class="text-600 mb-3">Saisie de tags/mots-clés multiples</p>
<fr:fieldChips id="tags"
label="Tags"
value="#{demoBean.user.tags}"
placeholder="Entrez un tag et appuyez sur Entrée" />
</div>
</div>
<!-- fieldColor -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldColor</h5>
<p class="text-600 mb-3">Sélecteur de couleur avec palette</p>
<fr:fieldColor id="couleurPref"
label="Couleur préférée"
value="#{demoBean.user.couleur}" />
</div>
</div>
<!-- fieldEditor -->
<div class="col-12">
<div class="card component-card">
<h5>fr:fieldEditor</h5>
<p class="text-600 mb-3">Éditeur de texte riche WYSIWYG</p>
<fr:fieldEditor id="bio"
label="Biographie"
value="#{demoBean.user.bio}"
height="200" />
</div>
</div>
<!-- fieldMask -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldMask</h5>
<p class="text-600 mb-3">Champ avec masque de saisie (téléphone, etc.)</p>
<fr:fieldMask id="telephone"
label="Téléphone"
value="#{demoBean.user.telephone}"
mask="(999) 999-9999"
placeholder="(555) 123-4567" />
</div>
</div>
<!-- fieldAutoComplete -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldAutoComplete</h5>
<p class="text-600 mb-3">Saisie avec suggestions automatiques</p>
<fr:fieldAutoComplete id="ville"
label="Ville"
value="#{demoBean.user.ville}"
completeMethod="#{demoBean.completeCities}"
placeholder="Commencez à taper..." />
</div>
</div>
<!-- fieldSpinner -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldSpinner</h5>
<p class="text-600 mb-3">Compteur numérique avec incréments</p>
<fr:fieldSpinner id="quantite"
label="Quantité"
value="#{demoBean.user.quantite}"
min="0"
max="100"
step="5" />
</div>
</div>
<!-- fieldFileUpload -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:fieldFileUpload</h5>
<p class="text-600 mb-3">Upload de fichiers avec contraintes</p>
<fr:fieldFileUpload id="document"
label="Document"
mode="simple"
allowTypes="/(\.|\/)(pdf|doc|docx)$/"
fileLimit="3" />
<p class="text-600 text-sm mt-2">Formats acceptés: PDF, DOC, DOCX (max 3 fichiers)</p>
</div>
</div>
<!-- ================================================================
SECTION: COMPOSANTS D'ACTION - 5 composants
================================================================ -->
<div class="col-12 mt-5">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-bolt text-orange-500"></i>
Composants d'Action
<fr:tag value="5 composants" severity="warning" />
</h2>
</div>
<!-- button -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:button</h5>
<p class="text-600 mb-3">Bouton de navigation simple (sans Ajax)</p>
<div class="flex flex-wrap gap-2">
<fr:button value="Primary" severity="primary" icon="pi pi-check" />
<fr:button value="Secondary" severity="secondary" icon="pi pi-times" />
<fr:button value="Success" severity="success" icon="pi pi-check" />
<fr:button value="Info" severity="info" icon="pi pi-info-circle" />
<fr:button value="Warning" severity="warning" icon="pi pi-exclamation-triangle" />
<fr:button value="Danger" severity="danger" icon="pi pi-trash" />
<fr:button value="Help" severity="help" icon="pi pi-question-circle" />
</div>
</div>
</div>
<!-- commandButton -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:commandButton</h5>
<p class="text-600 mb-3">Bouton avec action Ajax</p>
<div class="flex flex-wrap gap-2">
<fr:commandButton value="Valider" severity="success" icon="pi pi-check" action="#{demoBean.saveAction}" />
<fr:commandButton value="Annuler" severity="secondary" icon="pi pi-times" action="#{demoBean.cancelAction}" />
<fr:commandButton value="Rafraîchir" severity="info" icon="pi pi-refresh" action="#{demoBean.refreshAction}" />
</div>
</div>
</div>
<!-- linkButton -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:linkButton</h5>
<p class="text-600 mb-3">Bouton sous forme de lien</p>
<div class="flex flex-wrap gap-2">
<fr:linkButton value="Lien Primary" severity="primary" icon="pi pi-external-link" />
<fr:linkButton value="Lien Secondary" severity="secondary" icon="pi pi-link" />
<fr:linkButton value="Lien Info" severity="info" icon="pi pi-book" />
</div>
</div>
</div>
<!-- splitButton -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:splitButton</h5>
<p class="text-600 mb-3">Bouton avec menu déroulant d'actions</p>
<fr:splitButton value="Enregistrer"
icon="pi pi-save"
severity="success"
action="#{demoBean.saveAction}">
<p:menuitem value="Enregistrer et Fermer" icon="pi pi-check" />
<p:menuitem value="Enregistrer comme..." icon="pi pi-save" />
<p:separator />
<p:menuitem value="Annuler" icon="pi pi-times" />
</fr:splitButton>
</div>
</div>
<!-- actionDialog -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:actionDialog</h5>
<p class="text-600 mb-3">Dialogue modal pour confirmation d'action</p>
<fr:commandButton value="Ouvrir Dialogue Action"
icon="pi pi-question-circle"
severity="warning"
onclick="PF('actionDialogDemo').show()"
type="button" />
<fr:actionDialog widgetVar="actionDialogDemo"
header="Confirmation d'action"
message="Êtes-vous sûr de vouloir effectuer cette action?"
icon="pi pi-exclamation-triangle"
confirmLabel="Confirmer"
cancelLabel="Annuler"
confirmAction="#{demoBean.confirmAction}" />
</div>
</div>
<!-- ================================================================
SECTION: COMPOSANTS DE LAYOUT - 4 composants
================================================================ -->
<div class="col-12 mt-5">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-th-large text-green-500"></i>
Composants de Layout
<fr:tag value="4 composants" severity="success" />
</h2>
</div>
<!-- card -->
<div class="col-12 md:col-6">
<fr:card header="fr:card"
subheader="Carte conteneur avec en-tête et pied">
<p class="text-600">
Ceci est le contenu de la carte. Les cartes sont utilisées pour regrouper
des informations connexes dans un conteneur visuellement distinct.
</p>
<f:facet name="footer">
<div class="flex justify-content-end gap-2">
<fr:commandButton value="Action 1" severity="secondary" />
<fr:commandButton value="Action 2" severity="primary" />
</div>
</f:facet>
</fr:card>
</div>
<!-- panel -->
<div class="col-12 md:col-6">
<fr:panel header="fr:panel"
toggleable="true"
collapsed="false">
<p class="text-600">
Panel pliable/dépliable pour organiser le contenu en sections.
Cliquez sur l'icône pour replier/déplier.
</p>
</fr:panel>
</div>
<!-- divider -->
<div class="col-12">
<div class="card component-card">
<h5>fr:divider</h5>
<p class="text-600 mb-3">Séparateur visuel horizontal ou vertical</p>
<div class="mb-3">
<p>Contenu avant le divider</p>
<fr:divider />
<p>Contenu après le divider</p>
</div>
<fr:divider align="left">
<span class="text-600 font-bold">Divider avec texte aligné à gauche</span>
</fr:divider>
<fr:divider align="center">
<fr:tag value="Section Centrale" severity="info" />
</fr:divider>
</div>
</div>
<!-- spacer -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:spacer</h5>
<p class="text-600 mb-3">Espace vide pour ajuster la mise en page</p>
<div class="flex align-items-center">
<span>Élément gauche</span>
<fr:spacer />
<span>Élément droite</span>
</div>
</div>
</div>
<!-- ================================================================
SECTION: COMPOSANTS DE NAVIGATION - 3 composants
================================================================ -->
<div class="col-12 mt-5">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-map text-indigo-500"></i>
Composants de Navigation
<fr:tag value="3 composants" severity="help" />
</h2>
</div>
<!-- breadcrumb -->
<div class="col-12">
<div class="card component-card">
<h5>fr:breadcrumb</h5>
<p class="text-600 mb-3">Fil d'Ariane pour navigation hiérarchique</p>
<fr:breadcrumb>
<p:menuitem value="Accueil" icon="pi pi-home" url="#" />
<p:menuitem value="Users" url="#" />
<p:menuitem value="Liste" url="#" />
</fr:breadcrumb>
</div>
</div>
<!-- steps -->
<div class="col-12">
<div class="card component-card">
<h5>fr:steps</h5>
<p class="text-600 mb-3">Indicateur de progression par étapes</p>
<fr:steps activeIndex="1">
<p:menuitem value="Informations Personnelles" icon="pi pi-user" />
<p:menuitem value="Coordonnées" icon="pi pi-map-marker" />
<p:menuitem value="Confirmation" icon="pi pi-check" />
</fr:steps>
</div>
</div>
<!-- tabView -->
<div class="col-12">
<fr:tabView>
<p:tab title="fr:tabView - Onglet 1" icon="pi pi-calendar">
<p class="text-600">
Contenu du premier onglet. Les onglets permettent d'organiser le contenu en sections
accessibles via des onglets cliquables.
</p>
</p:tab>
<p:tab title="Onglet 2" icon="pi pi-user">
<p class="text-600">
Contenu du deuxième onglet avec informations différentes.
</p>
</p:tab>
<p:tab title="Onglet 3" icon="pi pi-cog">
<p class="text-600">
Troisième onglet pour démonstration complète.
</p>
</p:tab>
</fr:tabView>
</div>
<!-- ================================================================
SECTION: COMPOSANTS DE DONNÉES - 5 composants
================================================================ -->
<div class="col-12 mt-5">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-table text-cyan-500"></i>
Composants de Données
<fr:tag value="5 composants" severity="info" />
</h2>
</div>
<!-- dataTable -->
<div class="col-12">
<div class="card component-card">
<h5>fr:dataTable</h5>
<p class="text-600 mb-3">Tableau de données avec tri, filtrage et pagination</p>
<fr:dataTable value="#{demoBean.sampleUsers}"
var="user"
paginator="true"
rows="5">
<p:column headerText="Nom" sortBy="#{user.nom}">
<h:outputText value="#{user.nom}" />
</p:column>
<p:column headerText="Email" sortBy="#{user.email}">
<h:outputText value="#{user.email}" />
</p:column>
<p:column headerText="Statut">
<fr:tag value="#{user.actif ? 'Actif' : 'Inactif'}"
severity="#{user.actif ? 'success' : 'danger'}" />
</p:column>
</fr:dataTable>
</div>
</div>
<!-- dataView -->
<div class="col-12">
<div class="card component-card">
<h5>fr:dataView</h5>
<p class="text-600 mb-3">Affichage de données en grille/liste avec templates</p>
<fr:dataView value="#{demoBean.sampleUsers}"
var="user"
rows="3"
paginator="true">
<p:dataViewGridItem>
<div class="surface-50 p-3 border-round">
<div class="flex align-items-center gap-2 mb-2">
<i class="pi pi-user text-blue-500"></i>
<span class="font-bold">#{user.nom}</span>
</div>
<p class="text-600 text-sm m-0">#{user.email}</p>
</div>
</p:dataViewGridItem>
</fr:dataView>
</div>
</div>
<!-- tree -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:tree</h5>
<p class="text-600 mb-3">Arborescence hiérarchique navigable</p>
<fr:tree value="#{demoBean.treeRoot}"
var="node">
<p:treeNode>
<h:outputText value="#{node.label}" />
</p:treeNode>
</fr:tree>
</div>
</div>
<!-- treeTable -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:treeTable</h5>
<p class="text-600 mb-3">Tableau arborescent avec colonnes</p>
<fr:treeTable value="#{demoBean.treeRoot}"
var="node">
<p:column headerText="Nom">
<h:outputText value="#{node.label}" />
</p:column>
<p:column headerText="Type">
<h:outputText value="#{node.type}" />
</p:column>
</fr:treeTable>
</div>
</div>
<!-- inplace -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:inplace</h5>
<p class="text-600 mb-3">Édition en ligne activable au clic</p>
<fr:inplace label="Cliquez pour éditer">
<p:inputText value="#{demoBean.inplaceText}" />
</fr:inplace>
</div>
</div>
<!-- ================================================================
SECTION: COMPOSANTS DE FEEDBACK - 3 composants
================================================================ -->
<div class="col-12 mt-5">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-comments text-pink-500"></i>
Composants de Feedback
<fr:tag value="3 composants" severity="danger" />
</h2>
</div>
<!-- message -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:message</h5>
<p class="text-600 mb-3">Message de validation pour champ spécifique</p>
<fr:fieldInput id="champAvecErreur"
label="Champ requis"
value="#{demoBean.requiredField}"
required="true" />
<fr:message for="champAvecErreur" />
</div>
</div>
<!-- growl -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:growl</h5>
<p class="text-600 mb-3">Notifications toast en coin d'écran</p>
<fr:growl id="growlDemo" />
<fr:commandButton value="Afficher Success"
severity="success"
action="#{demoBean.showSuccessMessage}"
update=":formFreyaShowcase:growlDemo" />
<fr:commandButton value="Afficher Warning"
severity="warning"
action="#{demoBean.showWarningMessage}"
update=":formFreyaShowcase:growlDemo"
styleClass="ml-2" />
</div>
</div>
<!-- progressBar -->
<div class="col-12">
<div class="card component-card">
<h5>fr:progressBar</h5>
<p class="text-600 mb-3">Barre de progression pour opérations longues</p>
<fr:progressBar value="#{demoBean.progressValue}"
displayValue="true"
mode="determinate" />
<div class="flex gap-2 mt-3">
<fr:commandButton value="Démarrer"
severity="primary"
action="#{demoBean.startProgress}"
update=":formFreyaShowcase" />
<fr:commandButton value="Réinitialiser"
severity="secondary"
action="#{demoBean.resetProgress}"
update=":formFreyaShowcase" />
</div>
</div>
</div>
<!-- ================================================================
SECTION: COMPOSANTS UTILITAIRES - 3 composants
================================================================ -->
<div class="col-12 mt-5">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-wrench text-teal-500"></i>
Composants Utilitaires
<fr:tag value="3 composants" severity="success" />
</h2>
</div>
<!-- avatar -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:avatar</h5>
<p class="text-600 mb-3">Avatar utilisateur avec image ou initiales</p>
<div class="flex gap-3 align-items-center flex-wrap">
<fr:avatar label="JD" size="large" shape="circle" />
<fr:avatar label="AB" size="normal" shape="circle" />
<fr:avatar label="XY" size="small" shape="square" />
<fr:avatar icon="pi pi-user" size="large" shape="circle" />
</div>
</div>
</div>
<!-- badge -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:badge</h5>
<p class="text-600 mb-3">Badge de notification avec compteur</p>
<div class="flex gap-4 align-items-center flex-wrap">
<fr:badge value="2" severity="danger">
<i class="pi pi-bell" style="font-size: 2rem"></i>
</fr:badge>
<fr:badge value="5" severity="success">
<i class="pi pi-envelope" style="font-size: 2rem"></i>
</fr:badge>
<fr:badge value="10" severity="warning">
<i class="pi pi-shopping-cart" style="font-size: 2rem"></i>
</fr:badge>
</div>
</div>
</div>
<!-- tag -->
<div class="col-12">
<div class="card component-card">
<h5>fr:tag</h5>
<p class="text-600 mb-3">Tags colorés pour statuts et catégories</p>
<div class="flex gap-2 flex-wrap">
<fr:tag value="Primary" severity="primary" />
<fr:tag value="Success" severity="success" icon="pi pi-check" />
<fr:tag value="Info" severity="info" icon="pi pi-info-circle" />
<fr:tag value="Warning" severity="warning" icon="pi pi-exclamation-triangle" />
<fr:tag value="Danger" severity="danger" icon="pi pi-times" />
<fr:tag value="Secondary" severity="secondary" />
<fr:tag value="Help" severity="help" icon="pi pi-question-circle" />
</div>
</div>
</div>
<!-- ================================================================
SECTION: COMPOSANTS AVANCÉS - 3 composants
================================================================ -->
<div class="col-12 mt-5">
<h2 class="section-title flex align-items-center gap-2">
<i class="pi pi-chart-bar text-purple-500"></i>
Composants Avancés
<fr:tag value="3 composants" severity="help" />
</h2>
</div>
<!-- chart -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:chart</h5>
<p class="text-600 mb-3">Graphique avec Chart.js (bar, line, pie, etc.)</p>
<p:chart type="bar"
responsive="true"
style="height:300px">
<f:facet name="chartConfig">
#{demoBean.chartData}
</f:facet>
</p:chart>
</div>
</div>
<!-- formDialog -->
<div class="col-12 md:col-6">
<div class="card component-card">
<h5>fr:formDialog</h5>
<p class="text-600 mb-3">Dialogue modal avec formulaire intégré</p>
<fr:commandButton value="Ouvrir Form Dialog"
icon="pi pi-external-link"
severity="primary"
onclick="PF('formDialogDemo').show()"
type="button" />
<fr:formDialog widgetVar="formDialogDemo"
header="Créer un nouvel utilisateur"
saveLabel="Créer"
cancelLabel="Annuler"
saveAction="#{demoBean.createUser}">
<fr:fieldInput id="dialogNom"
label="Nom"
value="#{demoBean.dialogData.nom}"
required="true" />
<fr:fieldInput id="dialogEmail"
label="Email"
value="#{demoBean.dialogData.email}"
required="true" />
</fr:formDialog>
</div>
</div>
<!-- themeSelector -->
<div class="col-12">
<div class="card component-card">
<h5>fr:themeSelector</h5>
<p class="text-600 mb-3">Sélecteur de thème Freya (16 variantes)</p>
<fr:themeSelector />
<p class="text-600 text-sm mt-3">
<i class="pi pi-info-circle mr-1"></i>
Ce composant permet de changer dynamiquement le thème de l'application parmi
les 16 variantes Freya (8 couleurs × 2 modes dark/light)
</p>
</div>
</div>
<!-- ================================================================
FOOTER - RÉSUMÉ
================================================================ -->
<div class="col-12 mt-5">
<div class="card bg-blue-50">
<div class="text-center">
<i class="pi pi-check-circle text-green-500 mb-3" style="font-size: 3rem"></i>
<h3 class="text-900 mb-2">Intégration Complète Réussie</h3>
<p class="text-600 mb-3">
Les 46 composants PrimeFaces Freya Extension sont maintenant disponibles dans Lions User Manager
</p>
<div class="flex justify-content-center gap-2 flex-wrap">
<fr:tag value="20 Formulaires" severity="info" icon="pi pi-inbox" />
<fr:tag value="5 Actions" severity="warning" icon="pi pi-bolt" />
<fr:tag value="4 Layout" severity="success" icon="pi pi-th-large" />
<fr:tag value="3 Navigation" severity="help" icon="pi pi-map" />
<fr:tag value="5 Données" severity="info" icon="pi pi-table" />
<fr:tag value="3 Feedback" severity="danger" icon="pi pi-comments" />
<fr:tag value="3 Utilitaires" severity="success" icon="pi pi-wrench" />
<fr:tag value="3 Avancés" severity="help" icon="pi pi-chart-bar" />
</div>
</div>
</div>
</div>
</div>
</h:form>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,90 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets" xmlns:p="http://primefaces.org/ui" template="/template.xhtml">
<ui:define name="title">Gestion Rôles</ui:define>
<ui:define name="content">
<h:form id="form">
<div class="card">
<p:toolbar>
<p:toolbarGroup>
<p:commandButton value="Nouveau" icon="pi pi-plus" actionListener="#{roleView.openNew}"
update=":dialogs:manage-role-content" oncomplete="PF('manageRoleDialog').show()"
styleClass="ui-button-success" style="margin-right: .5rem" />
</p:toolbarGroup>
</p:toolbar>
<p:dataTable id="dt-roles" widgetVar="dtRoles" var="role" value="#{roleView.roles}" reflow="true"
styleClass="products-table" selection="#{roleView.selectedRole}" rowKey="#{role.id}"
paginator="true" rows="10" paginatorPosition="bottom">
<f:facet name="header">
<div class="products-table-header">
<span style="font-weight: bold">Rôles</span>
</div>
</f:facet>
<p:column headerText="Nom" sortBy="#{role.name}">
<h:outputText value="#{role.name}" />
</p:column>
<p:column headerText="Description" sortBy="#{role.description}">
<h:outputText value="#{role.description}" />
</p:column>
<p:column headerText="Composite">
<p:tag value="Composite" severity="warning" rendered="#{role.composite}" />
</p:column>
<p:column exportable="false">
<p:commandButton icon="pi pi-pencil" update=":dialogs:manage-role-content"
oncomplete="PF('manageRoleDialog').show()"
styleClass="edit-button rounded-button ui-button-success" process="@this"
style="margin-right: 5px;">
<f:setPropertyActionListener value="#{role}" target="#{roleView.selectedRole}" />
<p:resetInput target=":dialogs:manage-role-content" />
</p:commandButton>
<p:commandButton class="ui-button-warning rounded-button" icon="pi pi-trash" process="@this"
oncomplete="PF('deleteRoleDialog').show()">
<f:setPropertyActionListener value="#{role}" target="#{roleView.selectedRole}" />
</p:commandButton>
</p:column>
</p:dataTable>
</div>
</h:form>
<h:form id="dialogs">
<p:dialog header="Détails Rôle" showEffect="fade" modal="true" widgetVar="manageRoleDialog"
responsive="true" width="450">
<p:outputPanel id="manage-role-content" class="ui-fluid">
<p:outputPanel rendered="#{not empty roleView.selectedRole}">
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="name">Nom</p:outputLabel>
<p:inputText id="name" value="#{roleView.selectedRole.name}" required="true"
disabled="#{not empty roleView.selectedRole.id}" />
</div>
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="description">Description</p:outputLabel>
<p:inputTextarea id="description" value="#{roleView.selectedRole.description}" />
</div>
</p:outputPanel>
</p:outputPanel>
<f:facet name="footer">
<p:commandButton value="Sauvegarder" icon="pi pi-check" actionListener="#{roleView.saveRole}"
update="manage-role-content :form:dt-roles" process="manage-role-content @this"
oncomplete="if (!args.validationFailed) PF('manageRoleDialog').hide()" />
<p:commandButton value="Annuler" icon="pi pi-times" onclick="PF('manageRoleDialog').hide()"
class="ui-button-secondary" type="button" />
</f:facet>
</p:dialog>
<p:confirmDialog widgetVar="deleteRoleDialog" showEffect="fade" width="300" message="Supprimer ce rôle ?"
header="Confirmation" severity="warn">
<p:commandButton value="Oui" icon="pi pi-check" actionListener="#{roleView.deleteRole}" process="@this"
oncomplete="PF('deleteRoleDialog').hide()" update=":form:dt-roles" />
<p:commandButton value="Non" type="button" styleClass="ui-button-secondary" icon="pi pi-times"
onclick="PF('deleteRoleDialog').hide()" />
</p:confirmDialog>
</h:form>
</ui:define>
</ui:composition>

View File

@@ -1,32 +1,14 @@
<!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"
template="/templates/main-template.xhtml">
<ui:param name="page" value="#{userProfilBean}"/>
<ui:define name="title">Attribution de Rôles - Lions User Manager</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-key text-purple-500" />
<ui:param name="title" value="Attribution de Rôles" />
<ui:param name="description" value="Gérer les rôles de l'utilisateur" />
</ui:include>
<!-- Attribution de rôles -->
<div class="card">
<ui:include src="/templates/components/role-management/role-assignment.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="availableRoles" value="#{roleGestionBean.allRoles}" />
<ui:param name="userRoles" value="#{roleGestionBean.getUserRolesDTOs(userProfilBean.user)}" />
<ui:param name="update" value="roleAssignmentPanel" />
</ui:include>
</div>
</ui:define>
</ui:composition>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<f:metadata>
<f:viewAction action="/pages/user-manager/users/list.xhtml?faces-redirect=true" />
</f:metadata>
<h:head>
<title>Redirecting...</title>
</h:head>
<h:body>
Redirecting to user list...
</h:body>
</html>

View File

@@ -1,97 +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="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="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>
@@ -99,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

@@ -0,0 +1,131 @@
<!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:param name="page" value="#{settingsBean}"/>
<ui:define name="title">Paramètres - Lions User Manager</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-cog text-blue-500" />
<ui:param name="title" value="Paramètres" />
<ui:param name="description" value="Gérer vos préférences et paramètres de compte" />
</ui:include>
<div class="grid">
<!-- Informations du compte -->
<div class="col-12 lg:col-8">
<div class="card">
<h5>Informations du compte</h5>
<h:form id="formAccountInfo">
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-4, col-12 md:col-8">
<p:outputLabel for="username" value="Nom d'utilisateur" />
<p:inputText id="username"
value="#{userSessionBean.username}"
readonly="true"
styleClass="w-full" />
<p:outputLabel for="email" value="Email" />
<p:inputText id="email"
value="#{userSessionBean.email}"
readonly="true"
styleClass="w-full" />
<p:outputLabel for="fullName" value="Nom complet" />
<p:inputText id="fullName"
value="#{userSessionBean.fullName}"
readonly="true"
styleClass="w-full" />
<p:outputLabel for="mainRole" value="Rôle principal" />
<p:inputText id="mainRole"
value="#{userSessionBean.mainRole}"
readonly="true"
styleClass="w-full" />
</p:panelGrid>
</h:form>
</div>
</div>
<!-- Préférences -->
<div class="col-12 lg:col-4">
<div class="card">
<h5>Préférences</h5>
<h:form id="formPreferences">
<div class="flex flex-column gap-3">
<div class="flex align-items-center justify-content-between">
<span class="text-600">Thème des composants</span>
<p:selectOneMenu value="#{guestPreferences.componentTheme}"
styleClass="w-12rem">
<f:selectItems value="#{guestPreferences.componentThemes}"
var="theme"
itemLabel="#{theme.name}"
itemValue="#{theme.file}" />
<p:ajax event="change" update="@form" />
</p:selectOneMenu>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Mode sombre</span>
<p:selectOneMenu value="#{guestPreferences.darkMode}"
styleClass="w-12rem">
<f:selectItem itemLabel="Clair" itemValue="light" />
<f:selectItem itemLabel="Sombre" itemValue="dark" />
<p:ajax event="change" update="@form" />
</p:selectOneMenu>
</div>
<div class="flex align-items-center justify-content-between">
<span class="text-600">Style d'input</span>
<p:selectOneMenu value="#{guestPreferences.inputStyle}"
styleClass="w-12rem">
<f:selectItem itemLabel="Outlined" itemValue="outlined" />
<f:selectItem itemLabel="Filled" itemValue="filled" />
<p:ajax event="change" update="@form" />
</p:selectOneMenu>
</div>
</div>
</h:form>
</div>
</div>
<!-- Actions -->
<div class="col-12">
<div class="card">
<h5>Actions</h5>
<div class="flex gap-2">
<h:form>
<p:commandButton
value="Rafraîchir les informations"
icon="pi pi-refresh"
styleClass="p-button-secondary"
action="#{userSessionBean.loadUserInfo}"
update="formAccountInfo" />
</h:form>
<h:form>
<p:commandButton
value="Changer le mot de passe"
icon="pi pi-key"
styleClass="p-button-info"
outcome="/pages/user-manager/users/profile" />
</h:form>
<h:form>
<p:commandButton
value="Sauvegarder les préférences"
icon="pi pi-save"
styleClass="p-button-success"
action="#{settingsBean.savePreferences}"
update="@form" />
</h:form>
</div>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -1,49 +1,171 @@
<!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"
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" template="/templates/main-template.xhtml">
<ui:define name="title">Synchronisation Keycloak - Lions User Manager</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-sync text-blue-500" />
<ui:param name="title" value="Synchronisation Keycloak" />
<ui:param name="description" value="Synchronisation et vérification de l'état de Keycloak" />
</ui:include>
<h:form id="syncForm">
<p:growl id="growl" showDetail="true" />
<!-- Health Checks -->
<div class="grid mb-4">
<div class="col-12 md:col-6">
<div class="card">
<h5>État de Keycloak</h5>
<p:outputLabel value="Vérification de la connexion..." />
<!-- TODO: Intégrer SyncServiceClient pour afficher le statut -->
</div>
<!-- En-tête -->
<div class="col-12">
<ui:decorate template="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-sync text-blue-500" />
<ui:param name="title" value="Synchronisation Keycloak" />
<ui:param name="description" value="Synchronisation et vérification de l'état de Keycloak" />
<ui:define name="actions">
<p:commandButton value="Actualiser l'état" icon="pi pi-refresh"
actionListener="#{syncDashboardBean.checkKeycloakStatus}" update="@form"
styleClass="p-button-outlined" />
</ui:define>
</ui:decorate>
</div>
<div class="col-12 md:col-6">
<div class="card">
<h5>Actions de Synchronisation</h5>
<div class="flex flex-column gap-2">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Synchroniser Utilisateurs" />
<ui:param name="icon" value="pi pi-users" />
<ui:param name="severity" value="primary" />
</ui:include>
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Synchroniser Rôles" />
<ui:param name="icon" value="pi pi-shield" />
<ui:param name="severity" value="info" />
</ui:include>
<div class="grid">
<!-- Health Check Card -->
<div class="col-12 md:col-6">
<div class="card h-full">
<h5>État de Keycloak</h5>
<div class="flex align-items-center gap-3 mb-3">
<div class="flex align-items-center justify-content-center bg-blue-100 border-round p-3"
style="width: 3rem; height: 3rem">
<i class="pi pi-server text-blue-500 text-xl"></i>
</div>
<div class="flex flex-column">
<span class="text-900 font-medium text-xl">
#{syncDashboardBean.keycloakStatusLabel eq 'UP' ? 'En Ligne' : 'Hors Ligne'}
</span>
<span class="text-600">Statut du service</span>
</div>
<span class="ml-auto">
<p:tag value="#{syncDashboardBean.keycloakStatusLabel}"
severity="#{syncDashboardBean.keycloakStatusLabel eq 'UP' ? 'success' : 'danger'}" />
</span>
</div>
<p:divider />
<div class="flex flex-column gap-2">
<div class="flex justify-content-between">
<span class="text-600">Message:</span>
<span class="text-900">#{syncDashboardBean.keycloakStatusMessage}</span>
</div>
<div class="flex justify-content-between">
<span class="text-600">Version:</span>
<span class="text-900">#{syncDashboardBean.keycloakVersion}</span>
</div>
</div>
</div>
</div>
<!-- Sync Actions Card -->
<div class="col-12 md:col-6">
<div class="card h-full">
<h5>Actions de Synchronisation</h5>
<p class="text-600 mb-4">Lancez la synchronisation des données depuis Keycloak vers la base
locale.</p>
<div class="flex flex-column gap-3">
<div
class="flex align-items-center justify-content-between border-1 surface-border border-round p-3">
<div class="flex align-items-center gap-2">
<i class="pi pi-users text-primary text-xl"></i>
<span class="text-900 font-medium">Utilisateurs</span>
</div>
<p:commandButton value="Synchroniser" icon="pi pi-sync"
actionListener="#{syncDashboardBean.syncUsers}" update="growl"
styleClass="p-button-rounded p-button-text" />
</div>
<div
class="flex align-items-center justify-content-between border-1 surface-border border-round p-3">
<div class="flex align-items-center gap-2">
<i class="pi pi-shield text-primary text-xl"></i>
<span class="text-900 font-medium">Rôles</span>
</div>
<p:commandButton value="Synchroniser" icon="pi pi-sync"
actionListener="#{syncDashboardBean.syncRoles}" update="growl"
styleClass="p-button-rounded p-button-text p-button-secondary" />
</div>
</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>
</div>
</h:form>
</ui:define>
</ui:composition>
</ui:composition>

View File

@@ -0,0 +1,132 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="jakarta.faces.html" xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets" xmlns:p="http://primefaces.org/ui" template="/template.xhtml">
<ui:define name="title">Gestion Utilisateurs</ui:define>
<ui:define name="content">
<h:form id="form">
<div class="card">
<p:toolbar>
<p:toolbarGroup>
<p:commandButton value="Nouveau" icon="pi pi-plus" actionListener="#{userView.openNew}"
update=":dialogs:manage-user-content" oncomplete="PF('manageUserDialog').show()"
styleClass="ui-button-success" style="margin-right: .5rem" />
<!-- Custom CSV Download via Backend API -->
<p:commandButton value="Tout Exporter (CSV)" icon="pi pi-download"
actionListener="#{userView.downloadCSV}" ajax="false" styleClass="ui-button-warning ml-2" />
</p:toolbarGroup>
</p:toolbar>
<p:dataTable id="dt-users" widgetVar="dtUsers" var="user" value="#{userView.users}" reflow="true"
styleClass="products-table" selection="#{userView.selectedUser}" rowKey="#{user.id}"
paginator="true" rows="10" rowSelectMode="add" paginatorPosition="bottom" lazy="true">
<f:facet name="header">
<div class="products-table-header">
<span style="font-weight: bold">Utilisateurs</span>
<span class="filter-container ui-input-icon-left" style="margin-left: 20px;">
<i class="pi pi-search" />
<p:inputText id="globalFilter" onkeyup="PF('dtUsers').filter()"
placeholder="Recherche..." value="#{userView.searchTerm}">
<p:ajax event="keyup" delay="500" listener="#{userView.search}" update="dt-users" />
</p:inputText>
</span>
</div>
</f:facet>
<p:column headerText="Username" sortBy="#{user.username}">
<h:outputText value="#{user.username}" />
</p:column>
<p:column headerText="Email" sortBy="#{user.email}">
<h:outputText value="#{user.email}" />
</p:column>
<p:column headerText="Prénom" sortBy="#{user.prenom}">
<h:outputText value="#{user.prenom}" />
</p:column>
<p:column headerText="Nom" sortBy="#{user.nom}">
<h:outputText value="#{user.nom}" />
</p:column>
<p:column headerText="Statut">
<span class="product-badge status-#{user.enabled ? 'instock' : 'outofstock'}"
style="padding: 0.25em 0.5rem; border-radius: 4px; background-color: #{user.enabled ? '#C8E6C9' : '#FFCDD2'}; color: #{user.enabled ? '#256029' : '#C63737'}; font-weight: 700;">
#{user.enabled ? 'ACTIF' : 'INACTIF'}
</span>
</p:column>
<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="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 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>
</p:column>
</p:dataTable>
</div>
</h:form>
<h:form id="dialogs">
<p:dialog header="Détails Utilisateur" showEffect="fade" modal="true" widgetVar="manageUserDialog"
responsive="true" width="450">
<p:outputPanel id="manage-user-content" class="ui-fluid">
<p:outputPanel rendered="#{not empty userView.selectedUser}">
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="username">Username</p:outputLabel>
<p:inputText id="username" value="#{userView.selectedUser.username}" required="true"
disabled="#{not empty userView.selectedUser.id}" />
</div>
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="email">Email</p:outputLabel>
<p:inputText id="email" value="#{userView.selectedUser.email}" />
</div>
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="firstname">Prénom</p:outputLabel>
<p:inputText id="firstname" value="#{userView.selectedUser.prenom}" />
</div>
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="lastname">Nom</p:outputLabel>
<p:inputText id="lastname" value="#{userView.selectedUser.nom}" />
</div>
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="enabled" style="margin-right: 10px;">Actif</p:outputLabel>
<p:selectBooleanCheckbox id="enabled" value="#{userView.selectedUser.enabled}" />
</div>
<!-- Password field only for creation -->
<p:outputPanel rendered="#{empty userView.selectedUser.id}">
<div class="field" style="margin-bottom: 1rem;">
<p:outputLabel for="password">Mot de passe (Temporaire)</p:outputLabel>
<p:password id="password" value="#{userView.selectedUser.temporaryPassword}"
toggleMask="true" redisplay="true" />
</div>
</p:outputPanel>
</p:outputPanel>
</p:outputPanel>
<f:facet name="footer">
<p:commandButton value="Sauvegarder" icon="pi pi-check" actionListener="#{userView.saveUser}"
update="manage-user-content :form:dt-users" process="manage-user-content @this"
oncomplete="if (!args.validationFailed) PF('manageUserDialog').hide()" />
<p:commandButton value="Annuler" icon="pi pi-times" onclick="PF('manageUserDialog').hide()"
class="ui-button-secondary" type="button" />
</f:facet>
</p:dialog>
<p:confirmDialog widgetVar="deleteUserDialog" showEffect="fade" width="300"
message="Supprimer cet utilisateur ?" header="Confirmation" severity="warn">
<p:commandButton value="Oui" icon="pi pi-check" actionListener="#{userView.deleteUser}" process="@this"
oncomplete="PF('deleteUserDialog').hide()" update=":form:dt-users" />
<p:commandButton value="Non" type="button" styleClass="ui-button-secondary" icon="pi pi-times"
onclick="PF('deleteUserDialog').hide()" />
</p:confirmDialog>
</h:form>
</ui:define>
</ui:composition>

View File

@@ -1,35 +1,272 @@
<!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"
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" template="/templates/main-template.xhtml">
<ui:param name="page" value="#{userCreationBean}"/>
<ui:param name="page" value="#{userCreationBean}" />
<ui:define name="title">Nouvel Utilisateur - Lions User Manager</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/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:include>
<div class="grid">
<!-- En-tête -->
<div class="col-12">
<ui:decorate template="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-user-plus text-green-500" />
<ui:param name="title" value="Nouvel Utilisateur" />
<ui:param name="description" value="Créer un nouvel utilisateur dans Keycloak" />
<ui:param name="breadcrumbParent" value="Utilisateurs" />
<ui:param name="breadcrumbParentLink" value="/pages/user-manager/users/list" />
<ui:define name="actions">
<h:link outcome="/pages/user-manager/users/list"
styleClass="p-button p-button-outlined p-button-secondary">
<i class="pi pi-arrow-left mr-2"></i>Retour à la liste
</h:link>
</ui:define>
</ui:decorate>
</div>
<!-- Formulaire de création -->
<div class="card">
<ui:include src="/templates/components/user-management/user-form.xhtml">
<ui:param name="user" value="#{userCreationBean.newUser}" />
<ui:param name="mode" value="create" />
<ui:param name="showRealmSelector" value="true" />
<ui:param name="showPasswordFields" value="true" />
<ui:param name="submitAction" value="#{userCreationBean.createUser}" />
<ui:param name="hasSubmitAction" value="true" />
<ui:param name="cancelOutcome" value="/pages/user-manager/users/list" />
</ui:include>
<!-- 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">
<i class="pi pi-info-circle text-blue-500"></i>
Informations de l'Utilisateur
</h3>
<div class="grid">
<!-- 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">
<i class="pi pi-user text-blue-500"></i>
<span>Informations de Base</span>
</h4>
<div class="field mb-3">
<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"
requiredMessage="Le nom d'utilisateur est obligatoire"
placeholder="ex: jdupont">
<f:validateLength minimum="3" maximum="50" />
</p:inputText>
<p:message for="username" display="text" styleClass="mt-1" />
<small class="text-500 block mt-1">Identifiant unique de connexion</small>
</div>
<div class="field mb-3">
<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"
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>
<div class="field mb-3">
<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"
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>
<div class="field mb-0">
<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"
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 -->
<div class="col-12 lg:col-6">
<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>
<div class="field mb-3">
<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"
requiredMessage="Le mot de passe est obligatoire"
feedback="true" toggleMask="true"
placeholder="Minimum 8 caractères">
<f:validateLength minimum="8" maximum="100" />
</p:password>
<p:message for="password" display="text" styleClass="mt-1" />
<small class="text-500 block mt-1">Au moins 8 caractères</small>
</div>
<div class="field mb-0">
<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>
<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>
<div class="field mb-3">
<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"
itemLabel="#{realm}" itemValue="#{realm}" />
</p:selectOneMenu>
</div>
<div class="flex flex-column gap-3">
<div class="flex align-items-center gap-2">
<p:selectBooleanCheckbox id="enabled"
value="#{userCreationBean.newUser.enabled}" />
<p:outputLabel for="enabled" value="Compte activé" />
</div>
<div class="flex align-items-center gap-2">
<p:selectBooleanCheckbox id="emailVerified"
value="#{userCreationBean.newUser.emailVerified}" />
<p:outputLabel for="emailVerified" value="Email vérifié" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="col-12">
<div class="card">
<div class="flex flex-wrap gap-2 align-items-center">
<p:commandButton value="Créer l'utilisateur" icon="pi pi-check"
styleClass="p-button-success" action="#{userCreationBean.createUser}"
update=":formUserCreation" validateClient="true" />
<p:commandButton value="Réinitialiser" icon="pi pi-refresh"
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>
<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>
<p:commandButton value="Aide" icon="pi pi-question-circle"
styleClass="p-button-outlined p-button-help" type="button"
onclick="PF('helpDialog').show();" />
</div>
<p:messages id="messages" showDetail="true" closable="true" styleClass="mt-3">
<p:autoUpdate />
</p:messages>
</div>
</div>
</h:form>
</div>
<!-- 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">
<div class="col-12">
<h4 class="text-900 font-semibold flex align-items-center gap-2 mb-3">
<i class="pi pi-info-circle text-blue-500"></i>
Informations Requises
</h4>
<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>
<h4 class="text-900 font-semibold flex align-items-center gap-2 mb-3">
<i class="pi pi-shield text-purple-500"></i>
Sécurité
</h4>
<ul class="text-700 line-height-3 mb-4">
<li>Le mot de passe doit contenir au moins 8 caractères</li>
<li>Utilisez une combinaison de lettres, chiffres et caractères spéciaux</li>
<li>L'utilisateur pourra modifier son mot de passe après la première connexion</li>
</ul>
<h4 class="text-900 font-semibold flex align-items-center gap-2 mb-3">
<i class="pi pi-cog text-orange-500"></i>
Configuration
</h4>
<ul class="text-700 line-height-3 mb-0">
<li><strong>Compte activé</strong> : L'utilisateur peut se connecter immédiatement</li>
<li><strong>Email vérifié</strong> : L'email est considéré comme vérifié</li>
<li><strong>Realm</strong> : Espace d'administration Keycloak</li>
</ul>
</div>
</div>
<f:facet name="footer">
<p:commandButton value="Fermer" icon="pi pi-times" styleClass="p-button-text"
onclick="PF('helpDialog').hide();" type="button" />
</f:facet>
</p:dialog>
</ui:define>
</ui:composition>

View File

@@ -1,12 +1,15 @@
<!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"
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" template="/templates/main-template.xhtml">
<ui:param name="page" value="#{userProfilBean}"/>
<f:metadata>
<f:viewParam name="userId" value="#{userProfilBean.userId}" />
<f:viewParam name="realm" value="#{userProfilBean.realmName}" />
<f:event type="preRenderView" listener="#{userProfilBean.loadUser}" />
</f:metadata>
<ui:param name="page" value="#{userProfilBean}" />
<ui:define name="title">Modifier Utilisateur - Lions User Manager</ui:define>
<ui:define name="content">
@@ -14,20 +17,33 @@
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-pencil text-warning-500" />
<ui:param name="title" value="Modifier Utilisateur" />
<ui:param name="description" value="Modifier les informations de l'utilisateur" />
<ui:param name="description" value="Modifier les informations de l'utilisateur et gérer ses rôles" />
</ui:include>
<!-- Formulaire d'édition -->
<div class="card">
<ui:include src="/templates/components/user-management/user-form.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="mode" value="edit" />
<ui:param name="showPasswordFields" value="false" />
<ui:param name="submitAction" value="#{userProfilBean.updateUser}" />
<ui:param name="cancelOutcome" value="/pages/user-manager/users/list" />
</ui:include>
<p:tabView id="userTabs">
<p:tab title="Profil Utilisateur">
<ui:include src="/templates/components/user-management/user-form.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="mode" value="edit" />
<ui:param name="showPasswordFields" value="false" />
<ui:param name="submitAction" value="#{userProfilBean.updateUser}" />
<ui:param name="cancelOutcome" value="/pages/user-manager/users/list" />
<ui:param name="useParentForm" value="false" />
</ui:include>
</p:tab>
<p:tab title="Gestion des Rôles" rendered="#{not empty userProfilBean.user.id}">
<h:form id="rolesForm">
<ui:include src="/templates/components/user-management/user-roles-manager.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="realm" value="#{userProfilBean.realmName}" />
<ui:param name="roleBean" value="#{roleGestionBean}" />
</ui:include>
</h:form>
</p:tab>
</p:tabView>
</div>
</ui:define>
</ui:composition>
</ui:composition>

View File

@@ -1,120 +1,257 @@
<!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"
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" template="/templates/main-template.xhtml">
<ui:param name="page" value="#{userListBean}"/>
<ui:param name="page" value="#{userListBean}" />
<ui:define name="title">Liste des Utilisateurs - Lions User Manager</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<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" />
<ui:param name="description"
value="Gestion centralisée des utilisateurs Keycloak - Recherche, création, modification et suppression" />
<ui:define name="actions">
<h:form id="formActionsUsers">
<h:form id="formHeaderActions">
<div class="flex gap-2">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Nouvel Utilisateur" />
<ui:param name="icon" value="pi pi-user-plus" />
<ui:param name="outcome" value="/pages/user-manager/users/create" />
<ui:param name="severity" value="success" />
</ui:include>
<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:include>
</ui:decorate>
<!-- Statistiques KPI -->
<div class="grid mb-4">
<!-- Total Utilisateurs -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Total Utilisateurs" />
<ui:param name="value" value="#{userListBean.totalRecords}" />
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
<ui:param name="subtitle" value="Utilisateurs dans le realm" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/users/list" />
</ui:include>
<!-- Utilisateurs Actifs -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Utilisateurs Actifs" />
<ui:param name="value" value="#{userListBean.activeUsersCount}" />
<ui:param name="icon" value="pi-user-check" />
<ui:param name="iconColor" value="green-600" />
<ui:param name="subtitle" value="#{userListBean.activeUsersPercentage}% du total" />
<ui:param name="progressValue" value="#{userListBean.activeUsersPercentage}" />
<ui:param name="statusIcon" value="pi-check-circle" />
<ui:param name="statusLabel" value="Actifs" />
<ui:param name="statusValue" value="#{userListBean.activeUsersCount} utilisateurs" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/users/list" />
</ui:include>
<!-- Utilisateurs Désactivés -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Utilisateurs Désactivés" />
<ui:param name="value" value="#{userListBean.disabledUsersCount}" />
<ui:param name="icon" value="pi-user-times" />
<ui:param name="iconColor" value="red-600" />
<ui:param name="subtitle" value="#{userListBean.disabledUsersPercentage}% du total" />
<ui:param name="progressValue" value="#{userListBean.disabledUsersPercentage}" />
<ui:param name="statusIcon" value="pi-times-circle" />
<ui:param name="statusLabel" value="Désactivés" />
<ui:param name="statusValue" value="#{userListBean.disabledUsersCount} utilisateurs" />
<ui:param name="clickable" value="true" />
<ui:param name="clickOutcome" value="/pages/user-manager/users/list" />
</ui:include>
<!-- Realm Actuel -->
<ui:include src="/templates/components/shared/cards/kpi-card.xhtml">
<ui:param name="title" value="Realm Actuel" />
<ui:param name="value" value="#{empty userListBean.realmName ? 'master' : userListBean.realmName}" />
<ui:param name="icon" value="pi-globe" />
<ui:param name="iconColor" value="purple-600" />
<ui:param name="subtitle" value="Realm Keycloak" />
<ui:param name="statusIcon" value="pi-info-circle" />
<ui:param name="statusLabel" value="Realm" />
<ui:param name="statusValue" value="#{empty userListBean.realmName ? 'master' : userListBean.realmName}" />
<ui:param name="showProgress" value="false" />
</ui:include>
</div>
<!-- Messages globaux -->
<p:growl id="growlMessages" showDetail="true" life="5000">
<p:autoUpdate />
</p:growl>
<!-- Barre de recherche et Tableau des utilisateurs dans le même formulaire -->
<h:form id="formUsers">
<!-- Barre de recherche -->
<div class="card mb-3">
<ui:include src="/templates/components/user-management/user-search-bar.xhtml">
<ui:param name="searchCriteria" value="#{userListBean.searchCriteria}" />
<ui:param name="searchAction" value="#{userListBean.search}" />
<ui:param name="update" value="userTable" />
<ui:param name="showAdvanced" value="true" />
<ui:param name="useParentForm" value="true" />
</ui:include>
</div>
<h:form id="formUserList">
<div class="grid">
<!-- Tableau des utilisateurs -->
<div class="card">
<ui:include src="/templates/components/shared/tables/user-data-table.xhtml">
<ui:param name="users" value="#{userListBean.users}" />
<ui:param name="var" value="user" />
<ui:param name="tableId" value="userTable" />
<ui:param name="paginator" value="true" />
<ui:param name="rows" value="20" />
<ui:param name="showActions" value="true" />
<ui:param name="showRoles" value="true" />
<ui:param name="showEmail" value="true" />
<ui:param name="showStatus" value="true" />
<ui:param name="update" value="userTable" />
</ui:include>
<!-- ================================================================
STATISTIQUES KPI (4 CARTES)
================================================================ -->
<div class="col-12">
<h5 class="mb-3">Statistiques des Utilisateurs</h5>
</div>
<!-- KPI 1: Total Utilisateurs -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<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.kpiTotalUsers}</div>
</div>
<div class="flex align-items-center justify-content-center bg-blue-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-users text-blue-600 text-xl"></i>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-database text-600"></i>
<span class="ml-2">Utilisateurs dans le realm</span>
</div>
</div>
</div>
<!-- KPI 2: Utilisateurs Actifs -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Utilisateurs Actifs</div>
<div class="text-900 font-bold text-2xl">#{userListBean.activeUsersCount}</div>
</div>
<div class="flex align-items-center justify-content-center bg-green-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-check-circle text-green-600 text-xl"></i>
</div>
</div>
<div class="flex align-items-center gap-2">
<span class="text-green-600 font-semibold">
<i class="pi pi-arrow-up text-xs"></i>
#{userListBean.activeUsersPercentage}%
</span>
<span class="text-500 text-sm">Taux d'activation</span>
</div>
<p:progressBar value="#{userListBean.activeUsersPercentage}" styleClass="mt-2"
style="height: 4px" showValue="false" />
</div>
</div>
<!-- KPI 3: Utilisateurs Désactivés -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div>
<div class="text-500 font-medium mb-1">Utilisateurs Désactivés</div>
<div class="text-900 font-bold text-2xl">#{userListBean.disabledUsersCount}</div>
</div>
<div class="flex align-items-center justify-content-center bg-red-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-times-circle text-red-600 text-xl"></i>
</div>
</div>
<div class="flex align-items-center gap-2">
<span class="text-red-600 font-semibold">
<i class="pi pi-arrow-down text-xs"></i>
#{userListBean.disabledUsersPercentage}%
</span>
<span class="text-500 text-sm">Taux de désactivation</span>
</div>
<p:progressBar value="#{userListBean.disabledUsersPercentage}" styleClass="mt-2"
style="height: 4px" showValue="false" />
</div>
</div>
<!-- KPI 4: Realm Actuel -->
<div class="col-12 md:col-6 lg:col-3">
<div class="card surface-0 border-round-lg">
<div class="flex align-items-start justify-content-between mb-3">
<div style="max-width: 150px;">
<div class="text-500 font-medium mb-1">Realm Actuel</div>
<div class="text-900 font-bold text-xl" style="word-break: break-word;">
#{userListBean.realmName}</div>
</div>
<div class="flex align-items-center justify-content-center bg-purple-100 border-circle"
style="width: 2.5rem; height: 2.5rem">
<i class="pi pi-globe text-purple-600 text-xl"></i>
</div>
</div>
<div class="text-500 text-sm">
<i class="pi pi-server text-600"></i>
<span class="ml-2">Realm Keycloak</span>
</div>
</div>
</div>
<!-- ================================================================
SECTION RECHERCHE ET FILTRES
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center gap-2 mb-3">
<i class="pi pi-search text-blue-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Recherche et Filtres</h5>
</div>
<div class="grid">
<div class="col-12 md:col-6 lg:col-4">
<label for="searchText" class="block text-900 font-medium mb-2">Recherche</label>
<p:inputText id="searchText" value="#{userListBean.searchText}"
placeholder="Nom, email..." styleClass="w-full">
<p:ajax event="keyup" delay="500" update=":formUserList:userTable"
listener="#{userListBean.search}" />
</p:inputText>
</div>
<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:selectItems value="#{userListBean.availableRealms}" />
<p:ajax update=":formUserList" listener="#{userListBean.onRealmChange}" />
</p:selectOneMenu>
</div>
<div class="col-12 md:col-6 lg:col-3">
<label for="statusSelect" class="block text-900 font-medium mb-2">Statut</label>
<p:selectOneMenu id="statusSelect" value="#{userListBean.selectedStatut}"
styleClass="w-full">
<f:selectItem itemLabel="Tous" itemValue="#{null}" />
<f:selectItems value="#{userListBean.statutOptions}" />
<p:ajax update=":formUserList:userTable" listener="#{userListBean.search}" />
</p:selectOneMenu>
</div>
<div class="col-12 lg:col-2 flex align-items-end">
<p:commandButton value="Réinitialiser" icon="pi pi-refresh"
styleClass="p-button-secondary w-full" action="#{userListBean.resetSearch}"
update=":formUserList:userTable @form" />
</div>
</div>
</div>
</div>
<!-- ================================================================
TABLEAU DES UTILISATEURS
================================================================ -->
<div class="col-12">
<ui:include src="/templates/components/shared/tables/user-data-table.xhtml">
<ui:param name="users" value="#{userListBean.users}" />
<ui:param name="headerTitle" value="Liste des Utilisateurs" />
<ui:param name="tableId" value="userTable" />
<ui:param name="paginator" value="true" />
<ui:param name="rows" value="#{userListBean.pageSize}" />
<ui:param name="showActions" value="true" />
<ui:param name="showSelection" value="false" />
<ui:param name="lazy" value="true" />
<ui:param name="totalRecords" value="#{userListBean.totalRecords}" />
<ui:param name="actionBean" value="#{userListBean}" />
</ui:include>
</div>
<!-- ================================================================
ACTIONS RAPIDES
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center gap-2 mb-3">
<i class="pi pi-bolt text-orange-500" style="font-size: 1.5rem"></i>
<h5 class="m-0">Actions Rapides</h5>
</div>
<div class="grid">
<div class="col-12 md:col-6 lg:col-3">
<p:commandButton value="Créer un Utilisateur" icon="pi pi-user-plus"
styleClass="w-full p-button-success" outcome="/pages/user-manager/users/create" />
</div>
<div class="col-12 md:col-6 lg:col-3">
<p:commandButton value="Exporter la Liste" icon="pi pi-download"
styleClass="w-full p-button-secondary" action="#{userListBean.exportToCSV}"
ajax="false" />
</div>
<div class="col-12 md:col-6 lg:col-3">
<p:commandButton value="Importer des Utilisateurs" icon="pi pi-upload"
styleClass="w-full p-button-info" onclick="PF('importUsersDialog').show()"
type="button" />
</div>
<div class="col-12 md:col-6 lg:col-3">
<p:commandButton value="Gestion des Rôles" icon="pi pi-shield"
styleClass="w-full p-button-primary" outcome="/pages/user-manager/roles/list" />
</div>
</div>
</div>
</div>
</div>
</h:form>
<!-- ================================================================
DIALOG D'IMPORT
================================================================ -->
<p:dialog id="importUsersDialog" widgetVar="importUsersDialog" header="Importer des Utilisateurs" modal="true"
resizable="false" styleClass="w-full md:w-30rem">
<h:form id="formImportUsers">
<div class="flex flex-column gap-3">
<p class="text-600">
Importez des utilisateurs depuis un fichier CSV ou JSON.
</p>
<p:fileUpload mode="simple" skinSimple="true" accept=".csv,.json" label="Sélectionner un fichier" />
<div class="flex justify-content-end gap-2 mt-3">
<p:commandButton value="Annuler" icon="pi pi-times" styleClass="p-button-secondary"
onclick="PF('importUsersDialog').hide()" type="button" />
<p:commandButton value="Importer" icon="pi pi-upload" styleClass="p-button-success"
action="#{userListBean.importUsers}" update=":formUserList"
oncomplete="PF('importUsersDialog').hide()" />
</div>
</div>
</h:form>
</p:dialog>
</ui:define>
</ui:composition>
</ui:composition>

View File

@@ -7,101 +7,405 @@
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
template="/templates/main-template.xhtml">
<ui:param name="page" value="#{userProfilBean}"/>
<ui:define name="title">Profil Utilisateur - Lions User Manager</ui:define>
<ui:param name="page" value="#{userSessionBean}"/>
<ui:define name="title">Mon Profil - Lions User Manager</ui:define>
<ui:define name="content">
<!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-user text-blue-500" />
<ui:param name="title" value="Profil Utilisateur" />
<ui:param name="description" value="Détails et gestion de l'utilisateur" />
<ui:define name="actions">
<h:form id="formActionsProfile">
<div class="flex gap-2">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Modifier" />
<ui:param name="icon" value="pi pi-pencil" />
<ui:param name="action" value="#{userProfilBean.enableEditMode}" />
<ui:param name="update" value="userProfileForm" />
<ui:param name="severity" value="warning" />
<ui:param name="rendered" value="#{not userProfilBean.editMode}" />
</ui:include>
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Retour" />
<ui:param name="icon" value="pi pi-arrow-left" />
<ui:param name="outcome" value="/pages/user-manager/users/list" />
<ui:param name="severity" value="secondary" />
</ui:include>
</div>
</h:form>
</ui:define>
</ui:include>
<div class="grid">
<!-- Carte utilisateur -->
<div class="col-12 md:col-4">
<ui:include src="/templates/components/user-management/user-card.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="showActions" value="false" />
<ui:param name="showRoles" value="true" />
</ui:include>
<!-- ================================================================
EN-TÊTE DE LA PAGE
================================================================ -->
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between">
<h2 class="text-900 font-semibold text-xl m-0">
<i class="pi pi-user text-blue-500 mr-2"></i>
Mon Profil
</h2>
<h:link outcome="/pages/user-manager/dashboard" styleClass="p-button p-button-text">
<i class="pi pi-arrow-left mr-2"></i>
Retour au tableau de bord
</h:link>
</div>
</div>
</div>
<!-- Formulaire d'édition -->
<div class="col-12 md:col-8">
<!-- ================================================================
CARTE PROFIL PRINCIPAL
================================================================ -->
<div class="col-12">
<div class="card">
<h:form id="userProfileForm">
<c:choose>
<c:when test="#{userProfilBean.editMode}">
<ui:include src="/templates/components/user-management/user-form.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="mode" value="edit" />
<ui:param name="showPasswordFields" value="false" />
<ui:param name="submitAction" value="#{userProfilBean.updateUser}" />
<ui:param name="cancelOutcome" value="" />
</ui:include>
</c:when>
<c:otherwise>
<!-- Mode lecture seule -->
<ui:include src="/templates/components/user-management/user-form.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="readonly" value="true" />
</ui:include>
</c:otherwise>
</c:choose>
</h:form>
</div>
<div class="grid">
<!-- Photo de profil et informations principales -->
<div class="col-12 md:col-4">
<div class="text-center mb-4">
<!-- Avatar avec gradient -->
<div style="width: 140px; height: 140px; border-radius: 50%; background: linear-gradient(135deg, var(--primary-color), var(--primary-600, #387FE9)); display: flex; align-items: center; justify-content: center; margin: 0 auto 1.5rem auto; font-size: 3.5rem; font-weight: bold; color: white; box-shadow: 0 8px 24px rgba(0,0,0,0.12);">
#{userSessionBean.initials}
</div>
<!-- Actions rapides -->
<div class="card mt-3">
<h5>Actions Rapides</h5>
<div class="flex flex-wrap gap-2">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Réinitialiser mot de passe" />
<ui:param name="icon" value="pi pi-key" />
<ui:param name="onclick" value="PF('resetPasswordDialog').show()" />
<ui:param name="severity" value="info" />
</ui:include>
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="#{userProfilBean.user.enabled ? 'Désactiver' : 'Activer'}" />
<ui:param name="icon" value="pi #{userProfilBean.user.enabled ? 'pi-times' : 'pi-check'}" />
<ui:param name="action" value="#{userProfilBean.user.enabled ? userProfilBean.deactivateUser() : userProfilBean.activateUser()}" />
<ui:param name="update" value="userProfileForm" />
<ui:param name="severity" value="#{userProfilBean.user.enabled ? 'warning' : 'success'}" />
</ui:include>
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Déconnecter toutes les sessions" />
<ui:param name="icon" value="pi pi-sign-out" />
<ui:param name="action" value="#{userProfilBean.logoutAllSessions}" />
<ui:param name="update" value="userProfileForm" />
<ui:param name="severity" value="info" />
</ui:include>
<!-- Nom complet -->
<h3 class="text-900 font-semibold text-2xl mb-2">#{userSessionBean.fullName}</h3>
<!-- Email -->
<p class="text-600 mb-3 flex align-items-center justify-content-center gap-2">
<i class="pi pi-envelope"></i>
#{userSessionBean.email}
</p>
<!-- Badge de statut -->
<div class="inline-flex align-items-center justify-content-center gap-2">
<span class="inline-flex align-items-center gap-2 bg-green-100 text-green-700 px-3 py-2 border-round font-semibold" style="font-size: 1rem;">
<i class="pi pi-circle-fill" style="font-size: 0.5rem; animation: pulse 2s ease-in-out infinite;"></i>
<span>Connecté</span>
</span>
</div>
<!-- Badge du rôle principal -->
<div class="mt-3 flex justify-content-center">
<span class="inline-flex align-items-center bg-blue-100 text-blue-700 px-3 py-1 border-round font-semibold text-sm" style="text-transform: uppercase; letter-spacing: 0.5px;">
#{userSessionBean.primaryRole}
</span>
</div>
</div>
</div>
<!-- Informations détaillées -->
<div class="col-12 md:col-8">
<div class="grid">
<!-- Colonne gauche: Informations personnelles -->
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold text-lg mb-3 flex align-items-center gap-2">
<i class="pi pi-user text-blue-500"></i>
Informations Personnelles
</h4>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Nom d'utilisateur</label>
<p class="text-900 font-semibold m-0">#{userSessionBean.username}</p>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Nom complet</label>
<p class="text-900 font-semibold m-0">#{userSessionBean.fullName}</p>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Adresse email</label>
<div class="flex align-items-center gap-2">
<p class="text-900 font-semibold m-0">#{userSessionBean.email}</p>
<i class="pi pi-check-circle text-green-500" title="Email vérifié"></i>
</div>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Prénom</label>
<p class="text-900 font-semibold m-0">#{userSessionBean.firstName}</p>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Nom</label>
<p class="text-900 font-semibold m-0">#{userSessionBean.lastName}</p>
</div>
</div>
<!-- Colonne droite: Rôles et permissions -->
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold text-lg mb-3 flex align-items-center gap-2">
<i class="pi pi-shield text-purple-500"></i>
Rôles et Permissions
</h4>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-2 text-sm">Rôles assignés</label>
<div class="flex flex-wrap gap-2">
<ui:repeat value="#{userSessionBean.roles}" var="role">
<p:badge value="#{role}" severity="info" styleClass="text-sm"></p:badge>
</ui:repeat>
</div>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-2 text-sm">Rôle principal</label>
<div class="flex align-items-center">
<p:badge value="#{userSessionBean.primaryRole}"
severity="success"
styleClass="text-sm">
</p:badge>
</div>
</div>
<div class="mb-3 pb-3 border-bottom-1 surface-border">
<label class="block text-600 font-medium mb-1 text-sm">Niveau d'accès</label>
<p class="text-900 font-semibold m-0">
<h:outputText value="Administrateur système" rendered="#{userSessionBean.hasRole('admin')}" />
<h:outputText value="Gestionnaire utilisateurs" rendered="#{userSessionBean.hasRole('user_manager') and not userSessionBean.hasRole('admin')}" />
<h:outputText value="Consultation utilisateurs" rendered="#{userSessionBean.hasRole('user_viewer') and not userSessionBean.hasRole('user_manager') and not userSessionBean.hasRole('admin')}" />
<h:outputText value="Auditeur" rendered="#{userSessionBean.hasRole('auditor') and not userSessionBean.hasRole('user_viewer') and not userSessionBean.hasRole('user_manager') and not userSessionBean.hasRole('admin')}" />
<h:outputText value="Utilisateur standard" rendered="#{not userSessionBean.hasRole('admin') and not userSessionBean.hasRole('user_manager') and not userSessionBean.hasRole('user_viewer') and not userSessionBean.hasRole('auditor')}" />
</p>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-2 text-sm">Statut du compte</label>
<div class="flex align-items-center">
<p:badge value="Actif" severity="success" styleClass="text-sm"></p:badge>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ================================================================
INFORMATIONS DE SESSION OIDC
================================================================ -->
<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-shield text-orange-500"></i>
Informations de Session OIDC
</h3>
<div class="grid">
<!-- Colonne gauche: Token Information -->
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold mb-3">Informations du Token</h4>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Issuer (Émetteur)</label>
<p class="text-700 m-0 text-sm font-mono bg-bluegray-50 p-2 border-round">
#{userSessionBean.issuer}
</p>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Subject (Identifiant)</label>
<p class="text-700 m-0 text-sm font-mono bg-bluegray-50 p-2 border-round">
#{userSessionBean.subject}
</p>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Audience</label>
<p class="text-700 m-0 text-sm font-mono bg-bluegray-50 p-2 border-round">
account
</p>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-2 text-sm">Token Type</label>
<div class="flex align-items-center">
<p:badge value="Bearer" severity="info" styleClass="text-sm"></p:badge>
</div>
</div>
</div>
<!-- Colonne droite: Session Details -->
<div class="col-12 md:col-6">
<h4 class="text-900 font-semibold mb-3">Détails de la Session</h4>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Expiration du token</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-calendar text-orange-500"></i>
<p class="text-700 m-0 text-sm">
<h:outputText value="#{userSessionBean.expirationTime}">
<f:convertDateTime pattern="dd/MM/yyyy à HH:mm:ss" timeZone="Europe/Paris" type="both"/>
</h:outputText>
</p>
</div>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Émis le</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-clock text-blue-500"></i>
<p class="text-700 m-0 text-sm">
<h:outputText value="#{userSessionBean.issuedAt}">
<f:convertDateTime pattern="dd/MM/yyyy à HH:mm:ss" timeZone="Europe/Paris" type="both"/>
</h:outputText>
</p>
</div>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Realm Keycloak</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-globe text-purple-500"></i>
<p class="text-700 m-0 text-sm font-semibold">
lions-user-manager
</p>
</div>
</div>
<div class="mb-3">
<label class="block text-600 font-medium mb-1 text-sm">Durée de validité</label>
<div class="flex align-items-center gap-2">
<i class="pi pi-hourglass text-green-500"></i>
<p class="text-700 m-0 text-sm">
Session active
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ================================================================
STATISTIQUES D'ACTIVITÉ
================================================================ -->
<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-chart-line text-green-500"></i>
Statistiques d'Activité
</h3>
<div class="grid">
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between mb-2">
<span class="text-600 font-medium text-sm">Connexions</span>
<i class="pi pi-sign-in text-blue-500"></i>
</div>
<p class="text-900 font-bold text-2xl m-0">--</p>
<small class="text-500">Total des connexions</small>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between mb-2">
<span class="text-600 font-medium text-sm">Dernière connexion</span>
<i class="pi pi-clock text-green-500"></i>
</div>
<p class="text-900 font-bold text-xl m-0">Aujourd'hui</p>
<small class="text-500">Session en cours</small>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between mb-2">
<span class="text-600 font-medium text-sm">Actions</span>
<i class="pi pi-history text-orange-500"></i>
</div>
<p class="text-900 font-bold text-2xl m-0">--</p>
<small class="text-500">Actions effectuées</small>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="surface-50 border-round p-3">
<div class="flex align-items-center justify-content-between mb-2">
<span class="text-600 font-medium text-sm">Sessions</span>
<i class="pi pi-desktop text-purple-500"></i>
</div>
<p class="text-900 font-bold text-2xl m-0">1</p>
<small class="text-500">Session active</small>
</div>
</div>
</div>
</div>
</div>
<!-- ================================================================
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-cog text-gray-500"></i>
Actions Rapides
</h3>
<h:form id="formProfileActions">
<div class="grid">
<!-- Gestion du Profil -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 h-full">
<h4 class="text-900 font-semibold mb-3 flex align-items-center gap-2">
<i class="pi pi-user text-blue-500"></i>
<span>Gestion du Profil</span>
</h4>
<div class="flex flex-column gap-2">
<p:commandButton value="Modifier mon profil"
icon="pi pi-pencil"
styleClass="p-button-outlined w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité gérée par Keycloak"/>
</p:commandButton>
<p:commandButton value="Changer mon mot de passe"
icon="pi pi-key"
styleClass="p-button-outlined w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Utilisez le portail Keycloak"/>
</p:commandButton>
<p:commandButton value="Paramètres de sécurité"
icon="pi pi-shield"
styleClass="p-button-outlined w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton>
</div>
</div>
</div>
<!-- Gestion des Sessions -->
<div class="col-12 md:col-6">
<div class="surface-50 border-round p-3 h-full">
<h4 class="text-900 font-semibold mb-3 flex align-items-center gap-2">
<i class="pi pi-desktop text-purple-500"></i>
<span>Sessions et Sécurité</span>
</h4>
<div class="flex flex-column gap-2">
<p:commandButton value="Voir mes sessions actives"
icon="pi pi-desktop"
styleClass="p-button-outlined p-button-info w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton>
<p:commandButton value="Historique des connexions"
icon="pi pi-history"
styleClass="p-button-outlined p-button-secondary w-full justify-content-start"
disabled="true">
<f:attribute name="data-tooltip" value="Fonctionnalité à venir"/>
</p:commandButton>
<p:commandButton value="Se déconnecter"
icon="pi pi-sign-out"
styleClass="p-button-danger w-full justify-content-start"
action="#{userSessionBean.logout}">
<p:confirm header="Confirmation de déconnexion"
message="Êtes-vous sûr de vouloir vous déconnecter ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</div>
</div>
</div>
</div>
</h:form>
</div>
</div>
</div>
<!-- ================================================================
DIALOG DE CONFIRMATION
================================================================ -->
<!-- Animation CSS pour le badge "Connecté" -->
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,85 @@
<!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:fr="http://primefaces.org/freya"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core" template="/templates/main-template.xhtml">
<f:metadata>
<f:viewParam name="userId" value="#{userProfilBean.userId}" />
<f:viewParam name="realm" value="#{userProfilBean.realmName}" />
</f:metadata>
<ui:param name="page" value="#{userProfilBean}" />
<ui:define name="title">Profil Utilisateur - Lions User Manager</ui:define>
<ui:define name="content">
<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-user text-blue-500" style="font-size: 2rem"></i>
<div>
<h3 class="m-0 mb-1">Profil de l'Utilisateur</h3>
<p class="text-600 m-0">Détails et informations de l'utilisateur</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
</h:link>
</div>
</div>
</div>
<!-- ================================================================
CARTE PROFIL PRINCIPAL & ACTIONS
================================================================ -->
<ui:fragment rendered="#{userProfilBean.user != null}">
<div class="col-12">
<div class="card p-0 overflow-hidden">
<ui:include src="/templates/components/user-management/user-card.xhtml">
<ui:param name="user" value="#{userProfilBean.user}" />
<ui:param name="showActions" value="true" />
<ui:param name="layout" value="horizontal" />
<ui:param name="showOrganisation" value="true" />
<ui:param name="showRoles" value="true" />
<ui:param name="clickable" value="false" />
<ui:param name="actionBean" value="#{userProfilBean}" />
<ui:param name="showEdit" value="true" />
<ui:param name="editOutcome" value="/pages/user-manager/users/edit" />
<ui:param name="hasDeleteAction" value="true" />
<ui:param name="deleteAction" value="#{userProfilBean.deleteUser}" />
</ui:include>
</div>
</div>
</ui:fragment>
<ui:fragment rendered="#{userProfilBean.user == null}">
<div class="col-12">
<div class="card">
<div class="text-center p-5">
<i class="pi pi-exclamation-triangle text-orange-500" style="font-size: 3rem"></i>
<h3 class="text-900 font-semibold mt-3 mb-2">Utilisateur non trouvé</h3>
<p class="text-600">L'utilisateur demandé n'existe pas ou n'a pas pu être chargé.</p>
<h:link outcome="/pages/user-manager/users/list" styleClass="p-button mt-3">
<i class="pi pi-arrow-left mr-2"></i>
Retour à la liste
</h:link>
</div>
</div>
</div>
</ui:fragment>
</div>
<!-- Messages -->
<p:growl id="growlMessages" showDetail="true" life="5000">
<p:autoUpdate />
</p:growl>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:composite="http://xmlns.jcp.org/jsf/composite"
xmlns:p="http://primefaces.org/ui"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<composite:interface>
<composite:attribute name="userId" type="java.lang.String" required="true"/>
<composite:attribute name="userEnabled" type="java.lang.Boolean" required="false" default="true"/>
<composite:attribute name="update" type="java.lang.String" required="false" default="@form"/>
<composite:attribute name="showView" type="java.lang.Boolean" required="false" default="true"/>
<composite:attribute name="showEdit" type="java.lang.Boolean" required="false" default="true"/>
<composite:attribute name="showDelete" type="java.lang.Boolean" required="false" default="true"/>
<composite:attribute name="showActivate" type="java.lang.Boolean" required="false" default="true"/>
<composite:attribute name="showDeactivate" type="java.lang.Boolean" required="false" default="true"/>
<composite:attribute name="showResetPassword" type="java.lang.Boolean" required="false" default="true"/>
<composite:attribute name="viewPage" type="java.lang.String" required="false" default="/pages/user-manager/users/profile"/>
<composite:attribute name="editPage" type="java.lang.String" required="false" default="/pages/user-manager/users/edit"/>
<composite:attribute name="activateAction"
method-signature="void activateAction(jakarta.faces.event.ActionEvent)"
required="false"/>
<composite:attribute name="deactivateAction"
method-signature="void deactivateAction(jakarta.faces.event.ActionEvent)"
required="false"/>
<composite:attribute name="deleteAction"
method-signature="void deleteAction(jakarta.faces.event.ActionEvent)"
required="false"/>
</composite:interface>
<composite:implementation>
<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; padding: 0; margin: 0;">
<p:menu styleClass="w-12rem">
<c:if test="#{cc.attrs.showView}">
<p:menuitem
value="Voir le profil"
icon="pi pi-eye"
outcome="#{cc.attrs.viewPage}">
<f:param name="userId" value="#{cc.attrs.userId}" />
</p:menuitem>
</c:if>
<c:if test="#{cc.attrs.showEdit}">
<p:menuitem
value="Modifier"
icon="pi pi-pencil"
outcome="#{cc.attrs.editPage}">
<f:param name="userId" value="#{cc.attrs.userId}" />
</p:menuitem>
</c:if>
<c:if test="#{cc.attrs.showResetPassword}">
<p:menuitem
value="Réinitialiser mot de passe"
icon="pi pi-key"
onclick="PF('resetPasswordDialog').show()" />
</c:if>
<p:separator />
<c:if test="#{cc.attrs.showActivate and !cc.attrs.userEnabled}">
<c:if test="#{not empty cc.attrs.activateAction}">
<p:menuitem
value="Activer"
icon="pi pi-check"
styleClass="text-green-600"
actionListener="#{cc.attrs.activateAction}">
<f:attribute name="userId" value="#{cc.attrs.userId}" />
<p:ajax update="#{cc.attrs.update}" />
</p:menuitem>
</c:if>
</c:if>
<c:if test="#{cc.attrs.showDeactivate and cc.attrs.userEnabled}">
<c:if test="#{not empty cc.attrs.deactivateAction}">
<p:menuitem
value="Désactiver"
icon="pi pi-times"
styleClass="text-orange-600"
actionListener="#{cc.attrs.deactivateAction}">
<f:attribute name="userId" value="#{cc.attrs.userId}" />
<p:ajax update="#{cc.attrs.update}" />
</p:menuitem>
</c:if>
</c:if>
<c:if test="#{cc.attrs.showDelete}">
<c:if test="#{not empty cc.attrs.deleteAction}">
<p:separator />
<p:menuitem
value="Supprimer"
icon="pi pi-trash"
styleClass="text-red-600"
actionListener="#{cc.attrs.deleteAction}">
<f:attribute name="userId" value="#{cc.attrs.userId}" />
<p:ajax update="#{cc.attrs.update}" />
</p:menuitem>
</c:if>
</c:if>
</p:menu>
</p:commandButton>
</composite:implementation>
</html>

View File

@@ -0,0 +1,625 @@
/* ============================================================================
Lions User Manager - Enhanced Custom Topbar Styles
Auteur: Lions User Manager
Version: 2.0.0
Description: Styles améliorés pour la topbar avec intégration intelligente
des patterns Freya layout pour un rendu parfait
Intégrations:
- Freya Layout Variables & Patterns
- Support Dark/Light Theme
- Animations fluides (fadeInDown, modal-in)
- PrimeFlex utility classes
- Responsive design
============================================================================ */
/* ----------------------------------------------------------------------------
BASE TOPBAR LAYOUT OVERRIDES
Améliore la structure de base de la topbar Freya
---------------------------------------------------------------------------- */
.layout-topbar {
position: fixed;
top: 0;
z-index: 999;
width: 100%;
height: 62px;
transition: width 0.2s, box-shadow 0.3s ease;
}
.layout-topbar .layout-topbar-wrapper {
height: 100%;
display: flex;
align-items: center;
}
.layout-topbar .layout-topbar-wrapper .layout-topbar-right {
height: 100%;
flex-grow: 1;
padding: 0 16px 0 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.layout-topbar .layout-topbar-wrapper .layout-topbar-right .layout-topbar-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-grow: 1;
list-style-type: none;
margin: 0;
padding: 0;
height: 100%;
}
.layout-topbar .layout-topbar-wrapper .layout-topbar-right .layout-topbar-actions > li {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
/* ----------------------------------------------------------------------------
USER PROFILE LINK - Enhanced with Freya patterns
---------------------------------------------------------------------------- */
.layout-topbar .user-profile-link {
display: flex !important;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 6px;
transition: all 0.2s cubic-bezier(0.05, 0.74, 0.2, 0.99);
text-decoration: none;
cursor: pointer;
position: relative;
overflow: hidden;
}
.layout-topbar .user-profile-link::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
opacity: 0;
transition: opacity 0.2s ease;
}
.layout-topbar .user-profile-link:hover::before {
opacity: 1;
}
.layout-topbar .user-profile-link:hover {
background-color: rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* User Avatar - Integration with Freya avatar patterns */
.layout-topbar .user-profile-link .user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.layout-topbar .user-profile-link:hover .user-avatar {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* User Info Container */
.layout-topbar .user-info {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.2;
min-width: 0;
}
/* User Name - Enhanced typography */
.layout-topbar .user-name {
font-weight: 600;
font-size: 0.875rem;
color: var(--text-color);
margin-bottom: 0.125rem;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
/* User Email */
.layout-topbar .user-email {
font-size: 0.75rem;
color: var(--text-color-secondary);
opacity: 0.85;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
/* User Role Badge */
.layout-topbar .user-role {
font-size: 0.7rem;
color: var(--primary-color);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
background: rgba(var(--primary-color-rgb, 79, 142, 236), 0.1);
padding: 0.125rem 0.375rem;
border-radius: 4px;
white-space: nowrap;
}
.layout-topbar .user-separator {
color: var(--text-color-secondary);
opacity: 0.5;
font-weight: 300;
margin: 0 0.25rem;
}
/* Online Status Indicator */
.layout-topbar .user-status {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
position: relative;
}
.layout-topbar .user-status.online {
background-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
animation: pulse-online 2s ease-in-out infinite;
}
@keyframes pulse-online {
0%, 100% {
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
}
50% {
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.2);
}
}
/* ----------------------------------------------------------------------------
USER DROPDOWN MENU - Enhanced with Freya dropdown patterns
---------------------------------------------------------------------------- */
.layout-topbar .user-dropdown-menu {
display: none;
position: absolute;
top: 62px;
right: 0;
min-width: 280px;
max-width: 320px;
padding: 0;
margin: 0;
list-style-type: none;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08);
border: 1px solid var(--surface-border);
background: var(--surface-card);
overflow: hidden;
z-index: 1000;
animation-duration: 0.2s;
animation-timing-function: cubic-bezier(0.05, 0.74, 0.2, 0.99);
animation-fill-mode: forwards;
}
/* Show dropdown when parent is active */
.layout-topbar .user-profile.active-topmenuitem > .user-dropdown-menu {
display: block;
animation-name: fadeInDown;
}
/* Dropdown Header - Integration with Freya gradient patterns */
.user-dropdown-header {
padding: 1.25rem 1rem;
background: linear-gradient(135deg, var(--primary-color), var(--primary-600, #387FE9));
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
position: relative;
overflow: hidden;
}
.user-dropdown-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-20%, -20%); }
}
/* Dropdown Avatar */
.user-dropdown-avatar {
position: relative;
flex-shrink: 0;
}
.user-dropdown-avatar > div {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.user-status-indicator {
position: absolute;
bottom: 2px;
right: 2px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid white;
}
.user-status-indicator.online {
background-color: #4CAF50;
animation: pulse-indicator 2s ease-in-out infinite;
}
@keyframes pulse-indicator {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
/* Dropdown User Info */
.user-dropdown-info {
flex: 1;
min-width: 0;
}
.user-dropdown-name {
font-weight: 600;
font-size: 1rem;
margin-bottom: 0.25rem;
color: white;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-dropdown-email {
font-size: 0.875rem;
opacity: 0.95;
margin-bottom: 0.25rem;
color: rgba(255, 255, 255, 0.95);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-dropdown-role {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
background: rgba(255, 255, 255, 0.25);
padding: 0.25rem 0.5rem;
border-radius: 12px;
display: inline-block;
color: white;
backdrop-filter: blur(10px);
}
/* Dividers */
.user-dropdown-divider {
height: 1px;
background-color: var(--surface-border);
margin: 0;
}
/* Menu Sections */
.user-dropdown-section {
padding: 0.75rem 0;
}
.section-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-color-secondary);
padding: 0 1rem 0.5rem 1rem;
margin-bottom: 0.25rem;
}
.section-items {
display: flex;
flex-direction: column;
}
/* Dropdown Items - Enhanced with Freya interaction patterns */
.dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--text-color);
text-decoration: none;
transition: all 0.2s cubic-bezier(0.05, 0.74, 0.2, 0.99);
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
position: relative;
overflow: hidden;
}
.dropdown-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 3px;
height: 100%;
background: var(--primary-color);
transform: scaleX(0);
transition: transform 0.2s ease;
}
.dropdown-item:hover::before {
transform: scaleX(1);
}
.dropdown-item:hover {
background-color: var(--surface-hover);
color: var(--primary-color);
padding-left: 1.25rem;
}
.dropdown-item:active {
background-color: var(--surface-ground);
transform: scale(0.98);
}
.dropdown-item i {
width: 1.25rem;
text-align: center;
color: var(--text-color-secondary);
transition: all 0.2s ease;
font-size: 1rem;
}
.dropdown-item:hover i {
color: var(--primary-color);
transform: scale(1.1);
}
.dropdown-item span {
flex: 1;
}
.item-arrow {
margin-left: auto;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
font-size: 0.75rem;
}
.dropdown-item:hover .item-arrow {
opacity: 1;
transform: translateX(4px);
}
/* Logout Item - Enhanced danger state */
.logout-item {
color: var(--red-500) !important;
margin-top: 0.25rem;
}
.logout-item:hover {
background-color: var(--red-50) !important;
color: var(--red-600) !important;
}
.logout-item i {
color: var(--red-500) !important;
}
.logout-item:hover i {
color: var(--red-600) !important;
transform: scale(1.1) rotate(-5deg);
}
/* ----------------------------------------------------------------------------
ANIMATIONS - Integration with Freya animation patterns
---------------------------------------------------------------------------- */
@keyframes dropdownFadeIn {
0% {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.user-dropdown-menu {
animation: dropdownFadeIn 0.3s ease-out;
transform-origin: top right;
}
/* ----------------------------------------------------------------------------
DARK MODE SUPPORT - Integration with Freya dark theme
---------------------------------------------------------------------------- */
.layout-wrapper.layout-topbar-dark .layout-topbar {
background-color: #293241;
box-shadow: 0 10px 40px 0 rgba(0, 0, 0, 0.2);
}
.layout-wrapper.layout-topbar-dark .user-profile-link:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.layout-wrapper.layout-topbar-dark .user-dropdown-menu {
background: var(--surface-900, #1E1E1E);
border-color: var(--surface-700, #383838);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.3);
}
.layout-wrapper.layout-topbar-dark .user-dropdown-divider {
background-color: var(--surface-700, #383838);
}
.layout-wrapper.layout-topbar-dark .section-title {
color: var(--text-color-secondary);
opacity: 0.8;
}
.layout-wrapper.layout-topbar-dark .dropdown-item {
color: var(--text-color);
}
.layout-wrapper.layout-topbar-dark .dropdown-item:hover {
background-color: var(--surface-800, #2A2A2A);
}
.layout-wrapper.layout-topbar-dark .logout-item:hover {
background-color: rgba(211, 47, 47, 0.1) !important;
}
/* ----------------------------------------------------------------------------
RESPONSIVE DESIGN - Integration with Freya responsive patterns
---------------------------------------------------------------------------- */
@media (max-width: 991px) {
.layout-topbar .user-dropdown-menu {
left: 10px;
right: 10px;
position: fixed;
top: 62px;
max-width: none;
}
}
@media (max-width: 768px) {
.layout-topbar .user-dropdown-menu {
min-width: 260px;
max-width: 280px;
}
.user-dropdown-header {
padding: 1rem 0.75rem;
}
.dropdown-item {
padding: 0.625rem 0.75rem;
font-size: 0.8125rem;
}
.section-title {
padding: 0 0.75rem 0.5rem 0.75rem;
}
/* Hide user info on mobile */
.layout-topbar .user-info {
display: none;
}
.layout-topbar .user-profile-link {
padding: 0.5rem;
}
}
@media (max-width: 576px) {
.layout-topbar .user-dropdown-menu {
left: 8px;
right: 8px;
border-radius: 12px;
}
}
/* ----------------------------------------------------------------------------
ACCESSIBILITY ENHANCEMENTS
---------------------------------------------------------------------------- */
.dropdown-item:focus,
.user-profile-link:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.layout-topbar .user-profile-link,
.dropdown-item,
.user-dropdown-menu,
.user-avatar,
.item-arrow {
animation: none;
transition: none;
}
}
/* ----------------------------------------------------------------------------
UTILITY CLASSES - PrimeFlex integration
---------------------------------------------------------------------------- */
.layout-topbar .flex {
display: flex !important;
}
.layout-topbar .align-items-center {
align-items: center !important;
}
.layout-topbar .justify-content-center {
justify-content: center !important;
}
.layout-topbar .gap-2 {
gap: 0.5rem !important;
}
.layout-topbar .text-white {
color: white !important;
}
.layout-topbar .border-circle {
border-radius: 50% !important;
}
.layout-topbar .bg-primary {
background-color: var(--primary-color) !important;
}

View File

@@ -0,0 +1,795 @@
/*
* ╔════════════════════════════════════════════════════════════╗
* ║ Lions Platform Elite Topbar Styles (Freya Design System) ║
* ║ Modern, Professional, Responsive ║
* ╚════════════════════════════════════════════════════════════╝
*/
/* ═══════════════════════════════════════════════════════════ */
/* BASE TOPBAR */
/* ═══════════════════════════════════════════════════════════ */
.unionflow-elite,
.lions-elite {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-600) 100%);
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
position: relative;
z-index: 1000;
}
.unionflow-elite::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg,
transparent 0%,
var(--primary-300) 50%,
transparent 100%);
opacity: 0.5;
}
/* App Version Badge */
.app-version {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.625rem;
background: rgba(255,255,255,0.15);
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
color: rgba(255,255,255,0.9);
margin-left: 0.75rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
}
/* ═══════════════════════════════════════════════════════════ */
/* SEARCH */
/* ═══════════════════════════════════════════════════════════ */
.search-item .topbar-icon {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-item:hover .topbar-icon {
transform: scale(1.1);
color: var(--primary-100);
}
.search-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 400px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
padding: 1rem;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--surface-border);
}
.search-item:hover .search-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.search-wrapper-elite {
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--surface-50);
border-radius: 8px;
padding: 0.5rem 1rem;
border: 1px solid var(--surface-border);
transition: all 0.3s ease;
}
.search-wrapper-elite:focus-within {
background: white;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.1);
}
.search-wrapper-elite .pi-search {
color: var(--text-color-secondary);
font-size: 1rem;
}
.search-wrapper-elite .search-input {
flex: 1;
border: none;
background: transparent;
padding: 0.5rem 0;
font-size: 0.875rem;
}
.search-wrapper-elite .search-input:focus {
outline: none;
box-shadow: none;
}
/* ═══════════════════════════════════════════════════════════ */
/* NOTIFICATIONS */
/* ═══════════════════════════════════════════════════════════ */
.notifications-item {
position: relative;
}
.badge-count {
position: absolute;
top: -4px;
right: -4px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
font-size: 0.625rem;
font-weight: 700;
padding: 0.125rem 0.375rem;
border-radius: 10px;
min-width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
animation: pulse-badge 2s infinite;
}
@keyframes pulse-badge {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.notifications-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 360px;
max-width: 400px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--surface-border);
overflow: hidden;
}
.notifications-item:hover .notifications-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.notif-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
background: var(--surface-50);
border-bottom: 1px solid var(--surface-border);
}
.count-label {
font-size: 0.75rem;
color: var(--text-color-secondary);
background: var(--primary-color);
color: white;
padding: 0.25rem 0.625rem;
border-radius: 12px;
font-weight: 600;
}
.notif-item {
display: flex;
align-items: flex-start;
gap: 0.875rem;
padding: 0.875rem 1.25rem;
transition: all 0.2s ease;
cursor: pointer;
}
.notif-item:hover {
background: var(--surface-50);
}
.notif-item i {
font-size: 1.25rem;
margin-top: 0.25rem;
}
.notif-title {
font-weight: 600;
color: var(--text-color);
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.notif-time {
font-size: 0.75rem;
color: var(--text-color-secondary);
}
.notif-footer {
padding: 0.75rem 1.25rem;
text-align: center;
border-top: 1px solid var(--surface-border);
background: var(--surface-50);
}
.notif-footer a {
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
transition: color 0.2s ease;
}
.notif-footer a:hover {
color: var(--primary-600);
}
/* ═══════════════════════════════════════════════════════════ */
/* USER PROFILE */
/* ═══════════════════════════════════════════════════════════ */
.elite-user .profile-trigger {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.875rem;
border-radius: 10px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
}
.elite-user .profile-trigger:hover {
background: rgba(255,255,255,0.2);
transform: translateY(-1px);
}
.avatar-container {
position: relative;
}
.avatar {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 700;
color: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border: 2px solid rgba(255,255,255,0.3);
}
.bg-gradient-primary {
background: linear-gradient(135deg, var(--primary-400) 0%, var(--primary-600) 100%);
}
.status-dot {
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid white;
}
.status-dot.online {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
0%, 100% { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); }
50% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.4); }
}
.user-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.user-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-name {
font-size: 0.875rem;
font-weight: 600;
color: white;
line-height: 1.2;
}
.role-badge {
font-size: 0.625rem;
padding: 0.125rem 0.5rem;
background: rgba(255,255,255,0.25);
border-radius: 8px;
font-weight: 600;
color: white;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.session-timer {
display: flex;
align-items: center;
gap: 0.375rem;
}
.icon-sm {
font-size: 0.7rem;
}
.timer-text {
font-size: 0.75rem;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.arrow {
font-size: 0.75rem;
color: rgba(255,255,255,0.8);
transition: transform 0.3s ease;
}
.elite-user:hover .arrow {
transform: rotate(180deg);
}
/* ═══════════════════════════════════════════════════════════ */
/* USER DROPDOWN MENU */
/* ═══════════════════════════════════════════════════════════ */
.elite-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 340px;
background: white;
border-radius: 16px;
box-shadow: 0 12px 48px rgba(0,0,0,0.15);
opacity: 0;
visibility: hidden;
transform: translateY(-10px) scale(0.95);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--surface-border);
overflow: hidden;
}
.elite-user:hover .elite-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
}
/* Dropdown Header */
.dropdown-header {
padding: 1.25rem;
background: linear-gradient(135deg, var(--primary-50) 0%, var(--surface-50) 100%);
border-bottom: 1px solid var(--surface-border);
}
.header-content {
display: flex;
gap: 1rem;
}
.header-avatar {
position: relative;
}
.avatar-lg {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: 700;
color: white;
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
}
.status-indicator {
position: absolute;
bottom: 2px;
right: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
border: 3px solid white;
display: flex;
align-items: center;
justify-content: center;
}
.status-indicator.online {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
.status-indicator i {
font-size: 6px;
color: white;
}
.header-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
justify-content: center;
}
.header-info .name {
font-size: 1rem;
font-weight: 700;
color: var(--text-color);
line-height: 1.3;
}
.header-info .email {
font-size: 0.75rem;
color: var(--text-color-secondary);
line-height: 1.3;
}
.role-tag {
display: inline-flex;
align-items: center;
font-size: 0.625rem;
padding: 0.25rem 0.625rem;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-600) 100%);
color: white;
border-radius: 8px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
align-self: flex-start;
margin-top: 0.25rem;
}
/* Session Card */
.session-card {
padding: 1rem 1.25rem;
background: var(--surface-50);
border-bottom: 1px solid var(--surface-border);
}
.card-content {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-row .label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-color-secondary);
font-weight: 500;
}
.info-row .label i {
font-size: 0.875rem;
}
.info-row .value {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-color);
}
/* Progress Bar */
.progress-container {
margin-top: 0.5rem;
}
.progress-bar {
height: 6px;
background: var(--surface-200);
border-radius: 10px;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--green-400) 0%, var(--green-500) 100%);
border-radius: 10px;
transition: width 1s ease, background 0.3s ease;
}
.progress-fill[style*="width: 0%"],
.progress-fill[style*="width: 1%"],
.progress-fill[style*="width: 2%"],
.progress-fill[style*="width: 3%"],
.progress-fill[style*="width: 4%"],
.progress-fill[style*="width: 5%"] {
background: linear-gradient(90deg, var(--red-400) 0%, var(--red-500) 100%);
}
.progress-label {
font-size: 0.625rem;
color: var(--text-color-secondary);
margin-top: 0.375rem;
text-align: right;
font-weight: 500;
}
/* Menu Sections */
.divider {
height: 1px;
background: var(--surface-border);
margin: 0;
}
.menu-section {
padding: 0.75rem 0;
}
.menu-section.compact {
padding: 0.5rem 0;
}
.section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--text-color-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 0.5rem 1.25rem 0.75rem;
}
.section-items {
display: flex;
flex-direction: column;
}
.menu-item {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 0.75rem 1.25rem;
color: var(--text-color);
text-decoration: none;
transition: all 0.2s ease;
font-size: 0.875rem;
cursor: pointer;
border: none;
background: transparent;
width: 100%;
text-align: left;
}
.menu-item:hover {
background: var(--surface-100);
}
.menu-item i:first-child {
font-size: 1rem;
color: var(--text-color-secondary);
transition: all 0.2s ease;
}
.menu-item:hover i:first-child {
color: var(--primary-color);
transform: translateX(2px);
}
.menu-item span {
flex: 1;
font-weight: 500;
}
.arrow-right {
font-size: 0.75rem;
color: var(--text-color-secondary);
margin-left: auto;
transition: transform 0.2s ease;
}
.menu-item:hover .arrow-right {
transform: translateX(3px);
}
.item-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 0.375rem;
background: var(--primary-color);
color: white;
font-size: 0.625rem;
font-weight: 700;
border-radius: 10px;
margin-left: auto;
}
.value-badge {
font-size: 0.75rem;
color: var(--text-color-secondary);
background: var(--surface-100);
padding: 0.25rem 0.625rem;
border-radius: 8px;
font-weight: 600;
margin-left: auto;
}
/* Logout Section */
.logout-divider {
background: linear-gradient(90deg,
transparent 0%,
var(--red-200) 50%,
transparent 100%);
height: 2px;
}
.logout-section {
padding: 0.75rem 0;
background: linear-gradient(to bottom, white 0%, var(--red-50) 100%);
}
.logout-btn {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 0.875rem 1.25rem;
color: var(--red-600);
font-weight: 600;
font-size: 0.875rem;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
border: none;
background: transparent;
width: 100%;
text-align: left;
}
.logout-btn:hover {
background: var(--red-100);
color: var(--red-700);
}
.logout-btn i:first-child {
font-size: 1rem;
transition: transform 0.3s ease;
}
.logout-btn:hover i:first-child {
transform: scale(1.1) rotate(-10deg);
}
.logout-btn .pi-lock {
font-size: 0.875rem;
}
/* ═══════════════════════════════════════════════════════════ */
/* LOGOUT DIALOG */
/* ═══════════════════════════════════════════════════════════ */
.elite-dialog .dialog-content {
text-align: center;
padding: 1.5rem 1rem;
}
.icon-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, var(--red-50) 0%, var(--red-100) 100%);
margin-bottom: 1.5rem;
}
.icon-lg {
font-size: 2.5rem;
color: var(--red-500);
}
.dialog-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-color);
margin-bottom: 1.5rem;
line-height: 1.4;
}
.info-box {
background: var(--surface-50);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid var(--surface-border);
}
.info-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem;
color: var(--text-color);
font-size: 0.875rem;
}
.info-item i {
color: var(--primary-color);
font-size: 1rem;
}
.warning-text {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-color-secondary);
margin: 0;
}
.warning-text i {
color: var(--blue-500);
}
.dialog-footer {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
padding-top: 1rem;
}
/* ═══════════════════════════════════════════════════════════ */
/* UTILITY CLASSES */
/* ═══════════════════════════════════════════════════════════ */
.text-green-600 { color: #059669 !important; }
.text-yellow-600 { color: #d97706 !important; }
.text-orange-600 { color: #ea580c !important; }
.text-red-600 { color: #dc2626 !important; }
/* ═══════════════════════════════════════════════════════════ */
/* RESPONSIVE */
/* ═══════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
.app-version { display: none; }
.user-info { display: none; }
.elite-dropdown { min-width: 300px; }
.search-dropdown { min-width: 280px; }
}

View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:f="jakarta.faces.core"
xmlns:ui="jakarta.faces.facelets"
xmlns:p="http://primefaces.org/ui">
<h:head>
<f:facet name="first">
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
</f:facet>
<title><ui:insert name="title">Lions User Manager</ui:insert></title>
<style>
body {
background-color: var(--surface-b);
color: var(--text-color);
margin: 0;
font-family: var(--font-family);
-webkit-font-smoothing: antialiased;
}
.layout-wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.layout-topbar {
height: 4rem;
padding: 0 2rem;
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--surface-a);
box-shadow: 0 2px 4px -1px rgba(0,0,0,.15);
z-index: 1000;
}
.layout-content {
padding: 2rem;
flex: 1 1 auto;
}
.layout-footer {
padding: 1rem 2rem;
background-color: var(--surface-a);
text-align: center;
}
h1 { margin-top: 0; }
.card {
background: var(--surface-card);
padding: 2rem;
border-radius: 10px;
margin-bottom: 2rem;
box-shadow: 0 2px 1px -1px rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 1px 3px 0 rgba(0,0,0,.12);
}
.mr-2 { margin-right: 0.5rem; }
</style>
<ui:insert name="head"/>
</h:head>
<h:body>
<div class="layout-wrapper">
<div class="layout-topbar">
<h:link outcome="/index" style="text-decoration: none; color: inherit; font-weight: bold; font-size: 1.5rem;">
🦁 Lions User Manager
</h:link>
<h:form>
<p:menubar style="border:none; background:transparent;">
<p:menuitem value="Dashboard" outcome="/index" icon="pi pi-home"/>
<p:menuitem value="Utilisateurs" outcome="/pages/user-manager/users" icon="pi pi-users"/>
<p:menuitem value="Rôles" outcome="/pages/user-manager/roles" icon="pi pi-lock"/>
<p:menuitem value="Déconnexion" url="/auth/logout" icon="pi pi-power-off"/>
</p:menubar>
</h:form>
</div>
<div class="layout-content">
<ui:insert name="content"/>
</div>
<div class="layout-footer">
<span>Copyright 2024 - Lions User Manager Team</span>
</div>
</div>
<p:growl id="growl" showDetail="true" life="3000">
<p:autoUpdate/>
</p:growl>
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade" responsive="true" width="350">
<p:commandButton value="Non" type="button" styleClass="ui-confirmdialog-no ui-button-flat"/>
<p:commandButton value="Oui" type="button" styleClass="ui-confirmdialog-yes" />
</p:confirmDialog>
</h:body>
</html>

View File

@@ -1,21 +1,19 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Ligne de Log d'Audit (WOU/DRY Pattern)
Composant réutilisable: Ligne de Log Audit - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Affiche une ligne de log d'audit avec informations détaillées
Version: 2.0.0
Description: Affiche une ligne de log d'audit avec icône de statut, informations
principales et détails extensibles. Compatible avec les thèmes PrimeFaces.
Paramètres:
- auditLog: AuditLogDTO (requis) - Le log d'audit à afficher
- showDetails: Boolean (défaut: false) - Afficher les détails complets
- showActions: Boolean (défaut: false) - Afficher les actions possibles
- showDetails: Boolean (défaut: false) - Afficher les détails complets en ligne
- showActions: Boolean (défaut: false) - Afficher les boutons d'action
- compact: Boolean (défaut: false) - Mode compact
- styleClass: String (optionnel) - Classes CSS supplémentaires
@@ -26,85 +24,116 @@
<ui:param name="auditLog" value="#{log}" />
</ui:include>
2. Ligne avec détails:
2. Ligne avec détails et actions:
<ui:include src="/templates/components/audit/audit-log-row.xhtml">
<ui:param name="auditLog" value="#{log}" />
<ui:param name="showDetails" value="true" />
<ui:param name="showActions" value="true" />
</ui:include>
-->
<c:set var="showDetails" value="#{empty showDetails ? false : showDetails}" />
<c:set var="showActions" value="#{empty showActions ? false : showActions}" />
<c:set var="compact" value="#{empty compact ? false : compact}" />
<!-- Déterminer la severity selon le succès -->
<c:set var="severity" value="#{auditLog.succes ? 'success' : 'danger'}" />
<c:set var="icon" value="#{auditLog.succes ? 'pi-check-circle' : 'pi-times-circle'}" />
<div class="audit-log-row flex align-items-center gap-3 p-3 border-round surface-border border-1 #{styleClass}"
style="#{compact ? 'padding: 0.5rem;' : ''}">
<!-- Déterminer le style selon le succès/échec -->
<c:set var="isSuccess" value="#{auditLog.succes}" />
<c:set var="statusColor" value="#{isSuccess ? 'green' : 'red'}" />
<c:set var="statusIcon" value="#{isSuccess ? 'pi-check-circle' : 'pi-times-circle'}" />
<c:set var="statusLabel" value="#{isSuccess ? 'Succès' : 'Échec'}" />
<div
class="audit-log-row flex align-items-start gap-3 p-3 border-round surface-border border-1 hover:surface-hover transition-all transition-duration-200 #{compact ? 'py-2' : ''} #{styleClass}">
<!-- Icône de statut -->
<div class="flex align-items-center justify-content-center"
style="width: 2.5rem; height: 2.5rem; border-radius: 50%; background: var(--#{severity}-100);">
<i class="pi #{icon} text-#{severity}-600"></i>
<div class="flex align-items-center justify-content-center border-round-lg flex-shrink-0"
style="width: 2.5rem; height: 2.5rem; background-color: rgba(var(--#{statusColor}-500-rgb, 0,0,0), 0.1);">
<i class="pi #{statusIcon} text-#{statusColor}-500"></i>
</div>
<!-- Informations principales -->
<div class="flex-1">
<div class="flex align-items-center gap-2 mb-1">
<div class="flex align-items-center gap-2 mb-1 flex-wrap">
<strong class="text-900">#{auditLog.typeAction}</strong>
<p:tag
value="#{auditLog.succes ? 'Succès' : 'Échec'}"
severity="#{severity}"
styleClass="text-xs" />
<p:tag value="#{statusLabel}" severity="#{isSuccess ? 'success' : 'danger'}" styleClass="text-xs" />
</div>
<div class="text-color-secondary text-sm">
<div class="flex align-items-center gap-3 text-color-secondary text-sm flex-wrap">
<c:if test="#{not empty auditLog.acteurUsername}">
<span><i class="pi pi-user mr-1"></i>#{auditLog.acteurUsername}</span>
<span class="flex align-items-center gap-1">
<i class="pi pi-user text-xs"></i>
#{auditLog.acteurUsername}
</span>
</c:if>
<c:if test="#{not empty auditLog.ressourceType}">
<span class="ml-3"><i class="pi pi-database mr-1"></i>#{auditLog.ressourceType}</span>
<span class="flex align-items-center gap-1">
<i class="pi pi-database text-xs"></i>
#{auditLog.ressourceType}
</span>
</c:if>
<c:if test="#{not empty auditLog.dateAction}">
<span class="ml-3"><i class="pi pi-calendar mr-1"></i>#{auditLog.dateAction}</span>
<span class="flex align-items-center gap-1">
<i class="pi pi-clock text-xs"></i>
#{auditLog.dateAction}
</span>
</c:if>
<c:if test="#{not empty auditLog.realmName}">
<span class="flex align-items-center gap-1">
<i class="pi pi-globe text-xs"></i>
#{auditLog.realmName}
</span>
</c:if>
</div>
<!-- Détails (si affichés) -->
<!-- Détails extensibles -->
<c:if test="#{showDetails}">
<div class="mt-2 text-color-secondary text-xs">
<c:if test="#{not empty auditLog.ressourceId}">
<div><strong>Ressource ID:</strong> #{auditLog.ressourceId}</div>
</c:if>
<c:if test="#{not empty auditLog.details}">
<div><strong>Détails:</strong> #{auditLog.details}</div>
</c:if>
<c:if test="#{not empty auditLog.adresseIp}">
<div><strong>IP:</strong> #{auditLog.adresseIp}</div>
</c:if>
<c:if test="#{not empty auditLog.userAgent}">
<div><strong>User Agent:</strong> #{auditLog.userAgent}</div>
</c:if>
<c:if test="#{not empty auditLog.messageErreur}">
<div class="text-red-600"><strong>Erreur:</strong> #{auditLog.messageErreur}</div>
</c:if>
<div class="mt-2 surface-100 border-round p-2">
<div class="grid text-xs text-color-secondary">
<c:if test="#{not empty auditLog.ressourceId}">
<div class="col-12 md:col-6">
<span class="font-semibold">Ressource ID: </span>
<span class="text-900">#{auditLog.ressourceId}</span>
</div>
</c:if>
<c:if test="#{not empty auditLog.adresseIp}">
<div class="col-12 md:col-6">
<span class="font-semibold">Adresse IP: </span>
<span class="text-900">#{auditLog.adresseIp}</span>
</div>
</c:if>
<c:if test="#{not empty auditLog.details}">
<div class="col-12">
<span class="font-semibold">Détails: </span>
<span class="text-900">#{auditLog.details}</span>
</div>
</c:if>
<c:if test="#{not empty auditLog.userAgent}">
<div class="col-12">
<span class="font-semibold">User Agent: </span>
<span class="text-900 text-overflow-ellipsis white-space-nowrap overflow-hidden"
style="max-width: 300px;" title="#{auditLog.userAgent}">
#{auditLog.userAgent}
</span>
</div>
</c:if>
<c:if test="#{not empty auditLog.messageErreur}">
<div class="col-12">
<span class="font-semibold text-red-600">Erreur: </span>
<span class="text-red-700">#{auditLog.messageErreur}</span>
</div>
</c:if>
</div>
</div>
</c:if>
</div>
<!-- Actions (si affichées) -->
<!-- Actions -->
<c:if test="#{showActions}">
<div class="flex gap-1">
<p:commandButton
icon="pi pi-eye"
styleClass="p-button-text p-button-sm"
title="Voir les détails"
onclick="PF('auditLogDetailsDialog').show()" />
<div class="flex gap-1 flex-shrink-0">
<p:commandButton icon="pi pi-eye" styleClass="p-button-text p-button-sm p-button-rounded"
title="Voir les détails" onclick="PF('auditLogDetailsDialog').show()" type="button" />
</div>
</c:if>
</div>
</ui:composition>
</ui:composition>

View File

@@ -1,28 +1,31 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Carte Statistiques Audit (WOU/DRY Pattern)
Composant réutilisable: Carte Statistiques Audit - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Affiche des statistiques d'audit dans une carte
Version: 2.0.0
Description: Wrapper spécialisé autour de kpi-card.xhtml pour le domaine Audit.
Ajoute un contexte sémantique. Pour de nouvelles implémentations,
utiliser directement kpi-card.xhtml.
Paramètres:
- title: String (requis) - Titre de la carte
- value: Number (requis) - Valeur à afficher
- icon: String (requis) - Classe d'icône PrimeIcons
- iconColor: String (requis) - Couleur de l'icône
- icon: String (requis) - Classe d'icône PrimeIcons (sans préfixe "pi-")
- iconColor: String (requis) - Couleur de l'icône (ex: "blue-600")
- subtitle: String (optionnel) - Sous-titre
- trend: Number (optionnel) - Tendance (pourcentage)
- trendLabel: String (optionnel) - Libellé de la tendance
- colSize: String (défaut: "col-12 md:col-6 lg:col-3") - Taille de colonne
- clickable: Boolean (défaut: false) - Rendre la carte cliquable
- clickAction: String (optionnel) - Action au clic
- clickOutcome: String (optionnel) - Page de destination au clic
- statusIcon: String (optionnel) - Icône de statut (déléguée à kpi-card)
- statusLabel: String (optionnel) - Label de statut
- statusValue: String (optionnel) - Valeur de statut
Exemples d'utilisation:
@@ -44,77 +47,25 @@
<ui:param name="trendLabel" value="vs mois dernier" />
</ui:include>
-->
<c:set var="colSize" value="#{empty colSize ? 'col-12 md:col-6 lg:col-3' : colSize}" />
<c:set var="clickable" value="#{empty clickable ? false : clickable}" />
<div class="field #{colSize}">
<c:choose>
<c:when test="#{clickable and not empty clickAction}">
<p:commandLink
styleClass="card-link"
action="#{clickAction}">
<div class="card surface-0 hover:surface-100 border-round-lg transition-all transition-duration-200 p-4">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">#{title}</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi #{icon} text-#{iconColor} text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{value}</div>
<c:if test="#{not empty subtitle}">
<div class="text-500 text-xs mb-2">#{subtitle}</div>
</c:if>
<c:if test="#{not empty trend}">
<div class="flex align-items-center">
<i class="pi pi-arrow-#{trend >= 0 ? 'up' : 'down'} text-#{trend >= 0 ? 'green' : 'red'}-500 text-sm mr-2"></i>
<span class="text-#{trend >= 0 ? 'green' : 'red'}-600 font-semibold text-sm mr-2">
#{trend >= 0 ? '+' : ''}#{trend}%
</span>
<c:if test="#{not empty trendLabel}">
<span class="text-500 text-xs">#{trendLabel}</span>
</c:if>
</div>
</c:if>
</div>
</p:commandLink>
</c:when>
<c:otherwise>
<div class="card surface-0 border-round-lg p-4">
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">#{title}</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi #{icon} text-#{iconColor} text-lg"></i>
</div>
</div>
<div class="text-900 font-bold text-2xl mb-2">#{value}</div>
<c:if test="#{not empty subtitle}">
<div class="text-500 text-xs mb-2">#{subtitle}</div>
</c:if>
<c:if test="#{not empty trend}">
<div class="flex align-items-center">
<i class="pi pi-arrow-#{trend >= 0 ? 'up' : 'down'} text-#{trend >= 0 ? 'green' : 'red'}-500 text-sm mr-2"></i>
<span class="text-#{trend >= 0 ? 'green' : 'red'}-600 font-semibold text-sm mr-2">
#{trend >= 0 ? '+' : ''}#{trend}%
</span>
<c:if test="#{not empty trendLabel}">
<span class="text-500 text-xs">#{trendLabel}</span>
</c:if>
</div>
</c:if>
</div>
</c:otherwise>
</c:choose>
</div>
</ui:composition>
<!-- Déléguer à kpi-card.xhtml pour éviter la duplication de code (DRY) -->
<ui:include src="/templates/components/shared/cards/kpi-card.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="#{trend}" />
<ui:param name="growthLabel" value="#{trendLabel}" />
<ui:param name="growthType" value="percentage" />
<ui:param name="colSize" value="#{colSize}" />
<ui:param name="clickable" value="#{clickable}" />
<ui:param name="clickAction" value="#{clickAction}" />
<ui:param name="clickOutcome" value="#{clickOutcome}" />
<ui:param name="statusIcon" value="#{statusIcon}" />
<ui:param name="statusLabel" value="#{statusLabel}" />
<ui:param name="statusValue" value="#{statusValue}" />
<ui:param name="styleClass" value="#{styleClass}" />
</ui:include>
</ui:composition>

View File

@@ -1,9 +1,6 @@
<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:fr="http://primefaces.org/freya">
<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:fr="http://primefaces.org/freya">
<!--
Composant réutilisable: Menu Navigation (WOU/DRY Pattern)
@@ -13,46 +10,50 @@
Description: Menu de navigation latéral pour Lions User Manager
-->
<div class="menu-wrapper">
<div class="sidebar-logo">
<a href="/pages/user-manager/dashboard">
<p:graphicImage name="images/logo-freya-single.svg" library="freya-layout" />
</a>
<a href="#" class="sidebar-pin" title="Toggle Menu">
<span class="pin"></span>
</a>
</div>
<div class="layout-menu-container">
<h:form id="menuform">
<fr:menu widgetVar="FreyaMenuWidget">
<!-- Dashboard -->
<p:menuitem id="m_dashboard" value="Tableau de Bord" icon="pi pi-home" outcome="/pages/user-manager/dashboard" />
<!-- Gestion Utilisateurs -->
<p:submenu id="m_users" label="Gestion Utilisateurs" icon="pi pi-users">
<p:menuitem id="m_users_list" value="Liste des Utilisateurs" icon="pi pi-list" outcome="/pages/user-manager/users/list" />
<p:menuitem id="m_users_create" value="Nouvel Utilisateur" icon="pi pi-user-plus" outcome="/pages/user-manager/users/create" />
</p:submenu>
<!-- Gestion Rôles -->
<p:submenu id="m_roles" label="Gestion Rôles" icon="pi pi-shield">
<p:menuitem id="m_roles_list" value="Liste des Rôles" icon="pi pi-list" outcome="/pages/user-manager/roles/list" />
<p:menuitem id="m_roles_assign" value="Attribution Rôles" icon="pi pi-key" outcome="/pages/user-manager/roles/assign" />
</p:submenu>
<!-- Audit -->
<p:submenu id="m_audit" label="Audit" icon="pi pi-history">
<p:menuitem id="m_audit_logs" value="Journal d'Audit" icon="pi pi-file-o" outcome="/pages/user-manager/audit/logs" />
</p:submenu>
<!-- Synchronisation -->
<p:submenu id="m_sync" label="Synchronisation" icon="pi pi-sync">
<p:menuitem id="m_sync_dashboard" value="Dashboard" icon="pi pi-dashboard" outcome="/pages/user-manager/sync/dashboard" />
</p:submenu>
</fr:menu>
</h:form>
</div>
</div>
<div class="menu-wrapper">
<div class="sidebar-logo">
<a href="/pages/user-manager/dashboard">
<p:graphicImage name="images/logo-freya-single.svg" library="freya-layout" />
</a>
<a href="#" class="sidebar-pin" title="Toggle Menu">
<span class="pin"></span>
</a>
</div>
<div class="layout-menu-container">
<h:form id="menuform">
<fr:menu widgetVar="FreyaMenuWidget">
<!-- Dashboard -->
<p:menuitem id="m_dashboard" value="Tableau de Bord" icon="pi pi-home"
outcome="/pages/user-manager/dashboard" />
</ui:composition>
<!-- Gestion Utilisateurs -->
<p:submenu id="m_users" label="Gestion Utilisateurs" icon="pi pi-users">
<p:menuitem id="m_users_list" value="Liste des Utilisateurs" icon="pi pi-list"
outcome="/pages/user-manager/users/list" />
<p:menuitem id="m_users_create" value="Nouvel Utilisateur" icon="pi pi-user-plus"
outcome="/pages/user-manager/users/create" />
</p:submenu>
<!-- Gestion Rôles -->
<p:submenu id="m_roles" label="Gestion Rôles" icon="pi pi-shield">
<p:menuitem id="m_roles_list" value="Liste des Rôles" icon="pi pi-list"
outcome="/pages/user-manager/roles/list" />
</p:submenu>
<!-- Audit -->
<p:submenu id="m_audit" label="Audit" icon="pi pi-history">
<p:menuitem id="m_audit_logs" value="Journal d'Audit" icon="pi pi-file-o"
outcome="/pages/user-manager/audit/logs" />
</p:submenu>
<!-- Synchronisation -->
<p:submenu id="m_sync" label="Synchronisation" icon="pi pi-sync">
<p:menuitem id="m_sync_dashboard" value="Dashboard" icon="pi pi-dashboard"
outcome="/pages/user-manager/sync/dashboard" />
</p:submenu>
</fr:menu>
</h:form>
</div>
</div>
</ui:composition>

View File

@@ -1,52 +1,93 @@
<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">
<!--
Composant en-tête de page réutilisable (WOU/DRY Pattern)
<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">
<!--
Composant réutilisable: En-tête de Page - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: En-tête de page avec icône, titre, description et actions
Version: 2.0.0
Description: En-tête de page avec icône, titre, description, fil d'Ariane et actions.
Conforme au pattern WOU/DRY pour la cohérence visuelle de l'application.
Paramètres:
- icon: String (optionnel) - Classe d'icône PrimeIcons (ex: "pi pi-users text-blue-500")
- title: String (requis) - Titre de la page
- description: String (optionnel) - Description de la page
- breadcrumbParent: String (optionnel) - Libellé de la page parente (ex: "Utilisateurs")
- breadcrumbParentLink: String (optionnel) - Lien de la page parente
- styleClass: String (optionnel) - Classes CSS supplémentaires
Usage:
Slot facelet:
- "actions": Contenu personnalisé pour les boutons d'action à droite
Exemples d'utilisation:
1. En-tête simple:
<ui:include src="/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" />
</ui:include>
2. En-tête avec fil d'ariane et actions:
<ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="pi pi-user-edit text-green-500" />
<ui:param name="title" value="Créer un Utilisateur" />
<ui:param name="description" value="Formulaire de création d'un nouvel utilisateur" />
<ui:param name="breadcrumbParent" value="Utilisateurs" />
<ui:param name="breadcrumbParentLink" value="/pages/user-manager/users/list" />
<ui:define name="actions">
Boutons d'action ici
<p:commandButton value="Exporter" icon="pi pi-download" styleClass="p-button-secondary" />
</ui:define>
</ui:include>
-->
<div class="grid mb-4">
<div class="col-12">
<div class="card">
<div class="flex align-items-center justify-content-between">
<div>
<h3 class="mb-2">
<c:if test="#{not empty icon}">
<i class="#{icon} mr-2"></i>
<!-- Fil d'Ariane (Breadcrumb) -->
<c:if test="#{not empty breadcrumbParent}">
<div class="flex align-items-center gap-2 mb-2 text-sm">
<c:choose>
<c:when test="#{not empty breadcrumbParentLink}">
<h:link outcome="#{breadcrumbParentLink}"
styleClass="text-primary no-underline hover:underline">
#{breadcrumbParent}
</h:link>
</c:when>
<c:otherwise>
<span class="text-600">#{breadcrumbParent}</span>
</c:otherwise>
</c:choose>
<i class="pi pi-angle-right text-400 text-xs"></i>
<span class="text-900 font-medium">#{title}</span>
</div>
</c:if>
<div class="card #{styleClass}">
<div class="flex align-items-center justify-content-between flex-wrap gap-3">
<!-- Partie gauche: Icône + Titre + Description -->
<div class="flex align-items-center gap-3">
<c:if test="#{not empty icon}">
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 3rem; height: 3rem;">
<i class="#{icon} text-xl"></i>
</div>
</c:if>
<div>
<h3 class="m-0 text-900 font-bold">#{title}</h3>
<c:if test="#{not empty description}">
<p class="text-600 text-sm m-0 mt-1">#{description}</p>
</c:if>
#{title}
</h3>
<p class="text-600 m-0" rendered="#{not empty description}">#{description}</p>
</div>
</div>
<div>
<!-- Partie droite: Actions -->
<div class="flex align-items-center gap-2">
<ui:insert name="actions" />
</div>
</div>
</div>
</div>
</div>
</ui:composition>
</ui:composition>

View File

@@ -22,46 +22,93 @@
<p:graphicImage name="images/#{guestPreferences.lightLogo ? 'logo-freya-white.svg' : 'logo-freya.svg'}" library="freya-layout" />
</h:link>
</div>
<ui:include src="/templates/components/layout/menu.xhtml" />
<div class="layout-topbar-right">
<ul class="layout-topbar-actions">
<li class="topbar-item user-profile">
<a href="#" title="Profil utilisateur">
<div class="flex align-items-center">
<div class="bg-primary text-white border-round flex align-items-center justify-content-center mr-2"
style="width: 32px; height: 32px; font-size: 12px; font-weight: bold;">
<i class="pi pi-user"></i>
</div>
<div class="text-sm">
<div class="text-900 font-medium">Utilisateur</div>
<div class="text-600 text-xs">Connecté</div>
</div>
<a href="#" class="user-profile-link">
<div class="bg-primary text-white border-circle flex align-items-center justify-content-center user-avatar"
style="width: 36px; height: 36px; font-size: 14px; font-weight: bold; margin-right: 0.75rem;">
#{userSessionBean.initials}
</div>
<span class="user-info">
<span class="user-name">
#{userSessionBean.fullName}
<span class="user-status online"></span>
<span class="user-separator">|</span>
<span class="user-role">#{userSessionBean.primaryRole}</span>
</span>
<span class="user-email">#{userSessionBean.email}</span>
</span>
</a>
<ul>
<li>
<a href="/pages/user-manager/users/profile">
<i class="pi pi-user mr-2"></i>
<span>Mon Profil</span>
</a>
<ul class="user-dropdown-menu">
<!-- En-tête du menu avec infos utilisateur -->
<li class="user-dropdown-header">
<div class="user-dropdown-avatar">
<div class="bg-primary text-white border-circle flex align-items-center justify-content-center"
style="width: 48px; height: 48px; font-size: 20px; font-weight: bold;">
#{userSessionBean.initials}
</div>
<span class="user-status-indicator online"></span>
</div>
<div class="user-dropdown-info">
<div class="user-dropdown-name text-900 font-semibold">#{userSessionBean.fullName}</div>
<div class="user-dropdown-email text-600 text-sm">#{userSessionBean.email}</div>
<div class="user-dropdown-role text-500 text-xs">#{userSessionBean.primaryRole}</div>
</div>
</li>
<li>
<a href="#">
<i class="pi pi-cog mr-2"></i>
<span>Paramètres</span>
</a>
<!-- Séparateur -->
<li class="user-dropdown-divider"></li>
<!-- Actions principales -->
<li class="user-dropdown-section">
<div class="section-title">Mon Compte</div>
<div class="section-items">
<h:link outcome="/pages/user-manager/users/profile" styleClass="dropdown-item">
<i class="pi pi-user"></i>
<span>Mon Profil</span>
<i class="pi pi-angle-right item-arrow"></i>
</h:link>
<h:link outcome="/pages/user-manager/settings" styleClass="dropdown-item">
<i class="pi pi-cog"></i>
<span>Paramètres</span>
<i class="pi pi-angle-right item-arrow"></i>
</h:link>
<a href="#" class="dropdown-item">
<i class="pi pi-shield"></i>
<span>Sécurité</span>
<i class="pi pi-angle-right item-arrow"></i>
</a>
</div>
</li>
<li class="border-top-1 surface-border">
<a href="#" class="text-red-600">
<i class="pi pi-sign-out mr-2"></i>
<span>Déconnexion</span>
</a>
<!-- Séparateur -->
<li class="user-dropdown-divider"></li>
<!-- Actions système -->
<li class="user-dropdown-section">
<div class="section-items">
<a href="#" class="dropdown-item">
<i class="pi pi-question-circle"></i>
<span>Aide &amp; Support</span>
</a>
<h:form>
<p:commandLink action="#{userSessionBean.logout}" styleClass="dropdown-item logout-item">
<i class="pi pi-sign-out"></i>
<span>Déconnexion</span>
</p:commandLink>
</h:form>
</div>
</li>
</ul>
</li>
</ul>
<a href="#" class="layout-rightpanel-button">
<i class="pi pi-arrow-left"/>
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,170 @@
<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">
<!--
Composant interne: Contenu Attribution de Rôles - Écosystème LionsDev
Auteur: Lions User Manager
Version: 2.0.0
Description: Contenu de l'attribution de rôles, séparé du wrapper form.
NE PAS inclure directement - utiliser role-assignment.xhtml.
Paramètres hérités de role-assignment.xhtml
-->
<p:messages id="roleAssignmentMessages" showDetail="true" closable="true" styleClass="mb-3" />
<p:panel styleClass="w-full">
<f:facet name="header">
<div class="flex align-items-center gap-2">
<i class="pi pi-shield text-primary"></i>
<span class="font-semibold">Attribution de rôles</span>
<c:if test="#{not empty user.username}">
<span class="text-color-secondary">- #{user.username}</span>
</c:if>
</div>
</f:facet>
<!-- Section: Rôles actuels de l'utilisateur -->
<h4 class="text-primary mt-0 mb-3">
<i class="pi pi-user-edit mr-2"></i>Rôles actuels
</h4>
<div class="flex flex-wrap gap-2 mb-4">
<c:choose>
<c:when test="#{not empty userRoles and not userRoles.isEmpty()}">
<c:forEach var="userRole" items="#{userRoles}">
<div class="flex align-items-center surface-100 border-round-lg px-3 py-2 gap-2">
<i class="pi pi-shield text-primary text-sm"></i>
<span class="font-medium text-sm">#{userRole.name}</span>
<c:if test="#{not empty userRole.typeRole}">
<p:tag value="#{userRole.typeRole == 'REALM_ROLE' ? 'Realm' : 'Client'}"
severity="#{userRole.typeRole == 'REALM_ROLE' ? 'success' : 'info'}"
styleClass="text-xs" />
</c:if>
<p:commandButton icon="pi pi-times"
styleClass="p-button-text p-button-sm p-button-danger p-button-rounded"
action="#{roleGestionBean.revokeRoleFromParams}" update="#{update}"
title="Révoquer ce rôle" style="width: 1.5rem; height: 1.5rem;">
<f:param name="userId" value="#{user.id}" />
<f:param name="roleName" value="#{userRole.name}" />
<p:confirm header="Révoquer le rôle"
message="Voulez-vous révoquer le rôle '#{userRole.name}' de #{user.username} ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</div>
</c:forEach>
</c:when>
<c:otherwise>
<div class="text-color-secondary font-italic p-3 surface-100 border-round w-full text-center">
<i class="pi pi-info-circle mr-2"></i>
Aucun rôle attribué à cet utilisateur
</div>
</c:otherwise>
</c:choose>
</div>
<p:separator />
<!-- Section: Rôles disponibles -->
<h4 class="text-primary mt-3 mb-3">
<i class="pi pi-list mr-2"></i>Rôles disponibles
</h4>
<!-- Recherche de rôles -->
<div class="mb-3">
<div class="p-inputgroup" style="max-width: 400px;">
<span class="p-inputgroup-addon">
<i class="pi pi-search"></i>
</span>
<p:inputText id="roleSearchInput" value="#{roleGestionBean.roleSearchText}"
placeholder="Filtrer les rôles..." styleClass="w-full">
<p:ajax event="keyup" delay="300" update="realmRolesPanel clientRolesPanel" />
</p:inputText>
</div>
</div>
<!-- Rôles Realm -->
<c:if test="#{showRealmRoles}">
<p:panel id="realmRolesPanel" header="Rôles Realm" styleClass="mb-3" toggleable="true">
<div class="flex flex-wrap gap-2">
<c:forEach var="role" items="#{availableRoles}">
<c:if test="#{role.typeRole == 'REALM_ROLE'}">
<!-- Vérifier si déjà assigné -->
<c:set var="isAssigned" value="false" />
<c:forEach var="ur" items="#{userRoles}">
<c:if test="#{ur.id == role.id or ur.name == role.name}">
<c:set var="isAssigned" value="true" />
</c:if>
</c:forEach>
<div class="flex align-items-center border-1 border-round-lg px-3 py-2 gap-2
#{isAssigned ? 'surface-200 border-green-300' : 'surface-50 border-300 hover:surface-100'}
transition-all transition-duration-200">
<c:choose>
<c:when test="#{isAssigned}">
<i class="pi pi-check-circle text-green-500"></i>
<span class="font-medium text-sm text-green-700">#{role.name}</span>
</c:when>
<c:otherwise>
<i class="pi pi-shield text-400"></i>
<span class="font-medium text-sm text-700">#{role.name}</span>
<p:commandButton icon="pi pi-plus"
styleClass="p-button-text p-button-sm p-button-success p-button-rounded"
action="#{roleGestionBean.assignRoleFromParams}" update="#{update}"
title="Attribuer ce rôle" style="width: 1.5rem; height: 1.5rem;">
<f:param name="userId" value="#{user.id}" />
<f:param name="roleName" value="#{role.name}" />
</p:commandButton>
</c:otherwise>
</c:choose>
</div>
</c:if>
</c:forEach>
</div>
</p:panel>
</c:if>
<!-- Rôles Client -->
<c:if test="#{showClientRoles}">
<p:panel id="clientRolesPanel" header="Rôles Client" styleClass="mb-3" toggleable="true">
<div class="flex flex-wrap gap-2">
<c:forEach var="role" items="#{availableRoles}">
<c:if test="#{role.typeRole == 'CLIENT_ROLE'}">
<c:set var="isAssigned" value="false" />
<c:forEach var="ur" items="#{userRoles}">
<c:if test="#{ur.id == role.id or ur.name == role.name}">
<c:set var="isAssigned" value="true" />
</c:if>
</c:forEach>
<div class="flex align-items-center border-1 border-round-lg px-3 py-2 gap-2
#{isAssigned ? 'surface-200 border-green-300' : 'surface-50 border-300 hover:surface-100'}
transition-all transition-duration-200">
<c:choose>
<c:when test="#{isAssigned}">
<i class="pi pi-check-circle text-green-500"></i>
<span class="font-medium text-sm text-green-700">#{role.name}</span>
</c:when>
<c:otherwise>
<i class="pi pi-shield text-400"></i>
<span class="font-medium text-sm text-700">#{role.name}</span>
<p:commandButton icon="pi pi-plus"
styleClass="p-button-text p-button-sm p-button-success p-button-rounded"
action="#{roleGestionBean.assignRoleFromParams}" update="#{update}"
title="Attribuer ce rôle" style="width: 1.5rem; height: 1.5rem;">
<f:param name="userId" value="#{user.id}" />
<f:param name="roleName" value="#{role.name}" />
</p:commandButton>
</c:otherwise>
</c:choose>
</div>
</c:if>
</c:forEach>
</div>
</p:panel>
</c:if>
</p:panel>
</ui:composition>

View File

@@ -1,183 +1,76 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Attribution de Rôles (WOU/DRY Pattern)
Composant réutilisable: Attribution de Rôles - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Interface pour attribuer/révoquer des rôles à un utilisateur
Version: 2.0.0
Description: Interface complète pour attribuer/révoquer des rôles à un utilisateur.
Affiche les rôles actuels avec possibilité de révocation, les rôles
disponibles classés par type (Realm/Client), et une recherche intégrée.
Paramètres:
- user: UserDTO (requis) - L'utilisateur concerné
- availableRoles: List&lt;RoleDTO&gt; (requis) - Liste des rôles disponibles
- userRoles: List&lt;RoleDTO&gt; (requis) - Liste des rôles de l'utilisateur
- roleBean: String (optionnel) - Nom du bean pour les actions (défaut: "roleGestionBean")
- availableRoles: List<RoleDTO> (requis) - Liste des rôles disponibles
- userRoles: List<RoleDTO> (requis) - Liste des rôles actuels de l'utilisateur
- update: String (défaut: "@form") - Composants à mettre à jour
- showRealmRoles: Boolean (défaut: true) - Afficher les rôles Realm
- showClientRoles: Boolean (défaut: true) - Afficher les rôles Client
- formId: String (défaut: "roleAssignmentForm") - ID du formulaire
- useParentForm: Boolean (défaut: false) - Utiliser le formulaire parent
Exemples d'utilisation:
1. Attribution simple:
1. Attribution dans un formulaire standalone:
<ui:include src="/templates/components/role-management/role-assignment.xhtml">
<ui:param name="user" value="#{userBean.selectedUser}" />
<ui:param name="availableRoles" value="#{roleBean.availableRoles}" />
<ui:param name="userRoles" value="#{userBean.userRoles}" />
<ui:param name="assignAction" value="#{roleBean.assignRole}" />
<ui:param name="revokeAction" value="#{roleBean.revokeRole}" />
</ui:include>
2. Attribution dans un formulaire existant:
<ui:include src="/templates/components/role-management/role-assignment.xhtml">
<ui:param name="user" value="#{userBean.selectedUser}" />
<ui:param name="availableRoles" value="#{roleBean.availableRoles}" />
<ui:param name="userRoles" value="#{userBean.userRoles}" />
<ui:param name="useParentForm" value="true" />
</ui:include>
-->
<c:set var="formId" value="#{empty formId ? 'roleAssignmentForm' : formId}" />
<c:set var="update" value="#{empty update ? '@form' : update}" />
<c:set var="showRealmRoles" value="#{empty showRealmRoles ? true : showRealmRoles}" />
<c:set var="showClientRoles" value="#{empty showClientRoles ? true : showClientRoles}" />
<c:set var="roleBeanName" value="#{empty roleBean ? 'roleGestionBean' : roleBean}" />
<h:form id="#{formId}">
<p:panel header="Attribution de rôles - #{user.username}" styleClass="w-full">
<!-- Rôles actuels de l'utilisateur -->
<h3>Rôles actuels</h3>
<div class="flex flex-wrap gap-2 mb-4">
<c:forEach var="userRole" items="#{userRoles}">
<p:tag
value="#{userRole.name}"
severity="info"
icon="pi pi-shield">
<p:commandButton
icon="pi pi-times"
styleClass="p-button-text p-button-sm p-button-danger ml-2"
action="#{roleGestionBean.revokeRoleFromParams}"
update="#{update}"
title="Révoquer le rôle">
<f:param name="userId" value="#{user.id}" />
<f:param name="roleName" value="#{userRole.name}" />
</p:commandButton>
</p:tag>
</c:forEach>
<c:if test="#{empty userRoles}">
<span class="text-color-secondary">Aucun rôle attribué</span>
</c:if>
</div>
<p:separator />
<!-- Rôles disponibles -->
<h3>Rôles disponibles</h3>
<!-- Rôles Realm -->
<c:if test="#{showRealmRoles}">
<p:panel header="Rôles Realm" styleClass="mb-3">
<div class="flex flex-wrap gap-2">
<c:forEach var="role" items="#{availableRoles}">
<c:if test="#{role.typeRole == 'REALM_ROLE'}">
<c:set var="isAssigned" value="false" />
<c:forEach var="userRole" items="#{userRoles}">
<c:if test="#{userRole.id == role.id}">
<c:set var="isAssigned" value="true" />
</c:if>
</c:forEach>
<c:choose>
<c:when test="#{isAssigned}">
<p:tag
value="#{role.name}"
severity="success"
icon="pi pi-check" />
</c:when>
<c:otherwise>
<p:tag
value="#{role.name}"
severity="info"
icon="pi pi-shield">
<p:commandButton
icon="pi pi-plus"
styleClass="p-button-text p-button-sm p-button-success ml-2"
action="#{roleGestionBean.assignRoleFromParams}"
update="#{update}"
title="Attribuer le rôle">
<f:param name="userId" value="#{user.id}" />
<f:param name="roleName" value="#{role.name}" />
</p:commandButton>
</p:tag>
</c:otherwise>
</c:choose>
</c:if>
</c:forEach>
</div>
</p:panel>
</c:if>
<!-- Rôles Client -->
<c:if test="#{showClientRoles}">
<p:panel header="Rôles Client" styleClass="mb-3">
<div class="flex flex-wrap gap-2">
<c:forEach var="role" items="#{availableRoles}">
<c:if test="#{role.typeRole == 'CLIENT_ROLE'}">
<c:set var="isAssigned" value="false" />
<c:forEach var="userRole" items="#{userRoles}">
<c:if test="#{userRole.id == role.id}">
<c:set var="isAssigned" value="true" />
</c:if>
</c:forEach>
<c:choose>
<c:when test="#{isAssigned}">
<p:tag
value="#{role.name}"
severity="success"
icon="pi pi-check" />
</c:when>
<c:otherwise>
<p:tag
value="#{role.name}"
severity="info"
icon="pi pi-shield">
<p:commandButton
icon="pi pi-plus"
styleClass="p-button-text p-button-sm p-button-success ml-2"
action="#{roleGestionBean.assignRoleFromParams}"
update="#{update}"
title="Attribuer le rôle">
<f:param name="userId" value="#{user.id}" />
<f:param name="roleName" value="#{role.name}" />
</p:commandButton>
</p:tag>
</c:otherwise>
</c:choose>
</c:if>
</c:forEach>
</div>
</p:panel>
</c:if>
<!-- Recherche de rôles -->
<p:separator />
<h3>Rechercher un rôle</h3>
<div class="flex gap-2 mb-3">
<p:inputText
value="#{roleGestionBean.roleSearchText}"
placeholder="Rechercher un rôle..."
styleClass="flex-1">
<p:ajax event="keyup"
delay="300"
update="roleSearchResults" />
</p:inputText>
</div>
<div id="roleSearchResults" class="flex flex-wrap gap-2">
<!-- Résultats de recherche -->
</div>
</p:panel>
</h:form>
</ui:composition>
<c:set var="useParentForm" value="#{empty useParentForm ? false : useParentForm}" />
<!-- Contenu du composant (utilisable avec ou sans form wrapper) -->
<c:choose>
<c:when test="#{useParentForm}">
<ui:fragment>
<ui:include src="/templates/components/role-management/role-assignment-content.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="availableRoles" value="#{availableRoles}" />
<ui:param name="userRoles" value="#{userRoles}" />
<ui:param name="update" value="#{update}" />
<ui:param name="showRealmRoles" value="#{showRealmRoles}" />
<ui:param name="showClientRoles" value="#{showClientRoles}" />
</ui:include>
</ui:fragment>
</c:when>
<c:otherwise>
<h:form id="#{formId}">
<ui:include src="/templates/components/role-management/role-assignment-content.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="availableRoles" value="#{availableRoles}" />
<ui:param name="userRoles" value="#{userRoles}" />
<ui:param name="update" value="#{update}" />
<ui:param name="showRealmRoles" value="#{showRealmRoles}" />
<ui:param name="showClientRoles" value="#{showClientRoles}" />
</ui:include>
</h:form>
</c:otherwise>
</c:choose>
</ui:composition>

View File

@@ -1,25 +1,26 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions">
<!--
Composant réutilisable: Carte Rôle (WOU/DRY Pattern)
Composant réutilisable: Carte Rôle - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Affiche une carte rôle avec informations principales et actions
Version: 2.0.0
Description: Affiche une carte rôle avec icône, type, description et actions.
Prend en charge les différents types de rôles (Realm, Client, Composite).
Paramètres:
- role: RoleDTO (requis) - Le rôle à afficher
- showActions: Boolean (défaut: true) - Afficher les boutons d'action
- showDescription: Boolean (défaut: true) - Afficher la description
- showCompositeInfo: Boolean (défaut: true) - Afficher les infos de rôle composite
- clickable: Boolean (défaut: true) - Rendre la carte cliquable
- clickable: Boolean (défaut: true) - Rendre le titre cliquable
- outcome: String (optionnel) - Page de destination au clic
- styleClass: String (optionnel) - Classes CSS supplémentaires
- editOutcome: String (optionnel) - Page d'édition
- deleteAction: MethodExpression (optionnel) - Action de suppression
Exemples d'utilisation:
@@ -28,124 +29,146 @@
<ui:param name="role" value="#{roleBean.selectedRole}" />
</ui:include>
2. Carte avec actions:
<ui:include src="/templates/components/role-management/role-card.xhtml">
<ui:param name="role" value="#{roleBean.selectedRole}" />
<ui:param name="showActions" value="true" />
<ui:param name="outcome" value="/pages/user-manager/roles/details" />
</ui:include>
2. Carte pour liste:
<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="#{r}" />
<ui:param name="deleteAction" value="#{roleBean.deleteRole(r.id)}" />
</ui:include>
</div>
-->
<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}" />
<p:card styleClass="role-card #{styleClass}" rendered="#{not empty role}">
<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 -->
<c:set var="icon" value="pi-shield" />
<c:set var="severity" value="info" />
<c:set var="typeLabel" value="Rôle" />
<c:choose>
<c:when test="#{role.typeRole == 'REALM_ROLE'}">
<c:set var="severity" value="success" />
<c:set var="typeLabel" value="Realm" />
<c:set var="iconColor" value="text-green-500" />
<c:set var="bgColor" value="bg-green-100" />
</c:when>
<c:when test="#{role.typeRole == 'CLIENT_ROLE'}">
<c:set var="severity" value="info" />
<c:set var="typeLabel" value="Client" />
<c:set var="iconColor" value="text-blue-500" />
<c:set var="bgColor" value="bg-blue-100" />
</c:when>
<c:when test="#{role.typeRole == 'COMPOSITE_ROLE'}">
<c:set var="severity" value="warning" />
<c:set var="typeLabel" value="Composite" />
<c:set var="iconColor" value="text-orange-500" />
<c:set var="bgColor" value="bg-orange-100" />
<c:set var="icon" value="pi-sitemap" />
</c:when>
<c:otherwise>
<c:set var="severity" value="secondary" />
<c:set var="iconColor" value="text-gray-500" />
<c:set var="bgColor" value="surface-100" />
</c:otherwise>
</c:choose>
<p:card styleClass="role-card shadow-1 hover:shadow-3 transition-all transition-duration-200 #{styleClass}"
rendered="#{not empty role}">
<f:facet name="header">
<div class="flex align-items-center justify-content-between">
<div class="flex align-items-center gap-2">
<i class="pi pi-shield text-2xl text-primary"></i>
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
<div class="flex align-items-center gap-3">
<div class="flex align-items-center justify-content-center border-circle #{bgColor}"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi #{icon} #{iconColor} text-lg"></i>
</div>
<div class="flex flex-column">
<h3 class="m-0">#{role.name}</h3>
<span class="text-color-secondary text-sm">
<c:choose>
<c:when test="#{role.typeRole == 'REALM_ROLE'}">Rôle Realm</c:when>
<c:when test="#{role.typeRole == 'CLIENT_ROLE'}">Rôle Client</c:when>
<c:when test="#{role.typeRole == 'COMPOSITE_ROLE'}">Rôle Composite</c:when>
<c:otherwise>Rôle</c:otherwise>
</c:choose>
</span>
<c:choose>
<c:when test="#{clickable}">
<h:link outcome="#{not empty outcome ? outcome : '/pages/user-manager/roles/details'}"
styleClass="text-900 font-semibold text-lg no-underline hover:text-primary transition-colors transition-duration-200">
<f:param name="roleId" value="#{role.id}" />
#{role.name}
</h:link>
</c:when>
<c:otherwise>
<span class="text-900 font-semibold text-lg">#{role.name}</span>
</c:otherwise>
</c:choose>
<span class="text-color-secondary text-xs">#{typeLabel}</span>
</div>
</div>
<p:tag
value="#{role.typeRole != null ? role.typeRole : 'ROLE'}"
severity="#{role.typeRole == 'REALM_ROLE' ? 'success' : role.typeRole == 'CLIENT_ROLE' ? 'info' : 'warning'}" />
<c:if test="#{role.composite}">
<i class="pi pi-sitemap text-orange-500" title="Rôle Composite"></i>
</c:if>
</div>
</f:facet>
<div class="role-card-content">
<div class="role-card-content pt-3">
<!-- Description -->
<c:if test="#{showDescription and not empty role.description}">
<p class="text-color-secondary mb-3">#{role.description}</p>
<c:if test="#{showDescription}">
<div class="mb-3" style="min-height: 3rem;">
<c:choose>
<c:when test="#{not empty role.description}">
<p class="text-color-secondary text-sm m-0 line-height-3 text-overflow-ellipsis overflow-hidden"
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
#{role.description}
</p>
</c:when>
<c:otherwise>
<span class="text-color-secondary text-sm font-italic opacity-60">Aucune description</span>
</c:otherwise>
</c:choose>
</div>
</c:if>
<!-- Informations -->
<div class="flex flex-column gap-2 mb-3">
<!-- Informations contextuelles -->
<div class="flex flex-column gap-2">
<c:if test="#{not empty role.realmName}">
<div class="flex align-items-center gap-2">
<i class="pi pi-globe text-color-secondary"></i>
<span>Realm: <strong>#{role.realmName}</strong></span>
<i class="pi pi-globe text-color-secondary text-xs"></i>
<span class="text-sm">#{role.realmName}</span>
</div>
</c:if>
<c:if test="#{not empty role.clientId}">
<div class="flex align-items-center gap-2">
<i class="pi pi-desktop text-color-secondary"></i>
<span>Client: <strong>#{role.clientId}</strong></span>
</div>
</c:if>
<!-- Rôle composite -->
<c:if test="#{showCompositeInfo and role.composite}">
<div class="flex align-items-center gap-2">
<i class="pi pi-sitemap text-color-secondary"></i>
<span class="text-warning">Rôle composite</span>
<i class="pi pi-desktop text-color-secondary text-xs"></i>
<span class="text-sm">Client: #{role.clientId}</span>
</div>
</c:if>
</div>
</div>
<!-- Actions -->
<f:facet name="footer">
<c:if test="#{showActions}">
<div class="flex gap-2 justify-content-end">
<p:commandButton
icon="pi pi-eye"
title="Voir les détails"
styleClass="p-button-text p-button-sm"
outcome="#{not empty outcome ? outcome : '/pages/user-manager/roles/details'}"
rendered="#{clickable}">
<f:param name="roleId" value="#{role.id}" />
</p:commandButton>
<p:commandButton
icon="pi pi-pencil"
title="Modifier"
styleClass="p-button-text p-button-sm p-button-warning"
outcome="/pages/user-manager/roles/edit">
<f:param name="roleId" value="#{role.id}" />
</p:commandButton>
<p:commandButton
icon="pi pi-trash"
title="Supprimer"
styleClass="p-button-text p-button-sm p-button-danger"
onclick="PF('confirmDeleteRoleDialog').show()" />
<div class="flex gap-2 justify-content-end pt-2 border-top-1 surface-border">
<c:if test="#{showEdit}">
<p:commandButton icon="pi pi-pencil" title="Modifier"
styleClass="p-button-text p-button-sm p-button-warning p-button-rounded"
outcome="#{editOutcome}">
<f:param name="roleId" value="#{role.id}" />
</p:commandButton>
</c:if>
<c:if test="#{not empty 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="#{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>
</c:if>
</div>
</c:if>
</f:facet>
</p:card>
<!-- Dialog de confirmation de suppression -->
<p:confirmDialog
id="confirmDeleteRoleDialog"
widgetVar="confirmDeleteRoleDialog"
message="Êtes-vous sûr de vouloir supprimer le rôle #{role.name} ?"
header="Confirmation de suppression"
severity="warn">
<p:commandButton
value="Oui"
icon="pi pi-check"
styleClass="p-button-danger"
action="#{roleBean.deleteRole(role.id)}"
update="@form"
oncomplete="PF('confirmDeleteRoleDialog').hide()" />
<p:commandButton
value="Non"
icon="pi pi-times"
styleClass="p-button-secondary"
onclick="PF('confirmDeleteRoleDialog').hide()" />
</p:confirmDialog>
</ui:composition>
</ui:composition>

View File

@@ -1,131 +1,176 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<p:panel header="#{mode == 'create' ? 'Nouveau Rôle' : 'Modifier Rôle'}"
styleClass="w-full">
<!--
Composant réutilisable: Contenu du formulaire Rôle - Écosystème LionsDev
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-6">
Auteur: Lions User Manager
Version: 2.0.0
Description: Corps du formulaire rôle (création/édition) avec validation
et messages d'erreur. Doit être contenu dans un h:form ou utilisé
via role-form.xhtml qui gère automatiquement le wrapper.
Paramètres hérités de role-form.xhtml:
- role: RoleDTO (requis)
- mode: String ("create" | "edit")
- showRealmSelector: Boolean
- showClientSelector: Boolean
- showCompositeOptions: Boolean
- readonly: Boolean
- hasSubmitAction: Boolean
- submitAction: MethodExpression
- submitOutcome: String
- update: String
- cancelOutcome: String
-->
<p:messages id="roleFormMessages" showDetail="true" closable="true" styleClass="mb-3" />
<p:panel header="#{mode == 'create' ? 'Nouveau Rôle' : 'Modifier Rôle'}" styleClass="w-full">
<!-- Section: Informations du rôle -->
<h4 class="text-primary mt-0 mb-3">
<i class="pi pi-shield mr-2"></i>Informations du rôle
</h4>
<div class="grid">
<!-- Nom du rôle -->
<p:outputLabel for="roleName" value="Nom du rôle *" />
<p:inputText id="roleName"
value="#{role.name}"
required="true"
readonly="#{readonly}"
placeholder="ADMIN, USER, MODERATOR..."
styleClass="w-full">
<f:validateLength minimum="2" maximum="100" />
<f:validateRegex pattern="^[A-Z_]+$" />
</p:inputText>
<!-- Description -->
<p:outputLabel for="description" value="Description" />
<p:inputTextarea id="description"
value="#{role.description}"
readonly="#{readonly}"
placeholder="Description du rôle..."
rows="3"
styleClass="w-full" />
<div class="col-12 md:col-6">
<div class="field">
<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_manager, sync_manager..." styleClass="w-full">
<f:validateLength minimum="2" maximum="100" />
<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, chiffres, underscores et tirets (ex: admin, user_manager)
</small>
</div>
</div>
<!-- Type de rôle -->
<p:outputLabel for="typeRole" value="Type de rôle *" />
<p:selectOneMenu id="typeRole"
value="#{role.typeRole}"
required="true"
readonly="#{readonly or mode == 'edit'}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<f:selectItem itemLabel="Rôle Realm" itemValue="REALM_ROLE" />
<f:selectItem itemLabel="Rôle Client" itemValue="CLIENT_ROLE" />
<f:selectItem itemLabel="Rôle Composite" itemValue="COMPOSITE_ROLE" />
</p:selectOneMenu>
<!-- Realm (si affiché) -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="typeRole" value="Type de rôle" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<p:selectOneMenu id="typeRole" value="#{role.typeRole}" required="true"
readonly="#{readonly or mode == 'edit'}" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
<f:selectItem itemLabel="Rôle Realm" itemValue="REALM_ROLE" />
<f:selectItem itemLabel="Rôle Client" itemValue="CLIENT_ROLE" />
<f:selectItem itemLabel="Rôle Composite" itemValue="COMPOSITE_ROLE" />
</p:selectOneMenu>
<p:message for="typeRole" display="text" styleClass="mt-1" />
</div>
</div>
<!-- Description -->
<div class="col-12">
<div class="field">
<p:outputLabel for="description" value="Description" styleClass="font-medium" />
<p:inputTextarea id="description" value="#{role.description}" readonly="#{readonly}"
placeholder="Description du rôle..." rows="3" autoResize="true" styleClass="w-full" />
</div>
</div>
</div>
<p:separator />
<!-- Section: Configuration -->
<h4 class="text-primary mt-2 mb-3">
<i class="pi pi-cog mr-2"></i>Configuration
</h4>
<div class="grid">
<!-- Realm -->
<c:if test="#{showRealmSelector}">
<p:outputLabel for="realmName" value="Realm *" />
<p:selectOneMenu id="realmName"
value="#{role.realmName}"
required="#{showRealmSelector}"
readonly="#{readonly}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<f:selectItems value="#{roleBean.availableRealms}" />
</p:selectOneMenu>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="realmName" value="Realm" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<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="#{roleGestionBean.availableRealms}" />
</p:selectOneMenu>
<p:message for="realmName" display="text" styleClass="mt-1" />
</div>
</div>
</c:if>
<!-- Client (si affiché) -->
<!-- Client -->
<c:if test="#{showClientSelector}">
<p:outputLabel for="clientId" value="Client *" />
<p:selectOneMenu id="clientId"
value="#{role.clientId}"
required="#{showClientSelector}"
readonly="#{readonly}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<f:selectItems value="#{roleBean.availableClients}" />
</p:selectOneMenu>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="clientId" value="Client" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<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="#{roleGestionBean.availableClients}" />
</p:selectOneMenu>
<p:message for="clientId" display="text" styleClass="mt-1" />
</div>
</div>
</c:if>
<!-- Rôle composite -->
<c:if test="#{showCompositeOptions}">
<p:outputLabel for="composite" value="Rôle composite" />
<p:selectBooleanCheckbox id="composite"
value="#{role.composite}"
readonly="#{readonly}" />
<div class="col-12 md:col-6">
<div class="field">
<div class="flex align-items-center gap-2 mt-3">
<p:selectBooleanCheckbox id="composite" value="#{role.composite}" readonly="#{readonly}" />
<p:outputLabel for="composite" value="Rôle composite" styleClass="font-medium" />
</div>
<small class="text-color-secondary block mt-1">
Un rôle composite regroupe plusieurs sous-rôles
</small>
</div>
</div>
</c:if>
</p:panelGrid>
</div>
<!-- Boutons d'action -->
<f:facet name="footer">
<div class="flex gap-2 justify-content-end">
<c:if test="#{not readonly}">
<c:choose>
<!-- Si hasSubmitAction est explicitement défini à true, utiliser action -->
<c:when test="#{hasSubmitAction == true}">
<p:commandButton
value="#{mode == 'create' ? 'Créer' : 'Modifier'}"
icon="pi pi-check"
styleClass="p-button-success"
action="#{submitAction}"
update="#{not empty update ? update : '@form'}"
process="@form" />
<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"
oncomplete="if(!args.validationFailed &amp;&amp; '#{dialogWidgetVar}' !== '') PF('#{dialogWidgetVar}').hide()" />
</c:when>
<!-- Si submitOutcome est fourni, utiliser outcome -->
<c:when test="#{not empty submitOutcome}">
<p:commandButton
value="#{mode == 'create' ? 'Créer' : 'Modifier'}"
icon="pi pi-check"
styleClass="p-button-success"
outcome="#{submitOutcome}"
update="#{not empty update ? update : '@form'}"
process="@form" />
<p:commandButton value="#{mode == 'create' ? 'Créer le rôle' : 'Enregistrer'}"
icon="pi pi-check" styleClass="p-button-success" outcome="#{submitOutcome}" />
</c:when>
<!-- Sinon, essayer d'utiliser submitAction si fourni -->
<c:otherwise>
<p:commandButton
value="#{mode == 'create' ? 'Créer' : 'Modifier'}"
icon="pi pi-check"
styleClass="p-button-success"
action="#{submitAction}"
update="#{not empty update ? update : '@form'}"
process="@form" />
<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" />
</c:otherwise>
</c:choose>
</c:if>
<p:commandButton
value="Annuler"
icon="pi pi-times"
styleClass="p-button-secondary"
onclick="PF('createRealmRoleDialog').hide(); PF('createClientRoleDialog').hide();"
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>
</ui:composition>
</ui:composition>

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

@@ -1,118 +1,141 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Bouton Action Utilisateur (WOU/DRY Pattern)
Composant réutilisable: Bouton Action Utilisateur - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Bouton générique pour actions utilisateur
Version: 2.0.0
Description: Bouton générique pour actions utilisateur avec support complet de
navigation (outcome), d'action serveur (action), et d'action client (onclick).
Supporte aussi le mode icon-only, les tooltips et les confirmations.
Paramètres:
- value: String (requis) - Texte du bouton
- icon: String (optionnel) - Classe d'icône PrimeIcons
- action: String (optionnel) - Action à exécuter
- outcome: String (optionnel) - Page de redirection
- severity: String (défaut: "primary") - Severity: "primary", "success", "warning", "danger", "info", "secondary"
- size: String (défaut: "normal") - Taille: "small", "normal", "large"
- icon: String (optionnel) - Classe d'icône PrimeIcons complète (ex: "pi pi-user-plus")
- hasAction: Boolean (défaut: false) - Indique si une action serveur est fournie
- action: MethodExpression (optionnel) - Action à exécuter (requis si hasAction=true)
- hasOutcome: Boolean (défaut: false) - Indique si un outcome est fourni
- outcome: String (optionnel) - Page de redirection (requis si hasOutcome=true)
- onclick: String (optionnel) - Code JavaScript à exécuter au clic
- severity: String (défaut: "primary") - "primary", "success", "warning", "danger", "info", "secondary", "help"
- size: String (défaut: "normal") - "small", "normal", "large"
- disabled: Boolean (défaut: false) - Désactiver le bouton
- update: String (optionnel) - Composants à mettre à jour
- process: String (défaut: "@this") - Composants à traiter
- styleClass: String (optionnel) - Classes CSS supplémentaires
- title: String (optionnel) - Tooltip du bouton
- iconOnly: Boolean (défaut: false) - Mode bouton icône seulement
- outlined: Boolean (défaut: false) - Style outlined
- rounded: Boolean (défaut: false) - Style arrondi
- confirmMessage: String (optionnel) - Message de confirmation avant l'action
Exemples d'utilisation:
1. Bouton simple:
1. Bouton avec action serveur:
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Créer Utilisateur" />
<ui:param name="icon" value="pi-user-plus" />
<ui:param name="icon" value="pi pi-user-plus" />
<ui:param name="hasAction" value="true" />
<ui:param name="action" value="#{userBean.createUser}" />
<ui:param name="severity" value="success" />
</ui:include>
2. Bouton avec redirection:
2. Bouton de navigation:
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Voir Profil" />
<ui:param name="icon" value="pi-eye" />
<ui:param name="icon" value="pi pi-eye" />
<ui:param name="hasOutcome" value="true" />
<ui:param name="outcome" value="/pages/user-manager/users/profile" />
</ui:include>
3. Bouton icône seulement:
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="value" value="Supprimer" />
<ui:param name="icon" value="pi pi-trash" />
<ui:param name="iconOnly" value="true" />
<ui:param name="severity" value="danger" />
<ui:param name="rounded" value="true" />
<ui:param name="hasAction" value="true" />
<ui:param name="action" value="#{userBean.delete}" />
</ui:include>
-->
<c:set var="severity" value="#{empty severity ? 'primary' : severity}" />
<c:set var="size" value="#{empty size ? 'normal' : size}" />
<c:set var="disabled" value="#{empty disabled ? false : disabled}" />
<c:set var="process" value="#{empty process ? '@this' : process}" />
<c:set var="hasAction" value="#{not empty action}" />
<c:set var="hasOutcome" value="#{not empty outcome}" />
<!-- Déterminer la classe selon la severity -->
<c:choose>
<c:when test="#{severity == 'primary'}">
<c:set var="buttonClass" value="p-button-primary" />
</c:when>
<c:when test="#{severity == 'success'}">
<c:set var="buttonClass" value="p-button-success" />
</c:when>
<c:when test="#{severity == 'warning'}">
<c:set var="buttonClass" value="p-button-warning" />
</c:when>
<c:when test="#{severity == 'danger'}">
<c:set var="buttonClass" value="p-button-danger" />
</c:when>
<c:when test="#{severity == 'info'}">
<c:set var="buttonClass" value="p-button-info" />
</c:when>
<c:otherwise>
<c:set var="buttonClass" value="p-button-secondary" />
</c:otherwise>
</c:choose>
<!-- Ajouter la taille -->
<c:set var="hasAction" value="#{empty hasAction ? false : hasAction}" />
<c:set var="hasOutcome" value="#{empty hasOutcome ? false : hasOutcome}" />
<c:set var="iconOnly" value="#{empty iconOnly ? false : iconOnly}" />
<c:set var="outlined" value="#{empty outlined ? false : outlined}" />
<c:set var="rounded" value="#{empty rounded ? false : rounded}" />
<!-- Construire la classe CSS -->
<c:set var="buttonClass" value="p-button-#{severity}" />
<c:if test="#{size == 'small'}">
<c:set var="buttonClass" value="#{buttonClass} p-button-sm" />
</c:if>
<c:if test="#{size == 'large'}">
<c:set var="buttonClass" value="#{buttonClass} p-button-lg" />
</c:if>
<!-- Ajouter les classes personnalisées -->
<c:if test="#{outlined}">
<c:set var="buttonClass" value="#{buttonClass} p-button-outlined" />
</c:if>
<c:if test="#{rounded}">
<c:set var="buttonClass" value="#{buttonClass} p-button-rounded" />
</c:if>
<c:if test="#{iconOnly}">
<c:set var="buttonClass" value="#{buttonClass} p-button-text" />
</c:if>
<c:if test="#{not empty styleClass}">
<c:set var="buttonClass" value="#{buttonClass} #{styleClass}" />
</c:if>
<c:choose>
<!-- Bouton avec action serveur -->
<c:when test="#{hasAction}">
<p:commandButton
value="#{value}"
icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}"
disabled="#{disabled}"
action="#{action}"
update="#{not empty update ? update : '@form'}"
process="#{process}" />
<p:commandButton value="#{iconOnly ? '' : value}" icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}" disabled="#{disabled}" action="#{action}"
update="#{not empty update ? update : '@form'}" process="#{process}"
onclick="#{not empty onclick ? onclick : ''}"
title="#{not empty title ? title : (iconOnly ? value : '')}">
<c:if test="#{not empty confirmMessage}">
<p:confirm header="Confirmation" message="#{confirmMessage}" icon="pi pi-exclamation-triangle" />
</c:if>
</p:commandButton>
</c:when>
<!-- Bouton avec navigation -->
<!-- Bouton avec navigation -->
<c:when test="#{hasOutcome}">
<p:commandButton
value="#{value}"
icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}"
disabled="#{disabled}"
outcome="#{outcome}"
update="#{not empty update ? update : '@form'}"
process="#{process}" />
<p:button value="#{iconOnly ? '' : value}" icon="#{not empty icon ? icon : ''}" styleClass="#{buttonClass}"
disabled="#{disabled}" outcome="#{outcome}"
title="#{not empty title ? title : (iconOnly ? value : '')}">
<c:if test="#{not empty paramUserId}">
<f:param name="userId" value="#{paramUserId}" />
</c:if>
<c:if test="#{not empty paramRealm}">
<f:param name="realm" value="#{paramRealm}" />
</c:if>
</p:button>
</c:when>
<!-- Bouton client-side (onclick uniquement) -->
<c:when test="#{not empty onclick}">
<p:commandButton value="#{iconOnly ? '' : value}" icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}" disabled="#{disabled}" type="button" onclick="#{onclick}"
title="#{not empty title ? title : (iconOnly ? value : '')}" />
</c:when>
<!-- Bouton sans action définie (désactivé avec tooltip) -->
<c:otherwise>
<p:commandButton
value="#{value}"
icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}"
disabled="true"
title="Aucune action définie" />
<p:commandButton value="#{iconOnly ? '' : value}" icon="#{not empty icon ? icon : ''}"
styleClass="#{buttonClass}" disabled="true"
title="#{not empty title ? title : 'Aucune action définie'}" />
</c:otherwise>
</c:choose>
</ui:composition>
</ui:composition>

View File

@@ -1,28 +1,55 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions">
<!--
Composant réutilisable: Contenu Carte KPI - Écosystème LionsDev
Auteur: Lions User Manager
Version: 2.1.0
Description: Contenu interne d'une carte KPI. Ce composant est inclus par
kpi-card.xhtml et ne devrait pas être utilisé directement.
Affiche le titre, la valeur, le sous-titre, l'indicateur de
croissance/statut, et une barre de progression optionnelle.
Paramètres hérités de kpi-card.xhtml:
- title, value, icon, iconColor, subtitle
- growthValue, growthLabel, growthType, showGrowth
- progressValue, showProgress
- statusIcon, statusLabel, statusValue
- noDataLabel
-->
<div class="p-4" style="min-height: 9rem;">
<!-- Header: Titre et Icône -->
<div class="flex align-items-center justify-content-between mb-3">
<span class="block text-600 font-medium text-sm">#{title}</span>
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<div class="flex align-items-center justify-content-center surface-100 border-round-lg"
style="width: 2.5rem; height: 2.5rem;">
<i class="pi #{icon} text-#{iconColor} text-lg"></i>
</div>
</div>
<!-- Valeur principale -->
<div class="text-900 font-bold text-2xl mb-2">#{value}</div>
<div class="text-900 font-bold text-2xl mb-2">
<c:choose>
<c:when test="#{not empty value}">
<c:set var="valueStr" value="#{value.toString()}" />
<c:choose>
<c:when test="#{valueStr == '-' or valueStr == '...'}">0</c:when>
<c:otherwise>#{value}</c:otherwise>
</c:choose>
</c:when>
<c:otherwise>0</c:otherwise>
</c:choose>
</div>
<!-- Sous-titre -->
<c:if test="#{not empty subtitle}">
<div class="text-500 text-xs mb-2">#{subtitle}</div>
</c:if>
<!-- Section Croissance ou Statut -->
<c:choose>
<!-- Mode Statut (statusIcon fourni) -->
@@ -36,67 +63,60 @@
</div>
</c:when>
<c:otherwise>
<div class="text-500 text-xs mb-2">Aucun #{statusLabel}</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>
<!-- Mode Croissance -->
<c:otherwise>
<c:choose>
<!-- Croissance en nombre -->
<c:when test="#{growthType == 'number'}">
<c:choose>
<c:when test="#{showGrowth and not empty growthValue and growthValue != '0' and growthValue != '0.0'}">
<div class="flex align-items-center mb-2">
<c:when
test="#{showGrowth and not empty growthValue and growthValue != '0' and growthValue != '0.0'}">
<div class="flex align-items-center mb-2">
<c:choose>
<!-- Croissance positive -->
<c:when test="#{growthValue >= 0}">
<i class="pi pi-arrow-up text-green-500 text-sm mr-2"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{growthValue}</span>
<c:if test="#{not empty growthLabel}">
<span class="text-500 text-xs">#{growthLabel}</span>
</c:if>
</div>
</c:when>
<c:otherwise>
<div class="text-500 text-xs mb-2">#{noDataLabel}</div>
</c:otherwise>
</c:choose>
<span class="text-green-600 font-semibold text-sm mr-2">
+#{growthValue}#{growthType == 'number' ? '' : '%'}
</span>
</c:when>
<!-- Croissance négative -->
<c:otherwise>
<i class="pi pi-arrow-down text-red-500 text-sm mr-2"></i>
<span class="text-red-600 font-semibold text-sm mr-2">
#{growthValue}#{growthType == 'number' ? '' : '%'}
</span>
</c:otherwise>
</c:choose>
<c:if test="#{not empty growthLabel}">
<span class="text-500 text-xs">#{growthLabel}</span>
</c:if>
</div>
</c:when>
<!-- Croissance en pourcentage (défaut) -->
<c:otherwise>
<c:choose>
<c:when test="#{showGrowth and not empty growthValue and growthValue != '0' and growthValue != '0.0'}">
<div class="flex align-items-center mb-2">
<c:choose>
<c:when test="#{growthValue >= 0}">
<i class="pi pi-arrow-up text-green-500 text-sm mr-2"></i>
<span class="text-green-600 font-semibold text-sm mr-2">+#{growthValue}%</span>
</c:when>
<c:otherwise>
<i class="pi pi-arrow-down text-red-500 text-sm mr-2"></i>
<span class="text-red-600 font-semibold text-sm mr-2">#{growthValue}%</span>
</c:otherwise>
</c:choose>
<c:if test="#{not empty growthLabel}">
<span class="text-500 text-xs">#{growthLabel}</span>
</c:if>
</div>
</c:when>
<c:otherwise>
<div class="text-500 text-xs mb-2">#{noDataLabel}</div>
</c:otherwise>
</c:choose>
<c:if test="#{empty subtitle}">
<div class="text-500 text-xs mb-2">#{noDataLabel}</div>
</c:if>
</c:otherwise>
</c:choose>
</c:otherwise>
</c:choose>
<!-- Barre de progression -->
<c:if test="#{showProgress and not empty progressValue}">
<p:progressBar value="#{progressValue}"
showValue="false"
styleClass="surface-200"
style="height: 0.5rem; width: 100%;" />
<div class="mt-2">
<p:progressBar value="#{progressValue}" showValue="false" styleClass="surface-200"
style="height: 0.5rem; width: 100%; border-radius: 4px;" />
</div>
</c:if>
</div>
</ui:composition>
</ui:composition>

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

@@ -34,7 +34,7 @@
<ui:param name="icon" value="pi-users" />
<ui:param name="iconColor" value="blue-600" />
</ui:include>
<!-- Autres KPI... -->
Autres KPI à ajouter ici
</ui:define>
</ui:include>
-->

View File

@@ -1,38 +1,46 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Champ Formulaire Utilisateur (WOU/DRY Pattern)
Composant réutilisable: Champ de Formulaire Utilisateur - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Champ de formulaire générique pour utilisateur
Version: 2.0.0
Description: Champ de formulaire générique avec label, input, message de validation
et texte d'aide. Supporte plusieurs types d'inputs PrimeFaces.
Conforme au pattern WOU/DRY.
Paramètres:
- id: String (requis) - ID du champ
- id: String (requis) - ID unique du champ
- label: String (requis) - Label du champ
- value: Object (requis) - Valeur du champ
- type: String (défaut: "text") - Type: "text", "email", "password", "number", "textarea", "select", "checkbox", "calendar"
- value: Object (requis) - Valeur liée au champ (expression EL)
- type: String (défaut: "text") - Type: "text", "email", "password", "number", "textarea",
"select", "checkbox", "calendar", "switch"
- required: Boolean (défaut: false) - Champ requis
- readonly: Boolean (défaut: false) - Mode lecture seule
- disabled: Boolean (défaut: false) - Champ désactivé
- placeholder: String (optionnel) - Placeholder
- helpText: String (optionnel) - Texte d'aide
- styleClass: String (optionnel) - Classes CSS supplémentaires
- helpText: String (optionnel) - Texte d'aide sous le champ
- styleClass: String (optionnel) - Classes CSS sur l'input
- wrapperClass: String (optionnel) - Classes CSS sur le wrapper div.field
- selectItems: List (optionnel) - Items pour select
- rows: Number (optionnel, défaut: 3) - Nombre de lignes pour textarea
- rows: Number (défaut: 3) - Nombre de lignes pour textarea
- minLength: Number (optionnel) - Longueur min pour validation
- maxLength: Number (optionnel) - Longueur max pour validation
- pattern: String (optionnel) - Pattern regex pour validation
- showMessage: Boolean (défaut: true) - Afficher les messages de validation
- colSize: String (optionnel) - Taille de colonne pour layout en grille
Exemples d'utilisation:
1. Champ texte:
1. Champ texte requis:
<ui:include src="/templates/components/shared/forms/user-form-field.xhtml">
<ui:param name="id" value="username" />
<ui:param name="label" value="Nom d'utilisateur" />
<ui:param name="value" value="#{user.username}" />
<ui:param name="required" value="true" />
<ui:param name="minLength" value="3" />
</ui:include>
2. Champ email:
@@ -42,6 +50,7 @@
<ui:param name="value" value="#{user.email}" />
<ui:param name="type" value="email" />
<ui:param name="required" value="true" />
<ui:param name="helpText" value="Doit être une adresse email valide" />
</ui:include>
3. Champ select:
@@ -50,114 +59,132 @@
<ui:param name="label" value="Statut" />
<ui:param name="value" value="#{user.statut}" />
<ui:param name="type" value="select" />
<ui:param name="selectItems" value="#{userBean.statutOptions}" />
<ui:param name="selectItems" value="#{bean.statutOptions}" />
</ui:include>
4. Toggle switch:
<ui:include src="/templates/components/shared/forms/user-form-field.xhtml">
<ui:param name="id" value="enabled" />
<ui:param name="label" value="Compte activé" />
<ui:param name="value" value="#{user.enabled}" />
<ui:param name="type" value="switch" />
</ui:include>
-->
<c:set var="type" value="#{empty type ? 'text' : type}" />
<c:set var="required" value="#{empty required ? false : required}" />
<c:set var="readonly" value="#{empty readonly ? false : readonly}" />
<c:set var="disabled" value="#{empty disabled ? false : disabled}" />
<c:set var="rows" value="#{empty rows ? 3 : rows}" />
<div class="field">
<p:outputLabel for="#{id}" value="#{label}#{required ? ' *' : ''}" />
<c:set var="showMessage" value="#{empty showMessage ? true : showMessage}" />
<div class="field #{wrapperClass}">
<!-- Label avec indicateur requis -->
<c:if test="#{type != 'checkbox' and type != 'switch'}">
<p:outputLabel for="#{id}" value="#{label}" styleClass="font-medium" />
<c:if test="#{required}">
<span class="text-red-500 ml-1" title="Champ obligatoire">*</span>
</c:if>
</c:if>
<c:choose>
<!-- Champ texte -->
<!-- Champ texte ou email -->
<c:when test="#{type == 'text' or type == 'email'}">
<p:inputText
id="#{id}"
value="#{value}"
required="#{required}"
readonly="#{readonly}"
placeholder="#{placeholder}"
type="#{type == 'email' ? 'email' : 'text'}"
styleClass="w-full #{styleClass}" />
<p:inputText id="#{id}" value="#{value}" required="#{required}" readonly="#{readonly}"
disabled="#{disabled}" placeholder="#{placeholder}" type="#{type == 'email' ? 'email' : 'text'}"
styleClass="w-full #{styleClass}">
<c:if test="#{not empty minLength}">
<f:validateLength minimum="#{minLength}" />
</c:if>
<c:if test="#{not empty maxLength}">
<f:validateLength maximum="#{maxLength}" />
</c:if>
<c:if test="#{not empty pattern}">
<f:validateRegex pattern="#{pattern}" />
</c:if>
</p:inputText>
</c:when>
<!-- Champ mot de passe -->
<c:when test="#{type == 'password'}">
<p:password
id="#{id}"
value="#{value}"
required="#{required}"
readonly="#{readonly}"
placeholder="#{placeholder}"
feedback="#{not empty feedback ? feedback : false}"
styleClass="w-full #{styleClass}" />
<p:password id="#{id}" value="#{value}" required="#{required}" readonly="#{readonly}"
disabled="#{disabled}" placeholder="#{placeholder}"
feedback="#{not empty feedback ? feedback : false}" toggleMask="true"
styleClass="w-full #{styleClass}">
<c:if test="#{not empty minLength}">
<f:validateLength minimum="#{minLength}" />
</c:if>
<c:if test="#{not empty maxLength}">
<f:validateLength maximum="#{maxLength}" />
</c:if>
</p:password>
</c:when>
<!-- Champ nombre -->
<c:when test="#{type == 'number'}">
<p:inputNumber
id="#{id}"
value="#{value}"
required="#{required}"
readonly="#{readonly}"
placeholder="#{placeholder}"
styleClass="w-full #{styleClass}" />
<p:inputNumber id="#{id}" value="#{value}" required="#{required}" readonly="#{readonly}"
disabled="#{disabled}" placeholder="#{placeholder}" styleClass="w-full #{styleClass}" />
</c:when>
<!-- Champ textarea -->
<c:when test="#{type == 'textarea'}">
<p:inputTextarea
id="#{id}"
value="#{value}"
required="#{required}"
readonly="#{readonly}"
placeholder="#{placeholder}"
rows="#{rows}"
styleClass="w-full #{styleClass}" />
<p:inputTextarea id="#{id}" value="#{value}" required="#{required}" readonly="#{readonly}"
disabled="#{disabled}" placeholder="#{placeholder}" rows="#{rows}" autoResize="true"
styleClass="w-full #{styleClass}">
<c:if test="#{not empty maxLength}">
<f:validateLength maximum="#{maxLength}" />
</c:if>
</p:inputTextarea>
</c:when>
<!-- Champ select -->
<c:when test="#{type == 'select'}">
<p:selectOneMenu
id="#{id}"
value="#{value}"
required="#{required}"
readonly="#{readonly}"
styleClass="w-full #{styleClass}">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<p:selectOneMenu id="#{id}" value="#{value}" required="#{required}" readonly="#{readonly}"
disabled="#{disabled}" styleClass="w-full #{styleClass}">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
<f:selectItems value="#{selectItems}" />
</p:selectOneMenu>
</c:when>
<!-- Champ checkbox -->
<c:when test="#{type == 'checkbox'}">
<p:selectBooleanCheckbox
id="#{id}"
value="#{value}"
readonly="#{readonly}" />
<div class="flex align-items-center gap-2">
<p:selectBooleanCheckbox id="#{id}" value="#{value}" readonly="#{readonly}"
disabled="#{disabled}" />
<p:outputLabel for="#{id}" value="#{label}" styleClass="font-medium" />
</div>
</c:when>
<!-- Toggle switch -->
<c:when test="#{type == 'switch'}">
<div class="flex align-items-center gap-2">
<p:toggleSwitch id="#{id}" value="#{value}" readonly="#{readonly}" disabled="#{disabled}" />
<p:outputLabel for="#{id}" value="#{label}" styleClass="font-medium" />
</div>
</c:when>
<!-- Champ calendar -->
<c:when test="#{type == 'calendar'}">
<p:calendar
id="#{id}"
value="#{value}"
required="#{required}"
readonly="#{readonly}"
pattern="dd/MM/yyyy"
styleClass="w-full #{styleClass}" />
<p:datePicker id="#{id}" value="#{value}" required="#{required}" readonly="#{readonly}"
disabled="#{disabled}" pattern="dd/MM/yyyy" showIcon="true" styleClass="w-full #{styleClass}" />
</c:when>
<!-- Par défaut: champ texte -->
<c:otherwise>
<p:inputText
id="#{id}"
value="#{value}"
required="#{required}"
readonly="#{readonly}"
placeholder="#{placeholder}"
styleClass="w-full #{styleClass}" />
<p:inputText id="#{id}" value="#{value}" required="#{required}" readonly="#{readonly}"
disabled="#{disabled}" placeholder="#{placeholder}" styleClass="w-full #{styleClass}" />
</c:otherwise>
</c:choose>
<!-- Message de validation -->
<c:if test="#{showMessage}">
<p:message for="#{id}" display="text" styleClass="mt-1" />
</c:if>
<!-- Texte d'aide -->
<c:if test="#{not empty helpText}">
<small class="text-color-secondary text-xs">#{helpText}</small>
<small class="text-color-secondary block mt-1">#{helpText}</small>
</c:if>
</div>
</ui:composition>
</ui:composition>

View File

@@ -1,21 +1,20 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Tableau Utilisateurs (WOU/DRY Pattern)
Composant réutilisable: Tableau de Données Utilisateurs - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Tableau de données pour afficher une liste d'utilisateurs
Version: 2.0.0
Description: Tableau de données PrimeFaces pour afficher et gérer une liste
d'utilisateurs avec colonnes configurables, tri, pagination,
sélection et actions.
Paramètres:
- users: List&lt;UserDTO&gt; (requis) - Liste des utilisateurs
- users: List<UserDTO> (requis) - Liste des utilisateurs
- var: String (défaut: "user") - Nom de la variable pour itération
- tableId: String (défaut: "userTable") - ID du tableau
- tableId: String (défaut: "userTable") - ID unique du tableau
- paginator: Boolean (défaut: true) - Activer la pagination
- rows: Number (défaut: 20) - Nombre de lignes par page
- showActions: Boolean (défaut: true) - Afficher la colonne actions
@@ -23,10 +22,19 @@
- showEmail: Boolean (défaut: true) - Afficher la colonne email
- showStatus: Boolean (défaut: true) - Afficher la colonne statut
- showSelection: Boolean (défaut: false) - Activer la sélection
- selection: UserDTO (optionnel) - Utilisateur sélectionné
- selectionMode: String (défaut: "single") - Mode: "single" ou "multiple"
- update: String (optionnel) - Composants à mettre à jour
- selection: UserDTO (optionnel) - Utilisateur(s) sélectionné(s)
- selectionMode: String (défaut: "single") - "single" ou "multiple"
- totalRecords: Long (optionnel) - Nombre total d'enregistrements
- hasOnPageChange: Boolean (défaut: false) - Si un listener de pagination est fourni
- onPageChange: MethodExpression (optionnel) - Listener de changement de page
- lazy: Boolean (défaut: false) - Chargement paresseux
- activateAction: MethodExpression (optionnel) - Action pour activer un utilisateur
- deactivateAction: MethodExpression (optionnel) - Action pour désactiver un utilisateur
- deleteAction: MethodExpression (optionnel) - Action pour supprimer un utilisateur
- update: String (optionnel) - Composants à mettre à jour après action
- styleClass: String (optionnel) - Classes CSS supplémentaires
- headerTitle: String (défaut: "Utilisateurs") - Titre du tableau
- emptyMessage: String (défaut: "Aucun utilisateur trouvé") - Message quand vide
Exemples d'utilisation:
@@ -35,14 +43,17 @@
<ui:param name="users" value="#{userBean.users}" />
</ui:include>
2. Tableau avec sélection:
2. Tableau complet avec actions:
<ui:include src="/templates/components/shared/tables/user-data-table.xhtml">
<ui:param name="users" value="#{userBean.users}" />
<ui:param name="showSelection" value="true" />
<ui:param name="selection" value="#{userBean.selectedUser}" />
<ui:param name="totalRecords" value="#{userBean.totalUsers}" />
<ui:param name="activateAction" value="#{userBean.activateUser}" />
<ui:param name="deactivateAction" value="#{userBean.deactivateUser}" />
<ui:param name="deleteAction" value="#{userBean.deleteUser}" />
<ui:param name="update" value="@form" />
</ui:include>
-->
<c:set var="varName" value="#{empty var ? 'user' : var}" />
<c:set var="tableId" value="#{empty tableId ? 'userTable' : tableId}" />
<c:set var="paginator" value="#{empty paginator ? true : paginator}" />
@@ -53,102 +64,135 @@
<c:set var="showStatus" value="#{empty showStatus ? true : showStatus}" />
<c:set var="showSelection" value="#{empty showSelection ? false : showSelection}" />
<c:set var="selectionMode" value="#{empty selectionMode ? 'single' : selectionMode}" />
<p:dataTable
id="#{tableId}"
value="#{users}"
var="user"
paginator="#{paginator}"
rows="#{rows}"
selection="#{selection}"
selectionMode="#{selectionMode}"
styleClass="w-full #{styleClass}"
<c:set var="hasOnPageChange" value="#{empty hasOnPageChange ? false : hasOnPageChange}" />
<c:set var="headerTitle" value="#{empty headerTitle ? 'Utilisateurs' : headerTitle}" />
<c:set var="emptyMessage" value="#{empty emptyMessage ? 'Aucun utilisateur trouvé' : emptyMessage}" />
<p:dataTable id="#{tableId}" value="#{users}" var="user" rowKey="#{user.id}" paginator="#{paginator}" rows="#{rows}"
rowCount="#{not empty totalRecords ? totalRecords : (users != null ? users.size() : 0)}"
selection="#{selection}" selectionMode="#{showSelection ? selectionMode : ''}"
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped w-full #{styleClass}"
widgetVar="#{tableId}Widget"
paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
rowsPerPageTemplate="10,20,50,100"
emptyMessage="Aucun utilisateur trouvé">
currentPageReportTemplate="Affichage {startRecord}-{endRecord} sur {totalRecords}"
emptyMessage="#{emptyMessage}" reflow="true" responsiveLayout="scroll" lazy="#{not empty lazy and lazy}"
sortMode="multiple" resizableColumns="true" scrollable="true">
<f:facet name="header">
<div class="flex align-items-center justify-content-between flex-wrap gap-2">
<span class="text-900 font-semibold text-xl">
<i class="pi pi-users mr-2"></i>#{headerTitle}
</span>
<div class="flex align-items-center gap-2">
<c:if test="#{not empty totalRecords}">
<p:tag value="#{totalRecords} résultats" severity="info" styleClass="text-xs" />
</c:if>
</div>
</div>
</f:facet>
<!-- Gestionnaire d'événements pour la pagination -->
<c:if test="#{hasOnPageChange}">
<p:ajax event="page" listener="#{onPageChange}" update="#{not empty update ? update : tableId}" />
</c:if>
<!-- Colonne de sélection -->
<c:if test="#{showSelection}">
<p:column selectionMode="#{selectionMode}" style="width: 3rem" />
<p:column selectionMode="#{selectionMode}" style="width: 50px" exportable="false" />
</c:if>
<!-- Colonne Username -->
<p:column headerText="Nom d'utilisateur" sortBy="#{user.username}" style="width: 15%">
<div class="flex align-items-center gap-2">
<p:avatar
label="#{user.prenom != null ? user.prenom.substring(0,1) : 'U'}#{user.nom != null ? user.nom.substring(0,1) : ''}"
styleClass="user-avatar-small" />
<span class="font-semibold">#{user.username}</span>
<!-- Colonne Username avec avatar -->
<p:column headerText="Nom d'utilisateur" sortBy="#{user.username}" filterBy="#{user.username}"
filterMatchMode="contains" style="min-width: 200px">
<div class="flex align-items-center gap-2 py-1">
<div class="border-circle overflow-hidden flex-shrink-0 flex align-items-center justify-content-center"
style="width: 32px; height: 32px; background-color: var(--primary-color); color: var(--primary-color-text);">
<span style="font-size: 0.75rem; font-weight: 600;">
#{user.prenom != null and user.prenom.length() > 0 ? user.prenom.substring(0,1) : 'U'}#{user.nom
!= null and user.nom.length() > 0 ? user.nom.substring(0,1) : ''}
</span>
</div>
<span class="font-semibold text-900">#{user.username}</span>
</div>
</p:column>
<!-- Colonne Nom complet -->
<p:column headerText="Nom complet" sortBy="#{user.nom}">
<div class="flex flex-column">
<span class="font-semibold">#{user.prenom} #{user.nom}</span>
<p:column headerText="Nom complet" sortBy="#{user.nom}" filterBy="#{user.nom}" filterMatchMode="contains"
style="min-width: 200px">
<div class="flex flex-column py-1">
<span class="font-medium text-900">#{user.prenom} #{user.nom}</span>
<c:if test="#{not empty user.fonction}">
<span class="text-color-secondary text-xs">#{user.fonction}</span>
<small class="text-600 text-xs mt-1">#{user.fonction}</small>
</c:if>
</div>
</p:column>
<!-- Colonne Email -->
<c:if test="#{showEmail}">
<p:column headerText="Email" sortBy="#{user.email}">
<div class="flex align-items-center gap-2">
<i class="pi pi-envelope text-color-secondary"></i>
<span>#{user.email}</span>
<p:column headerText="Email" sortBy="#{user.email}" filterBy="#{user.email}" filterMatchMode="contains"
style="min-width: 250px">
<div class="flex align-items-center gap-2 py-1">
<i class="pi pi-envelope text-400 text-sm"></i>
<span class="text-900 text-sm">#{user.email}</span>
<c:if test="#{user.emailVerified}">
<i class="pi pi-check-circle text-green-500" title="Email vérifié"></i>
<i class="pi pi-verified text-green-500 text-xs" title="Email vérifié"></i>
</c:if>
</div>
</p:column>
</c:if>
<!-- Colonne Statut -->
<c:if test="#{showStatus}">
<p:column headerText="Statut" sortBy="#{user.statut}">
<div class="flex align-items-center gap-2">
<p:tag
value="#{user.statut != null ? user.statut : 'INCONNU'}"
severity="#{user.enabled ? 'success' : 'danger'}" />
<c:if test="#{user.enabled}">
<i class="pi pi-check-circle text-green-500" title="Compte activé"></i>
</c:if>
<c:if test="#{not user.enabled}">
<i class="pi pi-times-circle text-red-500" title="Compte désactivé"></i>
</c:if>
<p:column headerText="Statut" sortBy="#{user.enabled}" style="width: 120px" exportable="true">
<div class="flex align-items-center justify-content-center py-1">
<p:tag value="#{user.enabled ? 'Actif' : 'Inactif'}"
severity="#{user.enabled ? 'success' : 'danger'}"
icon="#{user.enabled ? 'pi pi-check' : 'pi pi-times'}" />
</div>
</p:column>
</c:if>
<!-- Colonne Rôles -->
<c:if test="#{showRoles}">
<p:column headerText="Rôles">
<div class="flex flex-wrap gap-1">
<c:forEach var="role" items="#{user.realmRoles}" varStatus="status">
<c:if test="#{status.index &lt; 3}">
<p:tag value="#{role}" severity="info" styleClass="text-xs" />
</c:if>
</c:forEach>
<c:if test="#{user.realmRoles != null and user.realmRoles.size() &gt; 3}">
<p:tag value="+#{user.realmRoles.size() - 3}" severity="secondary" styleClass="text-xs" />
</c:if>
<p:column headerText="Rôles" style="min-width: 180px" exportable="false">
<div class="flex flex-wrap gap-1 py-1 align-items-center">
<c:choose>
<c:when test="#{user.realmRoles != null and !user.realmRoles.isEmpty()}">
<c:forEach var="role" items="#{user.realmRoles}" varStatus="status">
<c:if test="#{status.index lt 3}">
<p:tag value="#{role}" severity="info" styleClass="text-xs" />
</c:if>
</c:forEach>
<c:if test="#{user.realmRoles.size() gt 3}">
<p:tag value="+#{user.realmRoles.size() - 3}" severity="secondary" styleClass="text-xs"
title="#{user.realmRoles.size() - 3} rôle(s) supplémentaire(s)" />
</c:if>
</c:when>
<c:otherwise>
<span class="text-400 text-xs font-italic">Aucun rôle</span>
</c:otherwise>
</c:choose>
</div>
</p:column>
</c:if>
<!-- Colonne Actions -->
<c:if test="#{showActions}">
<p:column headerText="Actions" style="width: 150px">
<ui:include src="/templates/components/user-management/user-actions.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="layout" value="dropdown" />
<ui:param name="update" value="#{not empty update ? update : tableId}" />
</ui:include>
<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" />
<ui:param name="update" value="#{not empty update ? update : tableId}" />
<ui:param name="actionBean" value="#{actionBean}" />
<ui:param name="activateAction" value="#{activateAction}" />
<ui:param name="deactivateAction" value="#{deactivateAction}" />
<ui:param name="deleteAction" value="#{deleteAction}" />
</ui:include>
</div>
</p:column>
</c:if>
</p:dataTable>
</ui:composition>
</ui:composition>

View File

@@ -1,61 +1,16 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Actions Utilisateur (WOU/DRY Pattern)
Composant réutilisable: Actions Utilisateur - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Boutons d'action pour un utilisateur (activate, deactivate, delete, etc.)
Paramètres:
- user: UserDTO (requis) - L'utilisateur concerné
- showView: Boolean (défaut: true) - Afficher le bouton "Voir"
- showEdit: Boolean (défaut: true) - Afficher le bouton "Modifier"
- showDelete: Boolean (défaut: true) - Afficher le bouton "Supprimer"
- showActivate: Boolean (défaut: true) - Afficher le bouton "Activer"
- showDeactivate: Boolean (défaut: true) - Afficher le bouton "Désactiver"
- showResetPassword: Boolean (défaut: true) - Afficher le bouton "Réinitialiser mot de passe"
- showLogoutSessions: Boolean (défaut: false) - Afficher le bouton "Déconnecter toutes les sessions"
- viewAction: String (optionnel) - Action pour "Voir"
- editAction: String (optionnel) - Action pour "Modifier"
- deleteAction: String (optionnel) - Action pour "Supprimer"
- activateAction: String (optionnel) - Action pour "Activer"
- deactivateAction: String (optionnel) - Action pour "Désactiver"
- resetPasswordAction: String (optionnel) - Action pour "Réinitialiser mot de passe"
- logoutSessionsAction: String (optionnel) - Action pour "Déconnecter sessions"
- viewOutcome: String (optionnel) - Page pour "Voir"
- editOutcome: String (optionnel) - Page pour "Modifier"
- update: String (défaut: "@form") - Composants à mettre à jour
- layout: String (défaut: "horizontal") - Layout: "horizontal" ou "vertical" ou "dropdown"
Exemples d'utilisation:
1. Actions horizontales (défaut):
<ui:include src="/templates/components/user-management/user-actions.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="update" value="userTable" />
</ui:include>
2. Actions en dropdown:
<ui:include src="/templates/components/user-management/user-actions.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="layout" value="dropdown" />
<ui:param name="update" value="userTable" />
</ui:include>
3. Actions limitées:
<ui:include src="/templates/components/user-management/user-actions.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="showDelete" value="false" />
<ui:param name="showResetPassword" value="false" />
</ui:include>
Version: 2.1.0
Description: Ensemble de boutons d'action pour un utilisateur.
Utilise le confirmDialog global pour les suppressions.
-->
<c:set var="update" value="#{empty update ? '@form' : update}" />
<c:set var="layout" value="#{empty layout ? 'horizontal' : layout}" />
<c:set var="showView" value="#{empty showView ? true : showView}" />
@@ -65,321 +20,178 @@
<c:set var="showDeactivate" value="#{empty showDeactivate ? true : showDeactivate}" />
<c:set var="showResetPassword" value="#{empty showResetPassword ? true : showResetPassword}" />
<c:set var="showLogoutSessions" value="#{empty showLogoutSessions ? false : showLogoutSessions}" />
<!-- Définir les actions par défaut si non fournies -->
<c:set var="defaultActivateAction" value="#{userBean.activateUser(user.id)}" />
<c:set var="defaultDeactivateAction" value="#{userBean.deactivateUser(user.id)}" />
<c:set var="defaultDeleteAction" value="#{userBean.deleteUser(user.id)}" />
<c:set var="defaultResetPasswordAction" value="#{userBean.resetPassword(user.id)}" />
<c:set var="defaultLogoutSessionsAction" value="#{userBean.logoutAllSessions(user.id)}" />
<c:set var="iconOnly" value="#{empty iconOnly ? true : iconOnly}" />
<!-- Bean Cible pour les actions (par défaut userBean) -->
<c:set var="targetBean" value="#{empty actionBean ? userBean : actionBean}" />
<!-- Actions par défaut -->
<c:set var="actActivate" value="#{empty activateAction ? targetBean.activateUser(user.id) : activateAction}" />
<c:set var="actDeactivate"
value="#{empty deactivateAction ? targetBean.deactivateUser(user.id) : deactivateAction}" />
<c:set var="actDelete" value="#{empty deleteAction ? targetBean.deleteUser(user.id) : deleteAction}" />
<c:set var="actReset"
value="#{empty resetPasswordAction ? targetBean.resetPassword(user.id) : resetPasswordAction}" />
<c:set var="actLogout"
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"
type="button">
<p:menu>
<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 mot de passe"
icon="pi pi-key"
onclick="PF('resetPasswordDialog').show()" />
</c:if>
<p:separator />
<c:if test="#{showActivate and not user.enabled}">
<c:choose>
<c:when test="#{not empty activateAction}">
<p:menuitem
value="Activer"
icon="pi pi-check"
styleClass="text-green-600"
action="#{activateAction}"
update="#{update}" />
</c:when>
<c:otherwise>
<p:menuitem
value="Activer"
icon="pi pi-check"
styleClass="text-green-600"
action="#{userBean.activateUser(user.id)}"
update="#{update}" />
</c:otherwise>
</c:choose>
</c:if>
<c:if test="#{showDeactivate and user.enabled}">
<c:choose>
<c:when test="#{not empty deactivateAction}">
<p:menuitem
value="Désactiver"
icon="pi pi-times"
styleClass="text-orange-600"
action="#{deactivateAction}"
update="#{update}" />
</c:when>
<c:otherwise>
<p:menuitem
value="Désactiver"
icon="pi pi-times"
styleClass="text-orange-600"
action="#{userBean.deactivateUser(user.id)}"
update="#{update}" />
</c:otherwise>
</c:choose>
</c:if>
<c:if test="#{showLogoutSessions}">
<c:choose>
<c:when test="#{not empty logoutSessionsAction}">
<p:menuitem
value="Déconnecter toutes les sessions"
icon="pi pi-sign-out"
styleClass="text-blue-600"
action="#{logoutSessionsAction}"
update="#{update}" />
</c:when>
<c:otherwise>
<p:menuitem
value="Déconnecter toutes les sessions"
icon="pi pi-sign-out"
styleClass="text-blue-600"
action="#{userBean.logoutAllSessions(user.id)}"
update="#{update}" />
</c:otherwise>
</c:choose>
</c:if>
<c:if test="#{showDelete}">
<p:separator />
<p:menuitem
value="Supprimer"
icon="pi pi-trash"
styleClass="text-red-600"
onclick="PF('confirmDeleteDialog').show()" />
</c:if>
</p:menu>
</p:commandButton>
</c:when>
<!-- Layout Horizontal (défaut) -->
<c:otherwise>
<div class="flex gap-1">
<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:commandButton
icon="pi pi-eye"
title="Voir le profil"
styleClass="p-button-text p-button-sm p-button-info"
outcome="#{not empty viewOutcome ? viewOutcome : '/pages/user-manager/users/profile'}">
<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:commandButton>
</p:menuitem>
</c:if>
<c:if test="#{showEdit}">
<p:commandButton
icon="pi pi-pencil"
title="Modifier"
styleClass="p-button-text p-button-sm p-button-warning"
outcome="#{not empty editOutcome ? editOutcome : '/pages/user-manager/users/edit'}">
<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:commandButton>
</p:menuitem>
</c:if>
<c:if test="#{showResetPassword}">
<p:commandButton
icon="pi pi-key"
title="Réinitialiser mot de passe"
styleClass="p-button-text p-button-sm p-button-help"
onclick="PF('resetPasswordDialog').show()" />
<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}">
<c:choose>
<c:when test="#{not empty activateAction}">
<p:commandButton
icon="pi pi-check"
title="Activer"
styleClass="p-button-text p-button-sm p-button-success"
action="#{activateAction}"
update="#{update}" />
</c:when>
<c:otherwise>
<p:commandButton
icon="pi pi-check"
title="Activer"
styleClass="p-button-text p-button-sm p-button-success"
action="#{userBean.activateUser(user.id)}"
update="#{update}" />
</c:otherwise>
</c:choose>
<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}">
<c:choose>
<c:when test="#{not empty deactivateAction}">
<p:commandButton
icon="pi pi-times"
title="Désactiver"
styleClass="p-button-text p-button-sm p-button-warning"
action="#{deactivateAction}"
update="#{update}" />
</c:when>
<c:otherwise>
<p:commandButton
icon="pi pi-times"
title="Désactiver"
styleClass="p-button-text p-button-sm p-button-warning"
action="#{userBean.deactivateUser(user.id)}"
update="#{update}" />
</c:otherwise>
</c:choose>
<p:menuitem value="Désactiver" icon="pi pi-times" styleClass="text-orange-600"
action="#{actDeactivate}" update="#{update}" />
</c:if>
<c:if test="#{showLogoutSessions}">
<c:choose>
<c:when test="#{not empty logoutSessionsAction}">
<p:commandButton
icon="pi pi-sign-out"
title="Déconnecter toutes les sessions"
styleClass="p-button-text p-button-sm p-button-info"
action="#{logoutSessionsAction}"
update="#{update}" />
</c:when>
<c:otherwise>
<p:commandButton
icon="pi pi-sign-out"
title="Déconnecter toutes les sessions"
styleClass="p-button-text p-button-sm p-button-info"
action="#{userBean.logoutAllSessions(user.id)}"
update="#{update}" />
</c:otherwise>
</c:choose>
<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:commandButton
icon="pi pi-trash"
title="Supprimer"
styleClass="p-button-text p-button-sm p-button-danger"
onclick="PF('confirmDeleteDialog').show()" />
<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:menuButton>
</c:when>
<!-- Layout Horizontal -->
<c:otherwise>
<div class="flex gap-1 align-items-center">
<c:if test="#{showView}">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="icon" value="pi pi-eye" />
<ui:param name="iconOnly" value="true" />
<ui:param name="severity" value="info" />
<ui:param name="title" value="Voir" />
<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/view'}" />
<ui:param name="paramUserId" value="#{user.id}" />
<ui:param name="paramRealm" value="#{user.realmName}" />
</ui:include>
</c:if>
<c:if test="#{showEdit}">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="icon" value="pi pi-pencil" />
<ui:param name="iconOnly" value="true" />
<ui:param name="severity" value="warning" />
<ui:param name="title" value="Modifier" />
<ui:param name="rounded" value="true" />
<ui:param name="hasOutcome" value="true" />
<ui:param name="outcome"
value="#{not empty editOutcome ? editOutcome : '/pages/user-manager/users/edit'}" />
<ui:param name="paramUserId" value="#{user.id}" />
<ui:param name="paramRealm" value="#{user.realmName}" />
</ui:include>
</c:if>
<c:if test="#{showResetPassword}">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="icon" value="pi pi-key" />
<ui:param name="iconOnly" value="true" />
<ui:param name="severity" value="help" />
<ui:param name="title" value="Réinitialiser MDP" />
<ui:param name="rounded" value="true" />
<ui:param name="onclick" value="PF('resetPasswordDialog_#{user.id}').show()" />
</ui:include>
</c:if>
<c:if test="#{showActivate and not user.enabled}">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="icon" value="pi pi-check" />
<ui:param name="iconOnly" value="true" />
<ui:param name="severity" value="success" />
<ui:param name="title" value="Activer" />
<ui:param name="rounded" value="true" />
<ui:param name="hasAction" value="true" />
<ui:param name="action" value="#{actActivate}" />
<ui:param name="update" value="#{update}" />
</ui:include>
</c:if>
<c:if test="#{showDeactivate and user.enabled}">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="icon" value="pi pi-times" />
<ui:param name="iconOnly" value="true" />
<ui:param name="severity" value="warning" />
<ui:param name="title" value="Désactiver" />
<ui:param name="rounded" value="true" />
<ui:param name="hasAction" value="true" />
<ui:param name="action" value="#{actDeactivate}" />
<ui:param name="update" value="#{update}" />
</ui:include>
</c:if>
<c:if test="#{showDelete}">
<ui:include src="/templates/components/shared/buttons/button-user-action.xhtml">
<ui:param name="icon" value="pi pi-trash" />
<ui:param name="iconOnly" value="true" />
<ui:param name="severity" value="danger" />
<ui:param name="title" value="Supprimer" />
<ui:param name="rounded" value="true" />
<ui:param name="hasAction" value="true" />
<ui:param name="action" value="#{actDelete}" />
<ui:param name="update" value="#{update}" />
<ui:param name="confirmMessage" value="Supprimer l'utilisateur #{user.username} ?" />
</ui:include>
</c:if>
</div>
</c:otherwise>
</c:choose>
<!-- Dialog de confirmation de suppression -->
<p:confirmDialog
id="confirmDeleteDialog"
widgetVar="confirmDeleteDialog"
message="Êtes-vous sûr de vouloir supprimer l'utilisateur #{user.username} ?"
header="Confirmation de suppression"
severity="warn">
<c:choose>
<c:when test="#{not empty deleteAction}">
<p:commandButton
value="Oui"
icon="pi pi-check"
styleClass="p-button-danger"
action="#{deleteAction}"
update="#{update}"
oncomplete="PF('confirmDeleteDialog').hide()" />
</c:when>
<c:otherwise>
<p:commandButton
value="Oui"
icon="pi pi-check"
styleClass="p-button-danger"
action="#{userBean.deleteUser(user.id)}"
update="#{update}"
oncomplete="PF('confirmDeleteDialog').hide()" />
</c:otherwise>
</c:choose>
<p:commandButton
value="Non"
icon="pi pi-times"
styleClass="p-button-secondary"
onclick="PF('confirmDeleteDialog').hide()" />
</p:confirmDialog>
<!-- Dialog de réinitialisation de mot de passe -->
<p:dialog
id="resetPasswordDialog"
widgetVar="resetPasswordDialog"
header="Réinitialiser le mot de passe"
modal="true"
styleClass="w-full md:w-4">
<!-- 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"
appendTo="@(body)">
<h:form>
<p:panelGrid columns="2" styleClass="w-full">
<p:outputLabel for="newPassword" value="Nouveau mot de passe *" />
<p:password id="newPassword"
value="#{userBean.newPassword}"
feedback="true"
required="true"
styleClass="w-full">
<f:validateLength minimum="8" maximum="100" />
</p:password>
<p:outputLabel for="newPasswordConfirm" value="Confirmer *" />
<p:password id="newPasswordConfirm"
value="#{userBean.newPasswordConfirm}"
required="true"
styleClass="w-full" />
</p:panelGrid>
<f:facet name="footer">
<div class="flex gap-2 justify-content-end">
<c:choose>
<c:when test="#{not empty resetPasswordAction}">
<p:commandButton
value="Réinitialiser"
icon="pi pi-check"
styleClass="p-button-primary"
action="#{resetPasswordAction}"
update="#{update}"
oncomplete="PF('resetPasswordDialog').hide()"
process="@form" />
</c:when>
<c:otherwise>
<p:commandButton
value="Réinitialiser"
icon="pi pi-check"
styleClass="p-button-primary"
action="#{userBean.resetPassword(user.id)}"
update="#{update}"
oncomplete="PF('resetPasswordDialog').hide()"
process="@form" />
</c:otherwise>
</c:choose>
<p:commandButton
value="Annuler"
icon="pi pi-times"
styleClass="p-button-secondary"
onclick="PF('resetPasswordDialog').hide()" />
</div>
</f:facet>
<div class="field">
<p:outputLabel for="newPass" value="Nouveau mot de passe" />
<p:password id="newPass" value="#{targetBean.newPassword}" feedback="true" required="true"
toggleMask="true" styleClass="w-full" />
</div>
<div class="field">
<p:outputLabel for="confPass" value="Confirmer" />
<p:password id="confPass" value="#{targetBean.newPasswordConfirm}" required="true" toggleMask="true"
styleClass="w-full" />
</div>
<div class="flex justify-content-end gap-2">
<p:commandButton value="Valider" icon="pi pi-check" action="#{actReset}" update="#{update}"
oncomplete="PF('resetPasswordDialog_#{user.id}').hide()" />
<p:commandButton value="Annuler" icon="pi pi-times" styleClass="p-button-secondary p-button-outlined"
onclick="PF('resetPasswordDialog_#{user.id}').hide()" />
</div>
</h:form>
</p:dialog>
</ui:composition>
</ui:composition>

View File

@@ -1,23 +1,25 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Carte Utilisateur (WOU/DRY Pattern)
Composant réutilisable: Carte Utilisateur - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Affiche une carte utilisateur avec informations principales et actions
Version: 2.1.0
Description: Affiche une carte utilisateur avec avatar, informations principales,
rôles et actions. Utilisable en mode carte standalone ou en grille.
Paramètres:
- user: UserDTO (requis) - L'utilisateur à afficher
- showActions: Boolean (défaut: true) - Afficher les boutons d'action
- showRoles: Boolean (défaut: true) - Afficher les rôles de l'utilisateur
- clickable: Boolean (défaut: true) - Rendre la carte cliquable
- showOrganisation: Boolean (défaut: false) - Afficher l'organisation
- clickable: Boolean (défaut: true) - Rendre la carte cliquable (bouton Voir)
- outcome: String (optionnel) - Page de destination au clic
- editOutcome: String (optionnel) - Page d'édition
- deleteAction: MethodExpression (optionnel) - Action de suppression
- hasDeleteAction: Boolean (défaut: false) - Si une action delete est fournie
- styleClass: String (optionnel) - Classes CSS supplémentaires
Exemples d'utilisation:
@@ -27,104 +29,127 @@
<ui:param name="user" value="#{userBean.selectedUser}" />
</ui:include>
2. Carte avec actions:
<ui:include src="/templates/components/user-management/user-card.xhtml">
<ui:param name="user" value="#{userBean.selectedUser}" />
<ui:param name="showActions" value="true" />
<ui:param name="outcome" value="/pages/user-manager/users/profile" />
</ui:include>
3. Carte sans rôles:
<ui:include src="/templates/components/user-management/user-card.xhtml">
<ui:param name="user" value="#{userBean.selectedUser}" />
<ui:param name="showRoles" value="false" />
</ui:include>
2. Carte dans une grille:
<c:forEach var="u" items="#{userBean.users}">
<div class="col-12 md:col-6 lg:col-4">
<ui:include src="/templates/components/user-management/user-card.xhtml">
<ui:param name="user" value="#{u}" />
<ui:param name="showOrganisation" value="true" />
</ui:include>
</div>
</c:forEach>
-->
<c:set var="showActions" value="#{empty showActions ? true : showActions}" />
<c:set var="showRoles" value="#{empty showRoles ? true : showRoles}" />
<c:set var="showOrganisation" value="#{empty showOrganisation ? false : showOrganisation}" />
<c:set var="clickable" value="#{empty clickable ? true : clickable}" />
<p:card styleClass="user-card #{styleClass}" rendered="#{not empty user}">
<c:set var="hasDeleteAction" value="#{empty hasDeleteAction ? false : hasDeleteAction}" />
<c:set var="userInitials"
value="#{user.prenom != null and user.prenom.length() > 0 ? user.prenom.substring(0,1) : 'U'}#{user.nom != null and user.nom.length() > 0 ? user.nom.substring(0,1) : ''}" />
<p:card styleClass="user-card shadow-1 hover:shadow-3 transition-all transition-duration-200 #{styleClass}"
rendered="#{not empty user}">
<f:facet name="header">
<div class="flex align-items-center gap-2">
<p:avatar
label="#{user.firstName != null ? user.firstName.substring(0,1) : 'U'}#{user.lastName != null ? user.lastName.substring(0,1) : ''}"
styleClass="user-avatar"
size="large" />
<div class="flex flex-column">
<h3 class="m-0">#{user.firstName} #{user.lastName}</h3>
<div class="flex align-items-center gap-3 p-3">
<!-- Avatar -->
<p:avatar label="#{userInitials}" size="large" shape="circle"
style="background-color: var(--primary-color); color: var(--primary-color-text);" />
<!-- Nom et Username -->
<div class="flex flex-column flex-1">
<h3 class="m-0 text-900">#{user.prenom} #{user.nom}</h3>
<span class="text-color-secondary text-sm">@#{user.username}</span>
</div>
<!-- Badge de statut -->
<p:tag value="#{user.enabled ? 'Actif' : 'Inactif'}" severity="#{user.enabled ? 'success' : 'danger'}"
icon="#{user.enabled ? 'pi pi-check' : 'pi pi-times'}" />
</div>
</f:facet>
<div class="user-card-content">
<!-- Informations principales -->
<!-- Informations de contact -->
<div class="flex flex-column gap-2 mb-3">
<!-- Email -->
<div class="flex align-items-center gap-2">
<i class="pi pi-envelope text-color-secondary"></i>
<span>#{user.email}</span>
<i class="pi pi-envelope text-color-secondary text-sm" style="width: 1rem;"></i>
<span class="text-sm">#{user.email}</span>
<c:if test="#{user.emailVerified}">
<i class="pi pi-verified text-green-500 text-xs" title="Email vérifié"></i>
</c:if>
</div>
<!-- Téléphone -->
<c:if test="#{not empty user.telephone}">
<div class="flex align-items-center gap-2">
<i class="pi pi-phone text-color-secondary"></i>
<span>#{user.telephone}</span>
<i class="pi pi-phone text-color-secondary text-sm" style="width: 1rem;"></i>
<span class="text-sm">#{user.telephone}</span>
</div>
</c:if>
<!-- Organisation -->
<c:if test="#{showOrganisation and not empty user.organisation}">
<div class="flex align-items-center gap-2">
<i class="pi pi-building text-color-secondary text-sm" style="width: 1rem;"></i>
<span class="text-sm">#{user.organisation}</span>
<c:if test="#{not empty user.fonction}">
<span class="text-color-secondary text-xs">- #{user.fonction}</span>
</c:if>
</div>
</c:if>
<!-- Statut -->
<div class="flex align-items-center gap-2">
<i class="pi pi-circle-fill text-color-secondary"></i>
<p:tag
value="#{user.statut != null ? user.statut : 'INCONNU'}"
severity="#{user.enabled ? 'success' : 'danger'}" />
</div>
</div>
<!-- Rôles -->
<c:if test="#{showRoles and not empty user.roles}">
<div class="flex flex-column gap-2 mb-3">
<h5 class="m-0">Rôles</h5>
<c:if test="#{showRoles}">
<div class="flex flex-column gap-2">
<span class="text-600 font-medium text-xs text-uppercase">Rôles</span>
<div class="flex flex-wrap gap-1">
<c:forEach var="role" items="#{user.roles}">
<p:tag value="#{role.name}" severity="info" />
</c:forEach>
<c:choose>
<c:when test="#{not empty user.realmRoles and not user.realmRoles.isEmpty()}">
<c:forEach var="role" items="#{user.realmRoles}" varStatus="status">
<c:if test="#{status.index lt 4}">
<p:tag value="#{role}" severity="info" styleClass="text-xs" />
</c:if>
</c:forEach>
<c:if test="#{user.realmRoles.size() gt 4}">
<p:tag value="+#{user.realmRoles.size() - 4}" severity="secondary"
styleClass="text-xs"
title="#{user.realmRoles.size() - 4} rôle(s) supplémentaire(s)" />
</c:if>
</c:when>
<c:otherwise>
<span class="text-color-secondary text-xs font-italic">Aucun rôle attribué</span>
</c:otherwise>
</c:choose>
</div>
</div>
</c:if>
</div>
<!-- Actions -->
<f:facet name="footer">
<c:if test="#{showActions}">
<div class="flex gap-2 justify-content-end">
<p:commandButton
icon="pi pi-eye"
title="Voir le profil"
styleClass="p-button-text p-button-sm"
outcome="#{not empty outcome ? outcome : '/pages/user-manager/users/profile'}"
rendered="#{clickable}">
<f:param name="userId" value="#{user.id}" />
</p:commandButton>
<p:commandButton
icon="pi pi-pencil"
title="Modifier"
styleClass="p-button-text p-button-sm p-button-warning"
outcome="/pages/user-manager/users/edit">
<f:param name="userId" value="#{user.id}" />
</p:commandButton>
<p:commandButton
icon="pi pi-trash"
title="Supprimer"
styleClass="p-button-text p-button-sm p-button-danger"
onclick="PF('confirmDeleteDialog').show()" />
<ui:include src="/templates/components/user-management/user-actions.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="iconOnly" value="true" />
<ui:param name="layout" value="horizontal" />
<ui:param name="showView" value="#{clickable}" />
<ui:param name="showEdit" value="true" />
<ui:param name="showDelete" value="#{hasDeleteAction}" />
<ui:param name="showResetPassword" value="true" />
<ui:param name="showActivate" value="true" />
<ui:param name="showDeactivate" value="true" />
<ui:param name="outcome" value="#{outcome}" />
<ui:param name="actionBean" value="#{actionBean}" />
<ui:param name="editOutcome" value="#{editOutcome}" />
<ui:param name="deleteAction" value="#{deleteAction}" />
</ui:include>
</div>
</c:if>
</f:facet>
</p:card>
</ui:composition>
</ui:composition>

View File

@@ -0,0 +1,283 @@
<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">
<!--
Composant réutilisable: Contenu du formulaire Utilisateur - Écosystème LionsDev
Auteur: Lions User Manager
Version: 2.0.0
Description: Corps du formulaire utilisateur (création/édition) avec validation
et messages d'erreur. Doit être contenu dans un h:form ou utilisé
via user-form.xhtml qui gère automatiquement le wrapper <h:form>.
Paramètres hérités de user-form.xhtml:
- user: UserDTO (requis)
- mode: String ("create" | "edit")
- showRealmSelector: Boolean
- showPasswordFields: Boolean
- readonly: Boolean
- hasSubmitAction: Boolean
- submitAction: MethodExpression
- submitOutcome: String
- update: String
- cancelOutcome: String
-->
<p:messages id="formMessages" showDetail="true" closable="true" styleClass="mb-3" />
<p:panel header="#{mode == 'create' ? 'Nouvel Utilisateur' : 'Modifier Utilisateur'}" styleClass="w-full">
<!-- Section: Identité -->
<h4 class="text-primary mt-0 mb-3">
<i class="pi pi-id-card mr-2"></i>Identité
</h4>
<div class="grid">
<!-- Username -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="username" value="Nom d'utilisateur" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<p:inputText id="username" value="#{user.username}" required="true"
readonly="#{readonly or mode == 'edit'}" placeholder="jdupont" styleClass="w-full">
<f:validateLength minimum="3" maximum="100" />
<f:validateRegex pattern="^[a-zA-Z0-9._-]+$" />
</p:inputText>
<p:message for="username" display="text" styleClass="mt-1" />
</div>
</div>
<!-- Email -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="email" value="Email" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<p:inputText id="email" value="#{user.email}" required="true" readonly="#{readonly}"
placeholder="jean.dupont@lions.dev" type="email" styleClass="w-full">
<f:validateLength minimum="5" maximum="255" />
</p:inputText>
<p:message for="email" display="text" styleClass="mt-1" />
</div>
</div>
<!-- Prénom -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="prenom" value="Prénom" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<p:inputText id="prenom" value="#{user.prenom}" required="true" readonly="#{readonly}"
placeholder="Jean" styleClass="w-full">
<f:validateLength minimum="2" maximum="100" />
</p:inputText>
<p:message for="prenom" display="text" styleClass="mt-1" />
</div>
</div>
<!-- Nom -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="nom" value="Nom" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<p:inputText id="nom" value="#{user.nom}" required="true" readonly="#{readonly}"
placeholder="Dupont" styleClass="w-full">
<f:validateLength minimum="2" maximum="100" />
</p:inputText>
<p:message for="nom" display="text" styleClass="mt-1" />
</div>
</div>
</div>
<p:separator />
<!-- Section: Coordonnées -->
<h4 class="text-primary mt-2 mb-3">
<i class="pi pi-map-marker mr-2"></i>Coordonnées
</h4>
<div class="grid">
<!-- Téléphone -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="telephone" value="Téléphone" styleClass="font-medium" />
<p:inputText id="telephone" value="#{user.telephone}" readonly="#{readonly}"
placeholder="+225 07 12 34 56 78" styleClass="w-full" />
<p:message for="telephone" display="text" styleClass="mt-1" />
</div>
</div>
<!-- Ville -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="ville" value="Ville" styleClass="font-medium" />
<p:inputText id="ville" value="#{user.ville}" readonly="#{readonly}" placeholder="Abidjan"
styleClass="w-full" />
</div>
</div>
<!-- Pays -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="pays" value="Pays" styleClass="font-medium" />
<p:inputText id="pays" value="#{user.pays}" readonly="#{readonly}" placeholder="Côte d'Ivoire"
styleClass="w-full" />
</div>
</div>
</div>
<p:separator />
<!-- Section: Organisation -->
<h4 class="text-primary mt-2 mb-3">
<i class="pi pi-building mr-2"></i>Organisation
</h4>
<div class="grid">
<!-- Organisation -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="organisation" value="Organisation" styleClass="font-medium" />
<p:inputText id="organisation" value="#{user.organisation}" readonly="#{readonly}"
placeholder="Lions Dev" styleClass="w-full" />
</div>
</div>
<!-- Département -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="departement" value="Département" styleClass="font-medium" />
<p:inputText id="departement" value="#{user.departement}" readonly="#{readonly}" placeholder="IT"
styleClass="w-full" />
</div>
</div>
<!-- Fonction -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="fonction" value="Fonction" styleClass="font-medium" />
<p:inputText id="fonction" value="#{user.fonction}" readonly="#{readonly}"
placeholder="Développeur Senior" styleClass="w-full" />
</div>
</div>
</div>
<p:separator />
<!-- Section: Statut et Paramètres -->
<h4 class="text-primary mt-2 mb-3">
<i class="pi pi-cog mr-2"></i>Statut et Paramètres
</h4>
<div class="grid">
<!-- Statut -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="statut" value="Statut" styleClass="font-medium" />
<p:selectOneMenu id="statut" value="#{user.statut}" readonly="#{readonly}" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
<f:selectItems value="#{userBean.statutOptions}" />
</p:selectOneMenu>
</div>
</div>
<!-- Realm (si affiché) -->
<c:if test="#{showRealmSelector}">
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="realmName" value="Realm" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<p:selectOneMenu id="realmName" value="#{user.realmName}" required="#{showRealmSelector}"
readonly="#{readonly}" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
<f:selectItems value="#{userBean.availableRealms}" />
</p:selectOneMenu>
<p:message for="realmName" display="text" styleClass="mt-1" />
</div>
</div>
</c:if>
<!-- Enabled -->
<div class="col-12 md:col-3">
<div class="field">
<div class="flex align-items-center gap-2 mt-4">
<p:selectBooleanCheckbox id="enabled" value="#{user.enabled}" readonly="#{readonly}" />
<p:outputLabel for="enabled" value="Compte activé" styleClass="font-medium" />
</div>
</div>
</div>
<!-- Email vérifié -->
<div class="col-12 md:col-3">
<div class="field">
<div class="flex align-items-center gap-2 mt-4">
<p:selectBooleanCheckbox id="emailVerified" value="#{user.emailVerified}"
readonly="#{readonly}" />
<p:outputLabel for="emailVerified" value="Email vérifié" styleClass="font-medium" />
</div>
</div>
</div>
</div>
<!-- Champs mot de passe (mode création uniquement) -->
<c:if test="#{showPasswordFields and mode == 'create'}">
<p:separator />
<h4 class="text-primary mt-2 mb-3">
<i class="pi pi-lock mr-2"></i>Mot de passe
</h4>
<div class="grid">
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="password" value="Mot de passe" styleClass="font-medium" />
<span class="text-red-500 ml-1">*</span>
<p:password id="password" value="#{userBean.password}" required="true" feedback="true"
toggleMask="true" placeholder="Minimum 8 caractères" styleClass="w-full">
<f:validateLength minimum="8" maximum="100" />
</p:password>
<p:message for="password" display="text" styleClass="mt-1" />
</div>
</div>
<div class="col-12 md:col-6">
<div class="field">
<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="#{userBean.passwordConfirm}" required="true"
toggleMask="true" placeholder="Répétez le mot de passe" styleClass="w-full" />
<p:message for="passwordConfirm" display="text" styleClass="mt-1" />
</div>
</div>
</div>
</c:if>
<!-- Boutons d'action -->
<f:facet name="footer">
<div class="flex gap-2 justify-content-end">
<c:if test="#{not readonly}">
<c:choose>
<c:when test="#{hasSubmitAction == true}">
<p:commandButton value="#{mode == 'create' ? 'Créer' : 'Enregistrer'}" icon="pi pi-check"
styleClass="p-button-success" action="#{submitAction}"
update="#{not empty update ? update : '@form'}" process="@form" validateClient="true" />
</c:when>
<c:when test="#{not empty submitOutcome}">
<p:commandButton value="#{mode == 'create' ? 'Créer' : 'Enregistrer'}" icon="pi pi-check"
styleClass="p-button-success" outcome="#{submitOutcome}" />
</c:when>
<c:otherwise>
<p:commandButton value="#{mode == 'create' ? 'Créer' : 'Enregistrer'}" icon="pi pi-check"
styleClass="p-button-success" action="#{submitAction}"
update="#{not empty update ? update : '@form'}" process="@form" validateClient="true" />
</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/users/list'}"
immediate="true" />
</div>
</f:facet>
</p:panel>
</ui:composition>

View File

@@ -23,6 +23,7 @@
- submitOutcome: String (optionnel) - Page de redirection après soumission
- update: String (optionnel) - Composants à mettre à jour après soumission
- hasSubmitAction: Boolean (optionnel) - Indicateur si submitAction est fourni (pour éviter l'évaluation)
- useParentForm: Boolean (défaut: false) - Utiliser le formulaire parent au lieu de créer un nouveau formulaire
Exemples d'utilisation:
@@ -53,215 +54,42 @@
<c:set var="showRealmSelector" value="#{empty showRealmSelector ? false : showRealmSelector}" />
<c:set var="showPasswordFields" value="#{empty showPasswordFields ? true : showPasswordFields}" />
<c:set var="readonly" value="#{empty readonly ? false : readonly}" />
<c:set var="useParentForm" value="#{empty useParentForm ? false : useParentForm}" />
<h:form id="#{formId}">
<p:panel header="#{mode == 'create' ? 'Nouvel Utilisateur' : 'Modifier Utilisateur'}"
styleClass="w-full">
<!-- Informations de base -->
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-6">
<!-- Username -->
<p:outputLabel for="username" value="Nom d'utilisateur *" />
<p:inputText id="username"
value="#{user.username}"
required="true"
readonly="#{readonly or mode == 'edit'}"
placeholder="jdupont"
styleClass="w-full">
<f:validateLength minimum="3" maximum="100" />
<f:validateRegex pattern="^[a-zA-Z0-9._-]+$" />
</p:inputText>
<!-- Email -->
<p:outputLabel for="email" value="Email *" />
<p:inputText id="email"
value="#{user.email}"
required="true"
readonly="#{readonly}"
placeholder="jean.dupont@lions.dev"
styleClass="w-full">
<f:validateLength minimum="5" maximum="255" />
</p:inputText>
<!-- Prénom -->
<p:outputLabel for="prenom" value="Prénom *" />
<p:inputText id="prenom"
value="#{user.prenom}"
required="true"
readonly="#{readonly}"
placeholder="Jean"
styleClass="w-full">
<f:validateLength minimum="2" maximum="100" />
</p:inputText>
<!-- Nom -->
<p:outputLabel for="nom" value="Nom *" />
<p:inputText id="nom"
value="#{user.nom}"
required="true"
readonly="#{readonly}"
placeholder="Dupont"
styleClass="w-full">
<f:validateLength minimum="2" maximum="100" />
</p:inputText>
<!-- Téléphone -->
<p:outputLabel for="telephone" value="Téléphone" />
<p:inputText id="telephone"
value="#{user.telephone}"
readonly="#{readonly}"
placeholder="+225 07 12 34 56 78"
styleClass="w-full" />
<!-- Organisation -->
<p:outputLabel for="organisation" value="Organisation" />
<p:inputText id="organisation"
value="#{user.organisation}"
readonly="#{readonly}"
placeholder="Lions Dev"
styleClass="w-full" />
<!-- Département -->
<p:outputLabel for="departement" value="Département" />
<p:inputText id="departement"
value="#{user.departement}"
readonly="#{readonly}"
placeholder="IT"
styleClass="w-full" />
<!-- Fonction -->
<p:outputLabel for="fonction" value="Fonction" />
<p:inputText id="fonction"
value="#{user.fonction}"
readonly="#{readonly}"
placeholder="Développeur Senior"
styleClass="w-full" />
<!-- Ville -->
<p:outputLabel for="ville" value="Ville" />
<p:inputText id="ville"
value="#{user.ville}"
readonly="#{readonly}"
placeholder="Abidjan"
styleClass="w-full" />
<!-- Pays -->
<p:outputLabel for="pays" value="Pays" />
<p:inputText id="pays"
value="#{user.pays}"
readonly="#{readonly}"
placeholder="Côte d'Ivoire"
styleClass="w-full" />
<!-- Statut -->
<p:outputLabel for="statut" value="Statut" />
<p:selectOneMenu id="statut"
value="#{user.statut}"
readonly="#{readonly}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<f:selectItems value="#{userBean.statutOptions}" />
</p:selectOneMenu>
<!-- Enabled -->
<p:outputLabel for="enabled" value="Compte activé" />
<p:selectBooleanCheckbox id="enabled"
value="#{user.enabled}"
readonly="#{readonly}" />
<!-- Email vérifié -->
<p:outputLabel for="emailVerified" value="Email vérifié" />
<p:selectBooleanCheckbox id="emailVerified"
value="#{user.emailVerified}"
readonly="#{readonly}" />
<!-- Realm (si affiché) -->
<c:if test="#{showRealmSelector}">
<p:outputLabel for="realmName" value="Realm *" />
<p:selectOneMenu id="realmName"
value="#{user.realmName}"
required="#{showRealmSelector}"
readonly="#{readonly}"
styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" />
<f:selectItems value="#{userBean.availableRealms}" />
</p:selectOneMenu>
</c:if>
</p:panelGrid>
<!-- Champs mot de passe (si affichés) -->
<c:if test="#{showPasswordFields and mode == 'create'}">
<p:separator />
<h3>Mot de passe</h3>
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-6">
<p:outputLabel for="password" value="Mot de passe *" />
<p:password id="password"
value="#{userBean.password}"
required="true"
feedback="true"
placeholder="Minimum 8 caractères"
styleClass="w-full">
<f:validateLength minimum="8" maximum="100" />
</p:password>
<p:outputLabel for="passwordConfirm" value="Confirmer le mot de passe *" />
<p:password id="passwordConfirm"
value="#{userBean.passwordConfirm}"
required="true"
placeholder="Répétez le mot de passe"
styleClass="w-full" />
</p:panelGrid>
</c:if>
<!-- Boutons d'action -->
<f:facet name="footer">
<div class="flex gap-2 justify-content-end">
<c:if test="#{not readonly}">
<c:choose>
<!-- Si hasSubmitAction est explicitement défini à true, utiliser action -->
<c:when test="#{hasSubmitAction == true}">
<p:commandButton
value="#{mode == 'create' ? 'Créer' : 'Modifier'}"
icon="pi pi-check"
styleClass="p-button-success"
action="#{submitAction}"
update="#{not empty update ? update : '@form'}"
process="@form" />
</c:when>
<!-- Si submitOutcome est fourni, utiliser outcome -->
<c:when test="#{not empty submitOutcome}">
<p:commandButton
value="#{mode == 'create' ? 'Créer' : 'Modifier'}"
icon="pi pi-check"
styleClass="p-button-success"
outcome="#{submitOutcome}"
update="#{not empty update ? update : '@form'}"
process="@form" />
</c:when>
<!-- Sinon, essayer d'utiliser submitAction si fourni -->
<c:otherwise>
<p:commandButton
value="#{mode == 'create' ? 'Créer' : 'Modifier'}"
icon="pi pi-check"
styleClass="p-button-success"
action="#{submitAction}"
update="#{not empty update ? update : '@form'}"
process="@form" />
</c:otherwise>
</c:choose>
</c:if>
<p:commandButton
value="Annuler"
icon="pi pi-times"
styleClass="p-button-secondary"
outcome="#{not empty cancelOutcome ? cancelOutcome : '/pages/user-manager/users/list'}"
immediate="true" />
</div>
</f:facet>
</p:panel>
</h:form>
<c:choose>
<c:when test="#{useParentForm}">
<!-- Utiliser le formulaire parent - pas de formulaire ici -->
<ui:include src="/templates/components/user-management/user-form-content.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="mode" value="#{mode}" />
<ui:param name="showRealmSelector" value="#{showRealmSelector}" />
<ui:param name="showPasswordFields" value="#{showPasswordFields}" />
<ui:param name="readonly" value="#{readonly}" />
<ui:param name="submitAction" value="#{submitAction}" />
<ui:param name="submitOutcome" value="#{submitOutcome}" />
<ui:param name="update" value="#{update}" />
<ui:param name="hasSubmitAction" value="#{hasSubmitAction}" />
<ui:param name="cancelOutcome" value="#{cancelOutcome}" />
</ui:include>
</c:when>
<c:otherwise>
<!-- Créer son propre formulaire -->
<h:form id="#{formId}">
<ui:include src="/templates/components/user-management/user-form-content.xhtml">
<ui:param name="user" value="#{user}" />
<ui:param name="mode" value="#{mode}" />
<ui:param name="showRealmSelector" value="#{showRealmSelector}" />
<ui:param name="showPasswordFields" value="#{showPasswordFields}" />
<ui:param name="readonly" value="#{readonly}" />
<ui:param name="submitAction" value="#{submitAction}" />
<ui:param name="submitOutcome" value="#{submitOutcome}" />
<ui:param name="update" value="#{update}" />
<ui:param name="hasSubmitAction" value="#{hasSubmitAction}" />
<ui:param name="cancelOutcome" value="#{cancelOutcome}" />
</ui:include>
</h:form>
</c:otherwise>
</c:choose>
</ui:composition>

View File

@@ -1,105 +1,55 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Badge de Rôle Utilisateur (WOU/DRY Pattern)
Composant réutilisable: Badge de Rôle Utilisateur - Écosystème LionsDev
Auteur: Lions User Manager
Version: 1.0.0
Description: Affiche un badge pour un rôle utilisateur avec icône et couleur
Paramètres:
- roleName: String (requis) - Nom du rôle
- roleType: String (optionnel) - Type de rôle: "REALM_ROLE", "CLIENT_ROLE", "COMPOSITE_ROLE"
- severity: String (optionnel) - Severity PrimeFaces: "success", "info", "warning", "danger" (défaut: "info")
- showIcon: Boolean (défaut: true) - Afficher l'icône
- icon: String (optionnel) - Classe d'icône PrimeIcons (défaut: "pi-shield")
- size: String (optionnel) - Taille: "small", "normal", "large" (défaut: "normal")
- clickable: Boolean (défaut: false) - Rendre le badge cliquable
- clickAction: String (optionnel) - Action au clic
- styleClass: String (optionnel) - Classes CSS supplémentaires
Exemples d'utilisation:
1. Badge simple:
<ui:include src="/templates/components/user-management/user-role-badge.xhtml">
<ui:param name="roleName" value="ADMIN" />
</ui:include>
2. Badge avec type:
<ui:include src="/templates/components/user-management/user-role-badge.xhtml">
<ui:param name="roleName" value="USER" />
<ui:param name="roleType" value="REALM_ROLE" />
<ui:param name="severity" value="success" />
</ui:include>
3. Badge cliquable:
<ui:include src="/templates/components/user-management/user-role-badge.xhtml">
<ui:param name="roleName" value="MODERATOR" />
<ui:param name="clickable" value="true" />
<ui:param name="clickAction" value="#{roleBean.viewRole(roleName)}" />
</ui:include>
Version: 2.0.0
Description: Badge typé pour afficher un rôle avec icône et couleur sémantique.
-->
<c:set var="showIcon" value="#{empty showIcon ? true : showIcon}" />
<c:set var="size" value="#{empty size ? 'normal' : size}" />
<c:set var="clickable" value="#{empty clickable ? false : clickable}" />
<c:set var="severity" value="#{empty severity ? 'info' : severity}" />
<c:set var="icon" value="#{empty icon ? 'pi-shield' : icon}" />
<!-- Déterminer la severity selon le type de rôle -->
<!-- Logique de couleur/icône centralisée -->
<c:choose>
<c:when test="#{roleType == 'REALM_ROLE'}">
<c:set var="severity" value="success" />
<c:set var="icon" value="pi-check-circle" />
</c:when>
<c:when test="#{roleType == 'CLIENT_ROLE'}">
<c:set var="severity" value="info" />
<c:set var="icon" value="pi-desktop" />
</c:when>
<c:when test="#{roleType == 'COMPOSITE_ROLE'}">
<c:set var="severity" value="warning" />
</c:when>
</c:choose>
<!-- Déterminer la taille -->
<c:choose>
<c:when test="#{size == 'small'}">
<c:set var="tagStyleClass" value="text-xs" />
</c:when>
<c:when test="#{size == 'large'}">
<c:set var="tagStyleClass" value="text-base" />
<c:set var="icon" value="pi-sitemap" />
</c:when>
<c:otherwise>
<c:set var="tagStyleClass" value="text-sm" />
<c:set var="severity" value="#{empty severity ? 'info' : severity}" />
<c:set var="icon" value="#{empty icon ? 'pi-shield' : icon}" />
</c:otherwise>
</c:choose>
<c:choose>
<!-- Badge cliquable -->
<c:when test="#{clickable and not empty clickAction}">
<p:commandLink
styleClass="role-badge-link"
action="#{clickAction}">
<p:tag
value="#{roleName}"
severity="#{severity}"
icon="#{showIcon ? icon : ''}"
styleClass="#{tagStyleClass} #{styleClass}" />
</p:commandLink>
</c:when>
<!-- Badge simple -->
<c:otherwise>
<p:tag
value="#{roleName}"
severity="#{severity}"
icon="#{showIcon ? icon : ''}"
styleClass="#{tagStyleClass} #{styleClass}" />
</c:otherwise>
</c:choose>
</ui:composition>
<!-- Override manuel de l'icône si fourni -->
<c:if test="#{not empty paramIcon}">
<c:set var="icon" value="#{paramIcon}" />
</c:if>
<c:set var="tagClass"
value="#{size == 'small' ? 'text-xs' : (size == 'large' ? 'text-lg' : 'text-sm')} #{styleClass}" />
<p:tag value="#{roleName}" severity="#{severity}" icon="#{showIcon ? 'pi '.concat(icon) : ''}"
styleClass="#{tagClass}">
<c:if test="#{clickable}">
<p:commandLink action="#{clickAction}" styleClass="ml-2">
<i class="pi pi-external-link text-xs text-white"></i>
</p:commandLink>
</c:if>
</p:tag>
</ui:composition>

View File

@@ -0,0 +1,81 @@
<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">
<!--
Composant réutilisable: Gestion des rôles utilisateur
Paramètres:
- user: UserDTO (requis)
- realm: String (requis)
- roleBean: RoleGestionBean (requis)
-->
<c:set var="userRoles" value="#{user.realmRoles}" />
<c:set var="availableRoles" value="#{roleBean.realmRoles}" />
<p:panel header="Gestion des Rôles" styleClass="w-full">
<p:messages id="roleMessages" showDetail="true" closable="true" styleClass="mb-3" />
<div class="grid">
<div class="col-12 md:col-6">
<!-- Rôles assignés -->
<h4 class="text-primary mt-0 mb-3">
<i class="pi pi-check-circle mr-2"></i>Rôles Assignés
</h4>
<p:dataTable value="#{userRoles}" var="roleName" emptyMessage="Aucun rôle assigné"
styleClass="border-1 surface-border border-round" size="small">
<p:column headerText="Nom du rôle">
<span class="font-medium">#{roleName}</span>
</p:column>
<p:column style="width: 4rem; text-align: center">
<p:commandButton icon="pi pi-trash" styleClass="p-button-rounded p-button-danger p-button-text"
action="#{roleBean.revokeRoleFromUser(user.id, roleName)}" process="@this" update="@form"
title="Révoquer ce rôle">
<p:confirm header="Confirmation" message="Révoquer le rôle '#{roleName}' ?"
icon="pi pi-exclamation-triangle" />
</p:commandButton>
</p:column>
</p:dataTable>
</div>
<div class="col-12 md:col-6">
<!-- Ajouter un rôle -->
<h4 class="text-primary mt-0 mb-3">
<i class="pi pi-plus-circle mr-2"></i>Ajouter un Rôle
</h4>
<div class="card bg-bluegray-50 border-none p-4">
<div class="field">
<p:outputLabel for="rolesPicklist" value="Rôles disponibles"
styleClass="font-medium mb-2 block" />
<p:selectOneMenu id="rolesPicklist" value="#{roleBean.selectedRoleName}" filter="true"
filterMatchMode="contains" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner un rôle..." itemValue="#{null}"
noSelectionOption="true" />
<f:selectItems value="#{availableRoles}" var="role" itemLabel="#{role.name}"
itemValue="#{role.name}" itemDisabled="#{userRoles.contains(role.name)}" />
</p:selectOneMenu>
</div>
<p:commandButton value="Attribuer le rôle" icon="pi pi-check"
actionListener="#{roleBean.assignRoleToUser(user.id, roleBean.selectedRoleName)}" update="@form"
process="@parent" styleClass="w-full mt-2" />
</div>
<div class="mt-4 surface-card p-3 border-round shadow-1">
<div class="flex align-items-center mb-2">
<i class="pi pi-info-circle text-blue-500 mr-2 text-xl"></i>
<span class="font-bold text-900">Information</span>
</div>
<p class="text-sm text-700 m-0 line-height-3">
Les rôles définissent les permissions de l'utilisateur dans l'application.
Certains rôles peuvent donner accès à des fonctionnalités administratives sensibles.
</p>
</div>
</div>
</div>
</p:panel>
</ui:composition>

View File

@@ -1,181 +1,184 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<!--
Composant réutilisable: Contenu Barre de Recherche Utilisateur - Écosystème LionsDev
Auteur: Lions User Manager
Version: 2.0.0
Description: Barre de recherche avec filtres rapides et options avancées.
Utilisé dans user-search-bar.xhtml, ne pas inclure directement.
Paramètres hérités de user-search-bar.xhtml:
- searchCriteria, searchAction, update
- showRealmFilter, showStatusFilter, showRoleFilter, showAdvanced
-->
<p:panel styleClass="w-full mb-3">
<f:facet name="header">
<div class="flex align-items-center justify-content-between">
<span>Recherche d'utilisateurs</span>
<p:commandButton
icon="pi pi-filter"
styleClass="p-button-text p-button-sm"
onclick="PF('advancedSearchDialog').toggle()"
rendered="#{showAdvanced}"
title="Options avancées" />
<span class="flex align-items-center gap-2">
<i class="pi pi-search"></i>
Recherche d'utilisateurs
</span>
<c:if test="#{showAdvanced}">
<p:commandButton icon="pi pi-filter" styleClass="p-button-text p-button-sm p-button-rounded"
onclick="PF('advancedSearchDialog').toggle()" title="Options avancées" type="button" />
</c:if>
</div>
</f:facet>
<!-- Recherche rapide -->
<div class="flex gap-2 align-items-end">
<div class="flex-1">
<p:outputLabel for="searchText" value="Rechercher" />
<p:inputText id="searchText"
value="#{searchCriteria.searchTerm}"
placeholder="Nom, prénom, email, username..."
styleClass="w-full">
<p:ajax event="keyup"
delay="500"
listener="#{searchAction}"
update="#{update}" />
</p:inputText>
<div class="flex gap-2 align-items-end flex-wrap">
<div class="flex-1" style="min-width: 250px;">
<p:outputLabel for="searchText" value="Rechercher" styleClass="font-medium" />
<div class="p-inputgroup mt-1">
<span class="p-inputgroup-addon">
<i class="pi pi-search"></i>
</span>
<p:inputText id="searchText" value="#{searchCriteria.searchTerm}"
placeholder="Nom, prénom, email, username..." styleClass="w-full">
<p:ajax event="keyup" delay="500" listener="#{searchAction}" update="#{update}" />
</p:inputText>
</div>
</div>
<!-- Filtre Realm -->
<c:if test="#{showRealmFilter}">
<div style="width: 200px;">
<p:outputLabel for="realmFilter" value="Realm" />
<p:selectOneMenu id="realmFilter"
value="#{searchCriteria.realmName}"
styleClass="w-full">
<div style="min-width: 180px;">
<p:outputLabel for="realmFilter" value="Realm" styleClass="font-medium" />
<p:selectOneMenu id="realmFilter" value="#{searchCriteria.realmName}" styleClass="w-full mt-1">
<f:selectItem itemLabel="Tous les realms" itemValue="" />
<f:selectItems value="#{userBean.availableRealms}" />
<p:ajax event="change" listener="#{searchAction}" update="#{update}" />
</p:selectOneMenu>
</div>
</c:if>
<!-- Filtre Statut -->
<c:if test="#{showStatusFilter}">
<div style="width: 180px;">
<p:outputLabel for="statusFilter" value="Statut" />
<p:selectOneMenu id="statusFilter"
value="#{searchCriteria.statut}"
styleClass="w-full">
<div style="min-width: 150px;">
<p:outputLabel for="statusFilter" value="Statut" styleClass="font-medium" />
<p:selectOneMenu id="statusFilter" value="#{searchCriteria.statut}" styleClass="w-full mt-1">
<f:selectItem itemLabel="Tous les statuts" itemValue="" />
<f:selectItems value="#{userBean.statutOptions}" />
<p:ajax event="change" listener="#{searchAction}" update="#{update}" />
</p:selectOneMenu>
</div>
</c:if>
<!-- Bouton Rechercher -->
<div>
<p:commandButton
value="Rechercher"
icon="pi pi-search"
styleClass="p-button-primary"
action="#{searchAction}"
update="#{update}"
process="@form" />
</div>
<!-- Bouton Réinitialiser -->
<div>
<p:commandButton
value="Réinitialiser"
icon="pi pi-refresh"
styleClass="p-button-secondary"
action="#{userBean.resetSearch}"
update="#{update}"
process="@form" />
<!-- Boutons -->
<div class="flex gap-2 align-items-end">
<p:commandButton value="Rechercher" icon="pi pi-search" styleClass="p-button-primary"
action="#{searchAction}" update="#{update}" process="@form" />
<p:commandButton icon="pi pi-refresh" title="Réinitialiser"
styleClass="p-button-secondary p-button-outlined p-button-rounded" action="#{userBean.resetSearch}"
update="#{update}" process="@form" />
</div>
</div>
</p:panel>
<!-- Options avancées (Dialog) -->
<c:if test="#{showAdvanced}">
<p:dialog id="advancedSearchDialog"
header="Options de recherche avancées"
widgetVar="advancedSearchDialog"
modal="true"
resizable="false"
styleClass="w-full md:w-6">
<p:panelGrid columns="2" styleClass="w-full" columnClasses="col-12 md:col-6">
<p:dialog id="advancedSearchDialog" header="Options de recherche avancées" widgetVar="advancedSearchDialog"
modal="true" resizable="false" draggable="true" responsive="true" styleClass="w-11 md:w-6"
closeOnEscape="true">
<div class="grid">
<!-- Email -->
<p:outputLabel for="emailFilter" value="Email" />
<p:inputText id="emailFilter"
value="#{searchCriteria.email}"
placeholder="email@example.com"
styleClass="w-full" />
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="emailFilter" value="Email" styleClass="font-medium" />
<p:inputText id="emailFilter" value="#{searchCriteria.email}" placeholder="email@example.com"
styleClass="w-full" />
</div>
</div>
<!-- Téléphone -->
<p:outputLabel for="phoneFilter" value="Téléphone" />
<p:inputText id="phoneFilter"
value="#{searchCriteria.telephone}"
placeholder="+225 07 12 34 56 78"
styleClass="w-full" />
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="phoneFilter" value="Téléphone" styleClass="font-medium" />
<p:inputText id="phoneFilter" value="#{searchCriteria.telephone}"
placeholder="+225 07 12 34 56 78" styleClass="w-full" />
</div>
</div>
<!-- Organisation -->
<p:outputLabel for="orgFilter" value="Organisation" />
<p:inputText id="orgFilter"
value="#{searchCriteria.organisation}"
placeholder="Nom de l'organisation"
styleClass="w-full" />
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="orgFilter" value="Organisation" styleClass="font-medium" />
<p:inputText id="orgFilter" value="#{searchCriteria.organisation}"
placeholder="Nom de l'organisation" styleClass="w-full" />
</div>
</div>
<!-- Ville -->
<p:outputLabel for="cityFilter" value="Ville" />
<p:inputText id="cityFilter"
value="#{searchCriteria.ville}"
placeholder="Nom de la ville"
styleClass="w-full" />
<!-- Rôle (si affiché) -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="cityFilter" value="Ville" styleClass="font-medium" />
<p:inputText id="cityFilter" value="#{searchCriteria.ville}" placeholder="Nom de la ville"
styleClass="w-full" />
</div>
</div>
<!-- Rôle -->
<c:if test="#{showRoleFilter}">
<p:outputLabel for="roleFilter" value="Rôle Realm" />
<p:selectManyMenu id="roleFilter"
value="#{searchCriteria.realmRoles}"
styleClass="w-full">
<f:selectItem itemLabel="Tous les rôles" itemValue="" />
<f:selectItems value="#{userBean.availableRoles}" />
</p:selectManyMenu>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="roleFilter" value="Rôle Realm" styleClass="font-medium" />
<p:selectCheckboxMenu id="roleFilter" value="#{searchCriteria.realmRoles}"
label="Sélectionner des rôles" multiple="true" filter="true" filterMatchMode="contains"
styleClass="w-full">
<f:selectItems value="#{userBean.availableRoles}" />
</p:selectCheckboxMenu>
</div>
</div>
</c:if>
<!-- Date de création (début) -->
<p:outputLabel for="dateDebut" value="Date de création (début)" />
<p:calendar id="dateDebut"
value="#{searchCriteria.dateCreationMin}"
pattern="dd/MM/yyyy"
styleClass="w-full" />
<!-- Date de création (fin) -->
<p:outputLabel for="dateFin" value="Date de création (fin)" />
<p:calendar id="dateFin"
value="#{searchCriteria.dateCreationMax}"
pattern="dd/MM/yyyy"
styleClass="w-full" />
<!-- Enabled -->
<p:outputLabel for="enabledFilter" value="Compte activé" />
<p:selectOneMenu id="enabledFilter"
value="#{searchCriteria.enabled}"
styleClass="w-full">
<f:selectItem itemLabel="Tous" itemValue="" />
<f:selectItem itemLabel="Activé" itemValue="true" />
<f:selectItem itemLabel="Désactivé" itemValue="false" />
</p:selectOneMenu>
</p:panelGrid>
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="enabledFilter" value="Compte activé" styleClass="font-medium" />
<p:selectOneMenu id="enabledFilter" value="#{searchCriteria.enabled}" styleClass="w-full">
<f:selectItem itemLabel="Tous" itemValue="" />
<f:selectItem itemLabel="Activé" itemValue="true" />
<f:selectItem itemLabel="Désactivé" itemValue="false" />
</p:selectOneMenu>
</div>
</div>
<!-- Date de création (début) -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="dateDebut" value="Date de création (début)" styleClass="font-medium" />
<p:datePicker id="dateDebut" value="#{searchCriteria.dateCreationMin}" pattern="dd/MM/yyyy"
showIcon="true" styleClass="w-full" />
</div>
</div>
<!-- Date de création (fin) -->
<div class="col-12 md:col-6">
<div class="field">
<p:outputLabel for="dateFin" value="Date de création (fin)" styleClass="font-medium" />
<p:datePicker id="dateFin" value="#{searchCriteria.dateCreationMax}" pattern="dd/MM/yyyy"
showIcon="true" styleClass="w-full" />
</div>
</div>
</div>
<f:facet name="footer">
<div class="flex gap-2 justify-content-end">
<p:commandButton
value="Appliquer"
icon="pi pi-check"
styleClass="p-button-primary"
action="#{searchAction}"
update="#{update}"
onclick="PF('advancedSearchDialog').hide()"
<p:commandButton value="Appliquer les filtres" icon="pi pi-check" styleClass="p-button-primary"
action="#{searchAction}" update="#{update}" oncomplete="PF('advancedSearchDialog').hide()"
process="@form" />
<p:commandButton
value="Annuler"
icon="pi pi-times"
styleClass="p-button-secondary"
onclick="PF('advancedSearchDialog').hide()" />
<p:commandButton value="Annuler" icon="pi pi-times"
styleClass="p-button-secondary p-button-outlined" onclick="PF('advancedSearchDialog').hide()"
type="button" />
</div>
</f:facet>
</p:dialog>
</c:if>
</ui:composition>
</ui:composition>

View File

@@ -1,53 +1,60 @@
<!DOCTYPE html>
<html 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"
lang="fr">
<html 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" lang="fr">
<h:head>
<f:facet name="first">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<meta name="mobile-web-app-capable" content="yes" />
<link rel="icon" href="#{request.contextPath}/resources/freya-layout/images/favicon.ico" type="image/x-icon"></link>
</f:facet>
<title><ui:insert name="title">Lions User Manager</ui:insert></title>
<h:outputScript name="js/layout.js" library="freya-layout" />
<h:outputScript name="js/prism.js" library="freya-layout"/>
<ui:insert name="head"/>
</h:head>
<h:head>
<f:facet name="first">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="icon" href="#{request.contextPath}/resources/freya-layout/images/favicon.ico" type="image/x-icon">
</link>
</f:facet>
<title>
<ui:insert name="title">Lions User Manager</ui:insert>
</title>
<h:outputScript name="js/layout.js" library="freya-layout" />
<h:outputScript name="js/prism.js" library="freya-layout" />
<ui:insert name="head" />
</h:head>
<h:body styleClass="#{guestPreferences.inputStyleClass}">
<div class="layout-wrapper layout-topbar-#{guestPreferences.topbarTheme} layout-menu-#{guestPreferences.menuTheme} #{guestPreferences.menuMode}" >
<ui:include src="/templates/components/layout/topbar.xhtml"/>
<div class="layout-main">
<div class="layout-content">
<p:messages id="messages" showDetail="true" closable="true" />
<ui:insert name="content"/>
</div>
<ui:include src="/templates/components/layout/footer.xhtml"/>
<h:body styleClass="#{guestPreferences.inputStyleClass}">
<div
class="layout-wrapper layout-topbar-#{guestPreferences.topbarTheme} layout-menu-#{guestPreferences.menuTheme} #{guestPreferences.menuMode}">
<ui:include src="/templates/components/layout/topbar.xhtml" />
<div class="layout-main">
<div class="layout-content">
<p:messages id="messages" showDetail="true" closable="true" />
<ui:insert name="content" />
</div>
<p:ajaxStatus style="width:32px;height:32px;position:fixed;right:7px;bottom:7px">
<f:facet name="start">
<i class="pi pi-spin pi-spinner ajax-loader" aria-hidden="true"/>
</f:facet>
<f:facet name="complete">
<h:outputText value="" />
</f:facet>
</p:ajaxStatus>
<div class="layout-mask modal-in"></div>
<ui:include src="/templates/components/layout/footer.xhtml" />
</div>
<h:outputStylesheet name="css/primeicons.css" library="freya-layout" />
<h:outputStylesheet name="css/primeflex.min.css" library="freya-layout" />
<h:outputStylesheet name="css/layout-#{guestPreferences.layout}.css" library="freya-layout" />
<h:outputStylesheet name="primefaces-freya-#{guestPreferences.componentTheme}/theme.css" />
</h:body>
</html>
<p:ajaxStatus style="width:32px;height:32px;position:fixed;right:7px;bottom:7px">
<f:facet name="start">
<i class="pi pi-spin pi-spinner ajax-loader" aria-hidden="true" />
</f:facet>
<f:facet name="complete">
<h:outputText value="" />
</f:facet>
</p:ajaxStatus>
<p:confirmDialog global="true" showEffect="fade" hideEffect="fade" responsive="true" width="350">
<p:commandButton value="Non" type="button" styleClass="ui-confirmdialog-no ui-button-flat" />
<p:commandButton value="Oui" type="button" styleClass="ui-confirmdialog-yes" />
</p:confirmDialog>
<div class="layout-mask modal-in"></div>
</div>
<h:outputStylesheet name="css/primeicons.css" library="freya-layout" />
<h:outputStylesheet name="css/primeflex.min.css" library="freya-layout" />
<h:outputStylesheet name="css/layout-#{guestPreferences.layout}.css" library="freya-layout" />
<h:outputStylesheet name="primefaces-freya-#{guestPreferences.componentTheme}/theme.css" />
<h:outputStylesheet name="css/custom-topbar.css" />
</h:body>
</html>

View File

@@ -1,53 +1,45 @@
# ============================================================================
# Configuration Développement - Lions User Manager Client
# Lions User Manager Client - Configuration DEV
# ============================================================================
# NOTE: La configuration OIDC principale est dans application.properties
# avec le préfixe %dev. Ce fichier contient UNIQUEMENT les surcharges
# spécifiques au développement qui ne sont pas déjà dans application.properties
# Ce fichier contient UNIQUEMENT les propriétés spécifiques au DÉVELOPPEMENT
# Il surcharge application.properties
# ============================================================================
# ============================================
# Logging - Surcharges DEV
# HTTP Configuration DEV
# ============================================
# Logging plus détaillé en dev
quarkus.log.console.level=DEBUG
quarkus.log.category."dev.lions.user.manager".level=TRACE
# Debug OIDC pour voir quelle valeur est chargée
quarkus.log.category."io.quarkus.oidc".level=DEBUG
quarkus.log.category."io.quarkus.oidc.runtime".level=TRACE
quarkus.http.port=8082
# ============================================
# MyFaces - Surcharges DEV
# OIDC Configuration DEV (Keycloak local)
# ============================================
quarkus.myfaces.project-stage=Development
quarkus.myfaces.check-id-production-mode=false
quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager
quarkus.oidc.client-id=lions-user-manager-client
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET:NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO}
quarkus.oidc.tls.verification=none
# ============================================
# Backend - Surcharges DEV
# 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
# ============================================
# Backend local (le serveur tourne sur le port 8081)
lions.user.manager.backend.url=http://localhost:8081
quarkus.rest-client."lions-user-manager-api".url=http://localhost:8081
quarkus.rest-client."user-api".url=http://localhost:8081
# ============================================
# CORS - Surcharges DEV
# Logging DEV (verbeux)
# ============================================
# CORS permissif en dev (surcharge de application.properties)
quarkus.http.cors.origins=*
quarkus.log.level=INFO
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
# ============================================
# OIDC - Surcharges DEV (si nécessaire)
# Dev Services DEV
# ============================================
# NOTE: La configuration OIDC principale est dans application.properties
# avec le préfixe %dev. (lignes 73-81)
# Ne définir ici QUE les propriétés qui ne sont pas déjà dans application.properties
#
# State Secret pour PKCE (OBLIGATOIRE quand pkce-required=true)
# Ce secret est utilisé pour encrypter le PKCE code verifier dans le state cookie
# Minimum 16 caractères requis, recommandé 32 caractères
# IMPORTANT: Ne PAS définir pkce-secret quand state-secret est défini (conflit Quarkus)
quarkus.oidc.authentication.state-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO
# Surcharge de encryption-secret (64 caractères pour garantir)
# Cette propriété est aussi définie dans application.properties avec %dev.,
# mais on la redéfinit ici pour garantir qu'elle soit chargée
quarkus.oidc.token-state-manager.encryption-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO
quarkus.devservices.enabled=false

View File

@@ -1,18 +1,41 @@
# ============================================================================
# Configuration Production - Lions User Manager Client
# ============================================================================
# NOTE: La configuration OIDC principale est dans application.properties
# avec le préfixe %prod. (lignes 86-94)
#
# Ce fichier peut être utilisé pour des surcharges spécifiques à la production
# qui ne sont pas déjà définies dans application.properties avec %prod.
#
# Exemple d'utilisation :
# - Surcharges de logging spécifiques à la production
# - Configurations spécifiques à un environnement de production particulier
# - Variables d'environnement qui doivent être surchargées
# ============================================================================
# Exemple (décommenter si nécessaire) :
# quarkus.log.console.level=WARN
# quarkus.log.category."dev.lions.user.manager".level=INFO
# ============================================================================
# Lions User Manager Client - Configuration PROD
# ============================================================================
# Ce fichier contient UNIQUEMENT les propriétés spécifiques à la PRODUCTION
# Il surcharge application.properties
# ============================================================================
# ============================================
# HTTP Configuration PROD
# ============================================
quarkus.http.port=8080
# Respecter X-Forwarded-Proto de l'ingress nginx (SSL termination K8s)
quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.allow-x-forwarded=true
# ============================================
# OIDC Configuration PROD (Keycloak Production)
# ============================================
quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/lions-user-manager}
quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:lions-user-manager-client}
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET:oGCivOdgbNHroNsHS1MRBZJXX8VpRGk3}
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=lax
quarkus.oidc.authentication.pkce-required=true
quarkus.oidc.token-state-manager.encryption-secret=${OIDC_ENCRYPTION_SECRET:gbztZB3CYpou0vFL2LqOWJQdXnvwVQkhjrHpsZHOJPI=}
# ============================================
# Backend REST Client PROD
# ============================================
lions.user.manager.backend.url=${LIONS_USER_MANAGER_BACKEND_URL:https://api.lions.dev/lions-user-manager}
quarkus.rest-client."lions-user-manager-api".url=${LIONS_USER_MANAGER_BACKEND_URL:https://api.lions.dev/lions-user-manager}
quarkus.rest-client."user-api".url=${LIONS_USER_MANAGER_BACKEND_URL:https://api.lions.dev/lions-user-manager}
# ============================================
# Logging PROD (minimal)
# ============================================
quarkus.log.level=INFO
quarkus.log.console.level=WARN
quarkus.log.category."dev.lions.user.manager".level=INFO
quarkus.log.category."io.quarkus.oidc".level=INFO

View File

@@ -1,128 +1,71 @@
# Configuration Lions User Manager Client
# ============================================================================
# Lions User Manager Client - Configuration COMMUNE (tous environnements)
# ============================================================================
# Ce fichier contient UNIQUEMENT les propriétés communes à dev et prod.
# Les configs OIDC, HTTP port, etc. vont dans application-dev/prod.properties
# ============================================================================
# ============================================
# Application Info (COMMUNE)
# ============================================
quarkus.application.name=lions-user-manager-client
quarkus.application.version=1.0.0
# Configuration HTTP
quarkus.http.port=8080
quarkus.http.host=0.0.0.0
quarkus.http.root-path=/
quarkus.http.so-reuse-port=true
# Configuration Session HTTP
quarkus.http.session-timeout=60m
quarkus.http.session-cookie-same-site=lax
quarkus.http.session-cookie-http-only=true
quarkus.http.session-cookie-secure=false
# Configuration logging
quarkus.log.console.enable=true
quarkus.log.console.level=INFO
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n
quarkus.log.category."dev.lions.user.manager".level=DEBUG
# MyFaces Configuration
quarkus.myfaces.project-stage=Development
quarkus.myfaces.state-saving-method=server
quarkus.myfaces.number-of-views-in-session=50
quarkus.myfaces.number-of-sequential-views-in-session=10
quarkus.myfaces.serialize-state-in-session=false
quarkus.myfaces.client-view-state-timeout=3600000
quarkus.myfaces.view-expired-exception-handler-redirect-page=/
quarkus.myfaces.check-id-production-mode=false
quarkus.myfaces.strict-xhtml-links=false
quarkus.myfaces.refresh-transient-build-on-pss=true
quarkus.myfaces.resource-max-time-expires=604800000
quarkus.myfaces.resource-buffer-size=2048
# PrimeFaces Configuration
primefaces.THEME=freya
primefaces.FONT_AWESOME=true
primefaces.CLIENT_SIDE_VALIDATION=true
primefaces.MOVE_SCRIPTS_TO_BOTTOM=true
primefaces.CSP=false
primefaces.UPLOADER=commons
primefaces.AUTO_UPDATE=false
primefaces.CACHE_PROVIDER=org.primefaces.cache.DefaultCacheProvider
# Configuration Backend Lions User Manager
lions.user.manager.backend.url=${LIONS_USER_MANAGER_BACKEND_URL:http://localhost:8081}
# Configuration REST Client
quarkus.rest-client."lions-user-manager-api".url=${lions.user.manager.backend.url}
# ============================================
# REST Client Configuration (COMMUNE)
# ============================================
# Config pour les clients service/ (UserServiceClient, RoleServiceClient, AuditServiceClient, etc.)
quarkus.rest-client."lions-user-manager-api".scope=jakarta.inject.Singleton
quarkus.rest-client."lions-user-manager-api".connect-timeout=5000
quarkus.rest-client."lions-user-manager-api".read-timeout=30000
# Config pour les clients api/ (AuditRestClient, HealthRestClient, RoleRestClient, etc.)
quarkus.rest-client."user-api".scope=jakarta.inject.Singleton
quarkus.rest-client."user-api".connect-timeout=5000
quarkus.rest-client."user-api".read-timeout=30000
# ============================================
# OIDC Configuration - Base (All Environments)
# OIDC Configuration (COMMUNE)
# ============================================
quarkus.oidc.enabled=true
quarkus.oidc.roles.role-claim-path=realm_access/roles
quarkus.oidc.roles.source=accesstoken
quarkus.oidc.application-type=web-app
quarkus.oidc.authentication.redirect-path=/auth/callback
quarkus.oidc.authentication.redirect-path=/
quarkus.oidc.authentication.restore-path-after-redirect=true
quarkus.oidc.authentication.scopes=openid,profile,email,roles
quarkus.oidc.authentication.cookie-same-site=lax
quarkus.oidc.authentication.java-script-auto-redirect=false
quarkus.oidc.discovery-enabled=true
quarkus.oidc.verify-access-token=true
quarkus.security.auth.enabled=true
quarkus.oidc.authentication.pkce-required=true
quarkus.oidc.logout.path=/auth/logout
quarkus.oidc.logout.post-logout-path=/
# ============================================
# OIDC Configuration - DEV Profile
# HTTP Security Policies (COMMUNE)
# ============================================
%dev.quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager
%dev.quarkus.oidc.client-id=lions-user-manager-client
%dev.quarkus.oidc.credentials.secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO
%dev.quarkus.oidc.token.issuer=http://localhost:8180/realms/lions-user-manager
%dev.quarkus.oidc.tls.verification=none
%dev.quarkus.oidc.authentication.pkce-required=true
# State Secret pour PKCE (OBLIGATOIRE quand pkce-required=true)
# Ce secret est utilisé pour encrypter le PKCE code verifier dans le state cookie
# Minimum 16 caractères requis, recommandé 32 caractères
# IMPORTANT: Ne PAS définir pkce-secret quand state-secret est défini (conflit Quarkus)
%dev.quarkus.oidc.authentication.state-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO
# Secret de chiffrement pour le token state manager (minimum 16 caractères requis)
# Cette clé est utilisée pour chiffrer les cookies d'état OIDC
# Valeur: 64 caractères (secret Keycloak dupliqué pour garantir la longueur)
%dev.quarkus.oidc.token-state-manager.encryption-secret=NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO
# Protéger toutes les pages JSF - force la redirection vers Keycloak login
quarkus.http.auth.permission.authenticated-pages.paths=/pages/*
quarkus.http.auth.permission.authenticated-pages.policy=authenticated
# Protéger la racine (index.xhtml / dashboard)
quarkus.http.auth.permission.authenticated-root.paths=/,/index.xhtml,/index.jsf
quarkus.http.auth.permission.authenticated-root.policy=authenticated
# Ressources publiques (CSS, JS, images, fonts, PrimeFaces resources)
quarkus.http.auth.permission.public-resources.paths=/jakarta.faces.resource/*,/resources/*,/css/*,/js/*,/images/*,/fonts/*,/favicon.ico
quarkus.http.auth.permission.public-resources.policy=permit
# Endpoint de logout (doit être accessible)
quarkus.http.auth.permission.logout.paths=/auth/logout
quarkus.http.auth.permission.logout.policy=authenticated
# Dev UI Quarkus (accessible en dev uniquement)
quarkus.http.auth.permission.dev-ui.paths=/q/*
quarkus.http.auth.permission.dev-ui.policy=permit
# ============================================
# OIDC Configuration - PROD Profile
# Configuration Lions (COMMUNE)
# ============================================
%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master}
%prod.quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:lions-user-manager-client}
%prod.quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
%prod.quarkus.oidc.token.issuer=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master}
%prod.quarkus.oidc.tls.verification=required
%prod.quarkus.oidc.authentication.cookie-same-site=strict
%prod.quarkus.oidc.authentication.pkce-required=false
# Secret production via variable d'environnement (32 caractères requis)
%prod.quarkus.oidc.token-state-manager.encryption-secret=${OIDC_ENCRYPTION_SECRET}
# Chemins publics (non protégés par OIDC)
# Note: Les pages JSF sont servies via /pages/*.xhtml
quarkus.http.auth.permission.public.paths=/,/index.xhtml,/index,/pages/public/*,/auth/*,/q/*,/q/oidc/*,/favicon.ico,/resources/*,/META-INF/resources/*,/images/*,/jakarta.faces.resource/*,/javax.faces.resource/*
quarkus.http.auth.permission.public.policy=permit
# Chemins protégés (requièrent authentification) - Désactivé en dev
quarkus.http.auth.permission.authenticated.paths=/pages/user-manager/*
quarkus.http.auth.permission.authenticated.policy=authenticated
# DEV: Chemins publics élargis pour faciliter le développement
%dev.quarkus.http.auth.permission.public.paths=/,/index.xhtml,/index,/pages/public/*,/auth/*,/q/*,/q/oidc/*,/favicon.ico,/resources/*,/META-INF/resources/*,/images/*,/jakarta.faces.resource/*,/javax.faces.resource/*
# CORS (si nécessaire pour développement)
quarkus.http.cors=true
quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:8080,http://localhost:8081}
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
quarkus.http.cors.headers=Accept,Authorization,Content-Type,X-Requested-With
# Health Checks
quarkus.smallrye-health.root-path=/health
quarkus.smallrye-health.liveness-path=/health/live
quarkus.smallrye-health.readiness-path=/health/ready
# Metrics (optionnel)
quarkus.micrometer.export.prometheus.enabled=true
quarkus.micrometer.export.prometheus.path=/metrics
# Realm par défaut utilisé par les beans (RoleView, UserView)
lions.user.manager.default.realm=lions-user-manager
# ============================================
# Keycloak Dev Services désactivé (COMMUNE)
# ============================================
quarkus.keycloak.devservices.enabled=false

View File

@@ -0,0 +1,93 @@
package dev.lions.user.manager.client.filter;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* Tests unitaires pour AuthHeaderFactory
*/
@ExtendWith(MockitoExtension.class)
class AuthHeaderFactoryTest {
@Mock
private JsonWebToken jwt;
@InjectMocks
private AuthHeaderFactory authHeaderFactory;
@BeforeEach
void setUp() {
// Setup
}
@Test
void testUpdate_WithToken() {
when(jwt.getRawToken()).thenReturn("test-token-123");
MultivaluedMap<String, String> incomingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> clientOutgoingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders);
assertEquals("Bearer test-token-123", result.getFirst("Authorization"));
}
@Test
void testUpdate_WithoutToken() {
when(jwt.getRawToken()).thenReturn(null);
MultivaluedMap<String, String> incomingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> clientOutgoingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders);
assertNull(result.getFirst("Authorization"));
}
@Test
void testUpdate_WithEmptyToken() {
when(jwt.getRawToken()).thenReturn("");
MultivaluedMap<String, String> incomingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> clientOutgoingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders);
assertNull(result.getFirst("Authorization"));
}
@Test
void testUpdate_WithNullJwt() {
AuthHeaderFactory factory = new AuthHeaderFactory();
MultivaluedMap<String, String> incomingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> clientOutgoingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> result = factory.update(incomingHeaders, clientOutgoingHeaders);
assertNotNull(result);
}
@Test
void testUpdate_ExceptionHandling() {
when(jwt.getRawToken()).thenThrow(new RuntimeException("Error"));
MultivaluedMap<String, String> incomingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> clientOutgoingHeaders = new MultivaluedHashMap<>();
MultivaluedMap<String, String> result = authHeaderFactory.update(incomingHeaders, clientOutgoingHeaders);
assertNotNull(result);
assertNull(result.getFirst("Authorization"));
}
}

View File

@@ -0,0 +1,126 @@
package dev.lions.user.manager.client.service;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import jakarta.ws.rs.core.Response.StatusType;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* Tests unitaires pour RestClientExceptionMapper
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class RestClientExceptionMapperTest {
private RestClientExceptionMapper exceptionMapper;
@Mock
private Response response;
@Mock
private StatusType statusType;
@BeforeEach
void setUp() {
exceptionMapper = new RestClientExceptionMapper();
// Mock getStatusInfo() to return a StatusType
when(response.getStatusInfo()).thenReturn(statusType);
when(statusType.getReasonPhrase()).thenReturn("Reason Phrase");
}
@Test
void testHandleBadRequest() {
when(response.getStatus()).thenReturn(400);
when(response.readEntity(String.class)).thenReturn("Bad Request");
RuntimeException exception = exceptionMapper.toThrowable(response);
assertNotNull(exception);
assertTrue(exception instanceof RestClientExceptionMapper.BadRequestException);
}
@Test
void testHandleUnauthorized() {
when(response.getStatus()).thenReturn(401);
when(response.readEntity(String.class)).thenReturn("Unauthorized");
RuntimeException exception = exceptionMapper.toThrowable(response);
assertNotNull(exception);
assertTrue(exception instanceof RestClientExceptionMapper.UnauthorizedException);
}
@Test
void testHandleForbidden() {
when(response.getStatus()).thenReturn(403);
when(response.readEntity(String.class)).thenReturn("Forbidden");
RuntimeException exception = exceptionMapper.toThrowable(response);
assertNotNull(exception);
assertTrue(exception instanceof RestClientExceptionMapper.ForbiddenException);
}
@Test
void testHandleNotFound() {
when(response.getStatus()).thenReturn(404);
when(response.readEntity(String.class)).thenReturn("Not Found");
RuntimeException exception = exceptionMapper.toThrowable(response);
assertNotNull(exception);
assertTrue(exception instanceof RestClientExceptionMapper.NotFoundException);
}
@Test
void testHandleInternalServerError() {
when(response.getStatus()).thenReturn(500);
when(response.readEntity(String.class)).thenReturn("Internal Server Error");
RuntimeException exception = exceptionMapper.toThrowable(response);
assertNotNull(exception);
assertTrue(exception instanceof RestClientExceptionMapper.InternalServerErrorException);
}
@Test
void testHandleUnknownStatus() {
when(response.getStatus()).thenReturn(418);
when(response.readEntity(String.class)).thenReturn("I'm a teapot");
RuntimeException exception = exceptionMapper.toThrowable(response);
assertNotNull(exception);
assertTrue(exception instanceof RestClientExceptionMapper.UnknownHttpStatusException);
}
@Test
void testHandlesMethod() {
MultivaluedMap<String, Object> headers = new jakarta.ws.rs.core.MultivaluedHashMap<>();
// La méthode handles vérifie le status code
assertTrue(exceptionMapper.handles(400, headers));
assertTrue(exceptionMapper.handles(500, headers));
assertFalse(exceptionMapper.handles(200, headers));
}
@Test
void testGetPriority() {
int priority = exceptionMapper.getPriority();
assertTrue(priority >= 0);
}
}

View File

@@ -0,0 +1,283 @@
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;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class AuditConsultationBeanTest {
@Mock
AuditServiceClient auditServiceClient;
@Mock
RealmServiceClient realmServiceClient;
@Mock
FacesContext facesContext;
@InjectMocks
AuditConsultationBean auditConsultationBean;
MockedStatic<FacesContext> facesContextMock;
@BeforeEach
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
void tearDown() {
facesContextMock.close();
}
@Test
void testInit() {
Map<TypeActionAudit, Long> actionStats = Map.of(TypeActionAudit.USER_CREATE, 10L);
Map<String, Long> userStats = Map.of("admin", 5L);
dev.lions.user.manager.dto.common.CountDTO failureDto = new dev.lions.user.manager.dto.common.CountDTO(2L);
dev.lions.user.manager.dto.common.CountDTO successDto = new dev.lions.user.manager.dto.common.CountDTO(8L);
when(auditServiceClient.getActionStatistics(isNull(), isNull())).thenReturn(actionStats);
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();
assertNotNull(auditConsultationBean.getAuditLogs());
assertNotNull(auditConsultationBean.getActionStatistics());
assertNotNull(auditConsultationBean.getUserActivityStatistics());
}
@Test
void testSearchLogs() {
List<AuditLogDTO> logs = Collections.singletonList(
AuditLogDTO.builder()
.acteurUsername("admin")
.typeAction(TypeActionAudit.USER_CREATE)
.build());
when(auditServiceClient.searchLogs(
nullable(String.class), nullable(String.class), nullable(String.class),
nullable(TypeActionAudit.class), nullable(String.class), nullable(Boolean.class),
anyInt(), anyInt()))
.thenReturn(logs);
auditConsultationBean.setActeurUsername("admin");
auditConsultationBean.searchLogs();
assertFalse(auditConsultationBean.getAuditLogs().isEmpty());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testSearchLogsError() {
when(auditServiceClient.searchLogs(
anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(),
anyInt(), anyInt()))
.thenThrow(new RuntimeException("Error"));
auditConsultationBean.searchLogs();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLoadLogsByActeur() {
List<AuditLogDTO> logs = Collections.singletonList(
AuditLogDTO.builder().acteurUsername("admin").build());
when(auditServiceClient.getLogsByActor("admin", 100)).thenReturn(logs);
auditConsultationBean.loadLogsByActeur("admin");
assertFalse(auditConsultationBean.getAuditLogs().isEmpty());
assertEquals(1, auditConsultationBean.getTotalRecords());
}
@Test
void testLoadLogsByActeurError() {
when(auditServiceClient.getLogsByActor("admin", 100))
.thenThrow(new RuntimeException("Error"));
auditConsultationBean.loadLogsByActeur("admin");
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLoadLogsByRealm() {
// La méthode loadLogsByRealm est maintenant un no-op loggant un avertissement
auditConsultationBean.loadLogsByRealm("master");
assertTrue(auditConsultationBean.getAuditLogs().isEmpty());
}
@Test
void testLoadLogsByRealmError() {
// No-op, no exception expected
auditConsultationBean.loadLogsByRealm("master");
}
@Test
void testLoadStatistics() {
Map<TypeActionAudit, Long> actionStats = Map.of(TypeActionAudit.USER_CREATE, 10L);
Map<String, Long> userStats = Map.of("admin", 5L);
dev.lions.user.manager.dto.common.CountDTO failureDto = new dev.lions.user.manager.dto.common.CountDTO(2L);
dev.lions.user.manager.dto.common.CountDTO successDto = new dev.lions.user.manager.dto.common.CountDTO(8L);
when(auditServiceClient.getActionStatistics(anyString(), anyString())).thenReturn(actionStats);
when(auditServiceClient.getUserActivityStatistics(anyString(), anyString())).thenReturn(userStats);
when(auditServiceClient.getFailureCount(anyString(), anyString())).thenReturn(failureDto);
when(auditServiceClient.getSuccessCount(anyString(), anyString())).thenReturn(successDto);
auditConsultationBean.setDateDebut(LocalDateTime.now().minusDays(7));
auditConsultationBean.setDateFin(LocalDateTime.now());
auditConsultationBean.loadStatistics();
assertNotNull(auditConsultationBean.getActionStatistics());
assertNotNull(auditConsultationBean.getUserActivityStatistics());
assertEquals(2L, auditConsultationBean.getFailureCount());
assertEquals(8L, auditConsultationBean.getSuccessCount());
}
@Test
void testLoadStatisticsError() {
when(auditServiceClient.getActionStatistics(anyString(), anyString()))
.thenThrow(new RuntimeException("Error"));
auditConsultationBean.loadStatistics();
// L'erreur est loggée mais ne doit pas planter
assertNotNull(auditConsultationBean);
}
@Test
void testExportToCSV() {
jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class);
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(externalContext).setResponseContentType(startsWith("text/csv"));
verify(externalContext).setResponseHeader(eq("Content-Disposition"), contains("audit-logs-test.csv"));
verify(facesContext).responseComplete();
}
@Test
void testExportToCSVError() {
when(auditServiceClient.exportLogsToCSV(anyString(), anyString()))
.thenThrow(new RuntimeException("Error"));
auditConsultationBean.exportToCSV();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testResetFilters() {
auditConsultationBean.setActeurUsername("admin");
auditConsultationBean.setDateDebut(LocalDateTime.now());
auditConsultationBean.setDateFin(LocalDateTime.now());
auditConsultationBean.setSelectedTypeAction(TypeActionAudit.USER_CREATE);
auditConsultationBean.setRessourceType("USER");
auditConsultationBean.setSucces(true);
auditConsultationBean.setCurrentPage(2);
auditConsultationBean.getAuditLogs().add(AuditLogDTO.builder().build());
auditConsultationBean.resetFilters();
assertNull(auditConsultationBean.getActeurUsername());
assertNull(auditConsultationBean.getDateDebut());
assertNull(auditConsultationBean.getDateFin());
assertNull(auditConsultationBean.getSelectedTypeAction());
assertNull(auditConsultationBean.getRessourceType());
assertNull(auditConsultationBean.getSucces());
assertEquals(0, auditConsultationBean.getCurrentPage());
assertTrue(auditConsultationBean.getAuditLogs().isEmpty());
}
@Test
void testPreviousPage() {
auditConsultationBean.setCurrentPage(2);
List<AuditLogDTO> logs = Collections.singletonList(AuditLogDTO.builder().build());
when(auditServiceClient.searchLogs(
anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(),
anyInt(), anyInt()))
.thenReturn(logs);
auditConsultationBean.previousPage();
assertEquals(1, auditConsultationBean.getCurrentPage());
}
@Test
void testPreviousPageAtFirstPage() {
auditConsultationBean.setCurrentPage(0);
auditConsultationBean.previousPage();
assertEquals(0, auditConsultationBean.getCurrentPage());
verify(auditServiceClient, never()).searchLogs(
anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(),
anyInt(), anyInt());
}
@Test
void testNextPage() {
List<AuditLogDTO> logs = Collections.singletonList(AuditLogDTO.builder().build());
when(auditServiceClient.searchLogs(
anyString(), anyString(), anyString(), any(), anyString(), anyBoolean(),
anyInt(), anyInt()))
.thenReturn(logs);
auditConsultationBean.setCurrentPage(0);
auditConsultationBean.nextPage();
assertEquals(1, auditConsultationBean.getCurrentPage());
}
}

View File

@@ -0,0 +1,128 @@
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;
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DashboardBeanTest {
@Mock
UserServiceClient userServiceClient;
@Mock
RoleServiceClient roleServiceClient;
@Mock
AuditServiceClient auditServiceClient;
@Mock
UserMetricsServiceClient userMetricsServiceClient;
@Mock
FacesContext facesContext;
@InjectMocks
DashboardBean dashboardBean;
MockedStatic<FacesContext> facesContextMock;
@BeforeEach
void setUp() {
facesContextMock = mockStatic(FacesContext.class);
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
}
@AfterEach
void tearDown() {
facesContextMock.close();
}
@Test
void testInit() {
// Mock User Client
UserSearchResultDTO userResult = new UserSearchResultDTO();
userResult.setTotalCount(100L);
when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(userResult);
// Mock Role Client
RoleDTO role = RoleDTO.builder().name("role").build();
when(roleServiceClient.getAllRealmRoles(anyString())).thenReturn(Collections.singletonList(role));
// Mock Audit Client
dev.lions.user.manager.dto.common.CountDTO successDto = new dev.lions.user.manager.dto.common.CountDTO(50L);
dev.lions.user.manager.dto.common.CountDTO failureDto = new dev.lions.user.manager.dto.common.CountDTO(5L);
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());
assertEquals(1L, dashboardBean.getTotalRoles());
assertEquals(55L, dashboardBean.getRecentActions());
assertEquals("100", dashboardBean.getTotalUsersDisplay());
assertEquals("55", dashboardBean.getRecentActionsDisplay());
}
@Test
void testLoadStatisticsError() {
when(userServiceClient.searchUsers(any())).thenThrow(new RuntimeException("Error"));
dashboardBean.loadStatistics();
assertEquals(0L, dashboardBean.getTotalUsers());
verify(facesContext).addMessage(any(), any(FacesMessage.class));
}
@Test
void testRefreshStatistics() {
// Mock User Client
UserSearchResultDTO userResult = new UserSearchResultDTO();
userResult.setTotalCount(10L);
when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(userResult);
// Mock Role Client
when(roleServiceClient.getAllRealmRoles(anyString())).thenReturn(Collections.emptyList());
// Mock Audit Client
dev.lions.user.manager.dto.common.CountDTO successDto = new dev.lions.user.manager.dto.common.CountDTO(0L);
dev.lions.user.manager.dto.common.CountDTO failureDto = new dev.lions.user.manager.dto.common.CountDTO(0L);
when(auditServiceClient.getSuccessCount(anyString(), anyString())).thenReturn(successDto);
when(auditServiceClient.getFailureCount(anyString(), anyString())).thenReturn(failureDto);
dashboardBean.refreshStatistics();
verify(facesContext, atLeastOnce()).addMessage(any(), any(FacesMessage.class));
}
}

View File

@@ -0,0 +1,152 @@
package dev.lions.user.manager.client.view;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests unitaires pour GuestPreferences
*/
class GuestPreferencesTest {
private GuestPreferences guestPreferences;
@BeforeEach
void setUp() {
guestPreferences = new GuestPreferences();
}
@Test
void testDefaultValues() {
assertEquals("blue-light", guestPreferences.getTheme());
assertEquals("light", guestPreferences.getLayout());
assertEquals("blue-light", guestPreferences.getComponentTheme());
assertEquals("light", guestPreferences.getDarkMode());
assertEquals("layout-sidebar", guestPreferences.getMenuMode());
assertEquals("light", guestPreferences.getTopbarTheme());
assertEquals("light", guestPreferences.getMenuTheme());
assertEquals("outlined", guestPreferences.getInputStyle());
assertFalse(guestPreferences.isLightLogo());
}
@Test
void testThemeSetterAndGetter() {
guestPreferences.setTheme("green-light");
assertEquals("green-light", guestPreferences.getTheme());
}
@Test
void testLayoutSetterAndGetter() {
guestPreferences.setLayout("dark");
assertEquals("dark", guestPreferences.getLayout());
}
@Test
void testComponentThemeSetterAndGetter() {
guestPreferences.setComponentTheme("purple-light");
assertEquals("purple-light", guestPreferences.getComponentTheme());
}
@Test
void testDarkModeSetterAndGetter() {
guestPreferences.setDarkMode("dark");
assertEquals("dark", guestPreferences.getDarkMode());
assertTrue(guestPreferences.isLightLogo());
}
@Test
void testDarkModeLight() {
guestPreferences.setDarkMode("light");
assertEquals("light", guestPreferences.getDarkMode());
assertFalse(guestPreferences.isLightLogo());
}
@Test
void testMenuModeSetterAndGetter() {
guestPreferences.setMenuMode("layout-horizontal");
assertEquals("layout-horizontal", guestPreferences.getMenuMode());
}
@Test
void testTopbarThemeSetterAndGetter() {
guestPreferences.setTopbarTheme("dark");
assertEquals("dark", guestPreferences.getTopbarTheme());
}
@Test
void testMenuThemeSetterAndGetter() {
guestPreferences.setMenuTheme("dark");
assertEquals("dark", guestPreferences.getMenuTheme());
}
@Test
void testInputStyleSetterAndGetter() {
guestPreferences.setInputStyle("filled");
assertEquals("filled", guestPreferences.getInputStyle());
}
@Test
void testLightLogoSetterAndGetter() {
guestPreferences.setLightLogo(true);
assertTrue(guestPreferences.isLightLogo());
guestPreferences.setLightLogo(false);
assertFalse(guestPreferences.isLightLogo());
}
@Test
void testGetInputStyleClass() {
guestPreferences.setInputStyle("outlined");
assertEquals("p-input-outlined", guestPreferences.getInputStyleClass());
guestPreferences.setInputStyle("filled");
assertEquals("p-input-filled", guestPreferences.getInputStyleClass());
}
@Test
void testGetLayoutClass() {
guestPreferences.setLayout("light");
guestPreferences.setTheme("blue-light");
assertEquals("layout-light layout-theme-blue-light", guestPreferences.getLayoutClass());
guestPreferences.setLayout("dark");
guestPreferences.setTheme("green-light");
assertEquals("layout-dark layout-theme-green-light", guestPreferences.getLayoutClass());
}
@Test
void testGetComponentThemes() {
var themes = guestPreferences.getComponentThemes();
assertNotNull(themes);
assertFalse(themes.isEmpty());
assertEquals(8, themes.size());
// Vérifier le premier thème
var firstTheme = themes.get(0);
assertEquals("blue-light", firstTheme.getFile());
assertEquals("Blue", firstTheme.getName());
assertEquals("#007ad9", firstTheme.getColor());
// Vérifier le dernier thème
var lastTheme = themes.get(themes.size() - 1);
assertEquals("cyan-light", lastTheme.getFile());
assertEquals("Cyan", lastTheme.getName());
assertEquals("#17a2b8", lastTheme.getColor());
}
@Test
void testOnMenuTypeChange() {
// Cette méthode ne fait rien, on vérifie juste qu'elle ne lance pas d'exception
assertDoesNotThrow(() -> guestPreferences.onMenuTypeChange());
}
@Test
void testComponentThemeClass() {
var theme = new GuestPreferences.ComponentTheme("test-file", "Test Name", "#FF0000");
assertEquals("test-file", theme.getFile());
assertEquals("Test Name", theme.getName());
assertEquals("#FF0000", theme.getColor());
}
}

View File

@@ -0,0 +1,345 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.service.RealmAssignmentServiceClient;
import dev.lions.user.manager.client.service.RealmServiceClient;
import dev.lions.user.manager.client.service.UserServiceClient;
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
import dev.lions.user.manager.dto.user.UserDTO;
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests unitaires pour RealmAssignmentBean
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class RealmAssignmentBeanTest {
@Mock
@RestClient
private RealmAssignmentServiceClient realmAssignmentServiceClient;
@Mock
@RestClient
private UserServiceClient userServiceClient;
@Mock
@RestClient
private RealmServiceClient realmServiceClient;
@Mock
private UserSessionBean userSessionBean;
@Mock
private FacesContext facesContext;
@InjectMocks
private RealmAssignmentBean realmAssignmentBean;
MockedStatic<FacesContext> facesContextMock;
@BeforeEach
void setUp() {
// Mock FacesContext
facesContextMock = mockStatic(FacesContext.class);
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
doNothing().when(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@AfterEach
void tearDown() {
if (facesContextMock != null) {
facesContextMock.close();
}
}
@Test
void testInit_WithAdminRole() {
when(userSessionBean.hasRole("admin")).thenReturn(true);
when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList());
when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt()))
.thenReturn(UserSearchResultDTO.builder().users(Collections.emptyList()).build());
when(realmServiceClient.getAllRealms()).thenReturn(Collections.emptyList());
realmAssignmentBean.init();
verify(realmAssignmentServiceClient).getAllAssignments();
verify(userServiceClient).getAllUsers(anyString(), anyInt(), anyInt());
verify(realmServiceClient).getAllRealms();
}
@Test
void testInit_WithoutAdminRole() {
when(userSessionBean.hasRole("admin")).thenReturn(false);
realmAssignmentBean.init();
verify(realmAssignmentServiceClient, never()).getAllAssignments();
}
@Test
void testLoadAssignments_Success() {
List<RealmAssignmentDTO> assignments = new ArrayList<>();
assignments.add(RealmAssignmentDTO.builder().id("1").build());
when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(assignments);
realmAssignmentBean.loadAssignments();
assertEquals(1, realmAssignmentBean.getAssignments().size());
verify(realmAssignmentServiceClient).getAllAssignments();
}
@Test
void testLoadAssignments_Error() {
when(realmAssignmentServiceClient.getAllAssignments()).thenThrow(new RuntimeException("Error"));
realmAssignmentBean.loadAssignments();
assertTrue(realmAssignmentBean.getAssignments().isEmpty());
}
@Test
void testLoadAvailableUsers_Success() {
List<UserDTO> users = new ArrayList<>();
users.add(UserDTO.builder().id("1").username("user1").build());
UserSearchResultDTO result = UserSearchResultDTO.builder().users(users).build();
when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt())).thenReturn(result);
realmAssignmentBean.loadAvailableUsers();
assertEquals(1, realmAssignmentBean.getAvailableUsers().size());
}
@Test
void testLoadAvailableUsers_NullResult() {
when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt())).thenReturn(null);
realmAssignmentBean.loadAvailableUsers();
assertTrue(realmAssignmentBean.getAvailableUsers().isEmpty());
}
@Test
void testLoadAvailableRealms_Success() {
List<String> realms = List.of("realm1", "realm2");
when(realmServiceClient.getAllRealms()).thenReturn(realms);
realmAssignmentBean.loadAvailableRealms();
assertEquals(2, realmAssignmentBean.getAvailableRealms().size());
}
@Test
void testLoadAvailableRealms_Empty() {
when(realmServiceClient.getAllRealms()).thenReturn(Collections.emptyList());
realmAssignmentBean.loadAvailableRealms();
assertTrue(realmAssignmentBean.getAvailableRealms().isEmpty());
}
@Test
void testAssignRealm_Success() {
when(userSessionBean.hasRole("admin")).thenReturn(true);
when(userSessionBean.getUsername()).thenReturn("admin");
List<UserDTO> users = new ArrayList<>();
UserDTO user = UserDTO.builder()
.id("user1")
.username("testuser")
.email("test@example.com")
.build();
users.add(user);
UserSearchResultDTO result = UserSearchResultDTO.builder().users(users).build();
when(userServiceClient.getAllUsers(anyString(), anyInt(), anyInt())).thenReturn(result);
realmAssignmentBean.setAvailableUsers(users);
realmAssignmentBean.setSelectedUserId("user1");
realmAssignmentBean.setSelectedRealmName("realm1");
RealmAssignmentDTO created = RealmAssignmentDTO.builder().id("1").build();
jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class);
when(response.readEntity(RealmAssignmentDTO.class)).thenReturn(created);
when(realmAssignmentServiceClient.assignRealmToUser(any(RealmAssignmentDTO.class)))
.thenReturn(response);
realmAssignmentBean.assignRealm();
verify(realmAssignmentServiceClient).assignRealmToUser(any(RealmAssignmentDTO.class));
}
@Test
void testAssignRealm_NoUserId() {
realmAssignmentBean.setSelectedUserId(null);
realmAssignmentBean.setSelectedRealmName("realm1");
realmAssignmentBean.assignRealm();
verify(realmAssignmentServiceClient, never()).assignRealmToUser(any());
}
@Test
void testAssignRealm_NoRealmName() {
realmAssignmentBean.setSelectedUserId("user1");
realmAssignmentBean.setSelectedRealmName(null);
realmAssignmentBean.assignRealm();
verify(realmAssignmentServiceClient, never()).assignRealmToUser(any());
}
@Test
void testRevokeAssignment_Success() {
RealmAssignmentDTO assignment = RealmAssignmentDTO.builder()
.id("1")
.userId("user1")
.username("testuser")
.realmName("realm1")
.build();
when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList());
realmAssignmentBean.revokeAssignment(assignment);
verify(realmAssignmentServiceClient).revokeRealmFromUser("user1", "realm1");
}
@Test
void testRevokeAssignment_Null() {
realmAssignmentBean.revokeAssignment(null);
verify(realmAssignmentServiceClient, never()).revokeRealmFromUser(anyString(), anyString());
}
@Test
void testDeactivateAssignment_Success() {
RealmAssignmentDTO assignment = RealmAssignmentDTO.builder()
.id("1")
.build();
when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList());
realmAssignmentBean.deactivateAssignment(assignment);
verify(realmAssignmentServiceClient).deactivateAssignment("1");
}
@Test
void testActivateAssignment_Success() {
RealmAssignmentDTO assignment = RealmAssignmentDTO.builder()
.id("1")
.build();
when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList());
realmAssignmentBean.activateAssignment(assignment);
verify(realmAssignmentServiceClient).activateAssignment("1");
}
@Test
void testSetSuperAdmin_Success() {
List<UserDTO> users = new ArrayList<>();
UserDTO user = UserDTO.builder()
.id("user1")
.username("testuser")
.build();
users.add(user);
realmAssignmentBean.setAvailableUsers(users);
when(realmAssignmentServiceClient.getAllAssignments()).thenReturn(Collections.emptyList());
realmAssignmentBean.setSuperAdmin("user1", true);
verify(realmAssignmentServiceClient).setSuperAdmin("user1", true);
}
@Test
void testResetForm() {
realmAssignmentBean.setSelectedUserId("user1");
realmAssignmentBean.setSelectedRealmName("realm1");
realmAssignmentBean.resetForm();
assertNull(realmAssignmentBean.getSelectedUserId());
assertNull(realmAssignmentBean.getSelectedRealmName());
}
@Test
void testGetFilteredAssignments_NoFilters() {
List<RealmAssignmentDTO> assignments = new ArrayList<>();
assignments.add(RealmAssignmentDTO.builder().username("user1").realmName("realm1").build());
realmAssignmentBean.setAssignments(assignments);
realmAssignmentBean.setFilterUserName(null);
realmAssignmentBean.setFilterRealmName(null);
List<RealmAssignmentDTO> filtered = realmAssignmentBean.getFilteredAssignments();
assertEquals(1, filtered.size());
}
@Test
void testGetFilteredAssignments_WithFilters() {
List<RealmAssignmentDTO> assignments = new ArrayList<>();
assignments.add(RealmAssignmentDTO.builder().username("user1").realmName("realm1").build());
assignments.add(RealmAssignmentDTO.builder().username("user2").realmName("realm2").build());
realmAssignmentBean.setAssignments(assignments);
realmAssignmentBean.setFilterUserName("user1");
realmAssignmentBean.setFilterRealmName("realm1");
List<RealmAssignmentDTO> filtered = realmAssignmentBean.getFilteredAssignments();
assertEquals(1, filtered.size());
assertEquals("user1", filtered.get(0).getUsername());
}
@Test
void testGetTotalAssignments() {
List<RealmAssignmentDTO> assignments = new ArrayList<>();
assignments.add(RealmAssignmentDTO.builder().build());
assignments.add(RealmAssignmentDTO.builder().build());
realmAssignmentBean.setAssignments(assignments);
assertEquals(2, realmAssignmentBean.getTotalAssignments());
}
@Test
void testGetActiveAssignmentsCount() {
List<RealmAssignmentDTO> assignments = new ArrayList<>();
assignments.add(RealmAssignmentDTO.builder().active(true).build());
assignments.add(RealmAssignmentDTO.builder().active(false).build());
assignments.add(RealmAssignmentDTO.builder().active(true).build());
realmAssignmentBean.setAssignments(assignments);
assertEquals(2, realmAssignmentBean.getActiveAssignmentsCount());
}
@Test
void testGetSuperAdminsCount() {
List<RealmAssignmentDTO> assignments = new ArrayList<>();
assignments.add(RealmAssignmentDTO.builder().isSuperAdmin(true).build());
assignments.add(RealmAssignmentDTO.builder().isSuperAdmin(false).build());
assignments.add(RealmAssignmentDTO.builder().isSuperAdmin(true).build());
realmAssignmentBean.setAssignments(assignments);
assertEquals(2, realmAssignmentBean.getSuperAdminsCount());
}
}

View File

@@ -0,0 +1,388 @@
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;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class RoleGestionBeanTest {
@Mock
RoleServiceClient roleServiceClient;
@Mock
RealmServiceClient realmServiceClient;
@Mock
FacesContext facesContext;
@Mock
ExternalContext externalContext;
@InjectMocks
RoleGestionBean roleGestionBean;
MockedStatic<FacesContext> facesContextMock;
private static final String REALM_NAME = "lions-user-manager";
private static final String CLIENT_NAME = "test-client";
@BeforeEach
void setUp() {
facesContextMock = mockStatic(FacesContext.class);
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
when(facesContext.getExternalContext()).thenReturn(externalContext);
}
@AfterEach
void tearDown() {
facesContextMock.close();
}
@Test
void testInit() {
roleGestionBean.init();
assertNotNull(roleGestionBean.getRealmRoles());
assertNotNull(roleGestionBean.getClientRoles());
assertNotNull(roleGestionBean.getAllRoles());
assertEquals(REALM_NAME, roleGestionBean.getRealmName());
}
@Test
void testLoadRealmRoles() {
List<RoleDTO> roles = Collections.singletonList(
RoleDTO.builder().id("1").name("admin").build());
when(roleServiceClient.getAllRealmRoles(REALM_NAME)).thenReturn(roles);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.loadRealmRoles();
assertFalse(roleGestionBean.getRealmRoles().isEmpty());
assertEquals(1, roleGestionBean.getRealmRoles().size());
}
@Test
void testLoadRealmRolesEmptyRealm() {
roleGestionBean.setRealmName("");
roleGestionBean.loadRealmRoles();
verify(roleServiceClient, never()).getAllRealmRoles(anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLoadRealmRolesError() {
when(roleServiceClient.getAllRealmRoles(REALM_NAME))
.thenThrow(new RuntimeException("Error"));
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.loadRealmRoles();
assertTrue(roleGestionBean.getRealmRoles().isEmpty());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLoadClientRoles() {
List<RoleDTO> roles = Collections.singletonList(
RoleDTO.builder().id("1").name("client-role").build());
when(roleServiceClient.getAllClientRoles(eq(CLIENT_NAME), eq(REALM_NAME))).thenReturn(roles);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.setClientName(CLIENT_NAME);
roleGestionBean.loadClientRoles();
assertFalse(roleGestionBean.getClientRoles().isEmpty());
assertEquals(1, roleGestionBean.getClientRoles().size());
}
@Test
void testLoadClientRolesEmptyClient() {
roleGestionBean.setClientName("");
roleGestionBean.loadClientRoles();
verify(roleServiceClient, never()).getAllClientRoles(anyString(), anyString());
}
@Test
void testCreateRealmRole() {
RoleDTO newRole = RoleDTO.builder().name("new-role").description("New role").build();
RoleDTO created = RoleDTO.builder().id("1").name("new-role").description("New role").build();
jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class);
when(response.readEntity(RoleDTO.class)).thenReturn(created);
when(roleServiceClient.createRealmRole(any(RoleDTO.class), eq(REALM_NAME)))
.thenReturn(response);
when(roleServiceClient.getAllRealmRoles(REALM_NAME))
.thenReturn(Collections.singletonList(created));
roleGestionBean.setNewRole(newRole);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.createRealmRole();
verify(roleServiceClient).createRealmRole(any(RoleDTO.class), eq(REALM_NAME));
verify(roleServiceClient).getAllRealmRoles(REALM_NAME);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testCreateRealmRoleError() {
RoleDTO newRole = RoleDTO.builder().name("new-role").build();
when(roleServiceClient.createRealmRole(any(RoleDTO.class), eq(REALM_NAME)))
.thenThrow(new RuntimeException("Error"));
roleGestionBean.setNewRole(newRole);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.createRealmRole();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testCreateClientRole() {
RoleDTO newRole = RoleDTO.builder().name("client-role").build();
RoleDTO created = RoleDTO.builder().id("1").name("client-role").build();
jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class);
when(response.readEntity(RoleDTO.class)).thenReturn(created);
when(roleServiceClient.createClientRole(eq(CLIENT_NAME), any(RoleDTO.class), eq(REALM_NAME)))
.thenReturn(response);
when(roleServiceClient.getAllClientRoles(eq(CLIENT_NAME), eq(REALM_NAME)))
.thenReturn(Collections.singletonList(created));
roleGestionBean.setNewRole(newRole);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.setClientName(CLIENT_NAME);
roleGestionBean.createClientRole();
verify(roleServiceClient).createClientRole(eq(CLIENT_NAME), any(RoleDTO.class), eq(REALM_NAME));
verify(roleServiceClient).getAllClientRoles(eq(CLIENT_NAME), eq(REALM_NAME));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testCreateClientRoleEmptyClient() {
roleGestionBean.setClientName("");
roleGestionBean.createClientRole();
verify(roleServiceClient, never()).createClientRole(anyString(),
any(dev.lions.user.manager.dto.role.RoleDTO.class), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testDeleteRealmRole() {
// Mock pour le rechargement après suppression (retourne une liste vide)
when(roleServiceClient.getAllRealmRoles(REALM_NAME))
.thenReturn(Collections.emptyList());
doNothing().when(roleServiceClient).deleteRealmRole("admin", REALM_NAME);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.deleteRealmRole("admin");
verify(roleServiceClient).deleteRealmRole("admin", REALM_NAME);
verify(roleServiceClient, atLeastOnce()).getAllRealmRoles(REALM_NAME);
// addMessage est appelé au moins une fois (pour le succès ou l'erreur)
verify(facesContext, atLeastOnce()).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testDeleteRealmRoleError() {
doThrow(new RuntimeException("Error"))
.when(roleServiceClient).deleteRealmRole("admin", REALM_NAME);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.deleteRealmRole("admin");
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testDeleteClientRole() {
when(roleServiceClient.getAllClientRoles(eq(CLIENT_NAME), eq(REALM_NAME)))
.thenReturn(Collections.emptyList());
doNothing().when(roleServiceClient).deleteClientRole(CLIENT_NAME, "client-role", REALM_NAME);
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.setClientName(CLIENT_NAME);
roleGestionBean.deleteClientRole("client-role");
verify(roleServiceClient).deleteClientRole(CLIENT_NAME, "client-role", REALM_NAME);
verify(roleServiceClient).getAllClientRoles(CLIENT_NAME, REALM_NAME);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testDeleteClientRoleEmptyClient() {
roleGestionBean.setClientName("");
roleGestionBean.deleteClientRole("client-role");
verify(roleServiceClient, never()).deleteClientRole(anyString(), anyString(), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testAssignRoleToUser() {
doNothing().when(roleServiceClient).assignRealmRoles(anyString(), anyString(),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.assignRoleToUser("user-1", "admin");
verify(roleServiceClient).assignRealmRoles(eq("user-1"), eq(REALM_NAME),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testAssignRoleToUserError() {
doThrow(new RuntimeException("Error"))
.when(roleServiceClient).assignRealmRoles(anyString(), anyString(),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.assignRoleToUser("user-1", "admin");
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testRevokeRoleFromUser() {
doNothing().when(roleServiceClient).revokeRealmRoles(anyString(), anyString(),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.revokeRoleFromUser("user-1", "admin");
verify(roleServiceClient).revokeRealmRoles(eq("user-1"), eq(REALM_NAME),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testRevokeRoleFromUserError() {
doThrow(new RuntimeException("Error"))
.when(roleServiceClient).revokeRealmRoles(anyString(), anyString(),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.revokeRoleFromUser("user-1", "admin");
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testAssignRoleFromParams() {
Map<String, String> params = new HashMap<>();
params.put("userId", "user-1");
params.put("roleName", "admin");
when(externalContext.getRequestParameterMap()).thenReturn(params);
doNothing().when(roleServiceClient).assignRealmRoles(anyString(), anyString(),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.assignRoleFromParams();
verify(roleServiceClient).assignRealmRoles(eq("user-1"), eq(REALM_NAME),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
}
@Test
void testAssignRoleFromParamsMissing() {
Map<String, String> params = new HashMap<>();
when(externalContext.getRequestParameterMap()).thenReturn(params);
roleGestionBean.assignRoleFromParams();
verify(roleServiceClient, never()).assignRealmRoles(anyString(), anyString(), any());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testRevokeRoleFromParams() {
Map<String, String> params = new HashMap<>();
params.put("userId", "user-1");
params.put("roleName", "admin");
when(externalContext.getRequestParameterMap()).thenReturn(params);
doNothing().when(roleServiceClient).revokeRealmRoles(anyString(), anyString(),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
roleGestionBean.setRealmName(REALM_NAME);
roleGestionBean.revokeRoleFromParams();
verify(roleServiceClient).revokeRealmRoles(eq("user-1"), eq(REALM_NAME),
any(dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO.class));
}
@Test
void testGetUserRolesDTOs() {
UserDTO user = UserDTO.builder()
.id("user-1")
.realmRoles(List.of("admin", "user_manager"))
.build();
RoleDTO role1 = RoleDTO.builder().id("1").name("admin").build();
RoleDTO role2 = RoleDTO.builder().id("2").name("user_manager").build();
RoleDTO role3 = RoleDTO.builder().id("3").name("other").build();
roleGestionBean.setAllRoles(List.of(role1, role2, role3));
List<RoleDTO> result = roleGestionBean.getUserRolesDTOs(user);
assertEquals(2, result.size());
assertTrue(result.stream().anyMatch(r -> r.getName().equals("admin")));
assertTrue(result.stream().anyMatch(r -> r.getName().equals("user_manager")));
}
@Test
void testGetUserRolesDTOsNullUser() {
List<RoleDTO> result = roleGestionBean.getUserRolesDTOs(null);
assertTrue(result.isEmpty());
}
@Test
void testGetUserRolesDTOsEmptyRoles() {
UserDTO user = UserDTO.builder().id("user-1").realmRoles(Collections.emptyList()).build();
List<RoleDTO> result = roleGestionBean.getUserRolesDTOs(user);
assertTrue(result.isEmpty());
}
@Test
void testResetForm() {
RoleDTO role = RoleDTO.builder().name("test").build();
roleGestionBean.setNewRole(role);
roleGestionBean.setEditMode(true);
roleGestionBean.resetForm();
assertNotNull(roleGestionBean.getNewRole());
assertFalse(roleGestionBean.isEditMode());
}
}

View File

@@ -0,0 +1,77 @@
package dev.lions.user.manager.client.view;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SettingsBeanTest {
@Mock
UserSessionBean userSessionBean;
@Mock
GuestPreferences guestPreferences;
@Mock
FacesContext facesContext;
@InjectMocks
SettingsBean settingsBean;
MockedStatic<FacesContext> facesContextMock;
@BeforeEach
void setUp() {
facesContextMock = mockStatic(FacesContext.class);
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
}
@AfterEach
void tearDown() {
if (facesContextMock != null) {
facesContextMock.close();
}
}
@Test
void testInit() {
settingsBean.init();
// Vérifier que l'initialisation se fait sans erreur
assertNotNull(settingsBean);
}
@Test
void testSavePreferences() {
settingsBean.savePreferences();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testSavePreferencesError() {
// Simuler une erreur lors de l'accès à FacesContext
facesContextMock.when(FacesContext::getCurrentInstance).thenThrow(new RuntimeException("Error"));
// Le bean devrait gérer l'erreur gracieusement
assertDoesNotThrow(() -> {
try {
settingsBean.savePreferences();
} catch (Exception e) {
// L'erreur est attendue dans ce cas
}
});
}
}

View File

@@ -0,0 +1,180 @@
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;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserCreationBeanTest {
@Mock
UserServiceClient userServiceClient;
@Mock
RealmServiceClient realmServiceClient;
@Mock
FacesContext facesContext;
@InjectMocks
UserCreationBean userCreationBean;
MockedStatic<FacesContext> facesContextMock;
private static final String REALM_NAME = "lions-user-manager";
@BeforeEach
void setUp() {
facesContextMock = mockStatic(FacesContext.class);
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
when(realmServiceClient.getAllRealms()).thenReturn(java.util.List.of("master"));
}
@AfterEach
void tearDown() {
if (facesContextMock != null) {
facesContextMock.close();
}
}
@Test
void testInit() {
userCreationBean.init();
assertNotNull(userCreationBean.getNewUser());
assertTrue(Boolean.TRUE.equals(userCreationBean.getNewUser().getEnabled()));
assertFalse(Boolean.TRUE.equals(userCreationBean.getNewUser().getEmailVerified()));
assertEquals(StatutUser.ACTIF, userCreationBean.getNewUser().getStatut());
assertEquals("master", userCreationBean.getRealmName());
}
@Test
void testCreateUser() {
UserDTO newUser = UserDTO.builder()
.username("newuser")
.email("newuser@example.com")
.prenom("John")
.nom("Doe")
.build();
UserDTO createdUser = UserDTO.builder()
.id("user-123")
.username("newuser")
.email("newuser@example.com")
.build();
jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class);
when(response.readEntity(UserDTO.class)).thenReturn(createdUser);
when(userServiceClient.createUser(any(UserDTO.class), eq(REALM_NAME)))
.thenReturn(response);
doNothing().when(userServiceClient).resetPassword(eq("user-123"), eq(REALM_NAME),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
userCreationBean.setNewUser(newUser);
userCreationBean.setPassword("password123");
userCreationBean.setPasswordConfirm("password123");
userCreationBean.setRealmName(REALM_NAME);
String result = userCreationBean.createUser();
assertEquals("userListPage", result);
verify(userServiceClient).createUser(any(UserDTO.class), eq(REALM_NAME));
verify(userServiceClient).resetPassword(eq("user-123"), eq(REALM_NAME),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testCreateUserEmptyPassword() {
userCreationBean.setPassword("");
userCreationBean.setPasswordConfirm("");
String result = userCreationBean.createUser();
assertNull(result);
verify(userServiceClient, never()).createUser(any(), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testCreateUserPasswordMismatch() {
userCreationBean.setPassword("password1");
userCreationBean.setPasswordConfirm("password2");
String result = userCreationBean.createUser();
assertNull(result);
verify(userServiceClient, never()).createUser(any(), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testCreateUserPasswordTooShort() {
userCreationBean.setPassword("short");
userCreationBean.setPasswordConfirm("short");
String result = userCreationBean.createUser();
assertNull(result);
verify(userServiceClient, never()).createUser(any(), anyString());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testCreateUserError() {
when(userServiceClient.createUser(any(UserDTO.class), eq(REALM_NAME)))
.thenThrow(new RuntimeException("Error"));
userCreationBean.setPassword("password123");
userCreationBean.setPasswordConfirm("password123");
userCreationBean.setRealmName(REALM_NAME);
String result = userCreationBean.createUser();
assertNull(result);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testResetForm() {
UserDTO user = UserDTO.builder()
.username("testuser")
.email("test@example.com")
.build();
userCreationBean.setNewUser(user);
userCreationBean.setPassword("password123");
userCreationBean.setPasswordConfirm("password123");
userCreationBean.resetForm();
assertNotNull(userCreationBean.getNewUser());
assertNull(userCreationBean.getPassword());
assertNull(userCreationBean.getPasswordConfirm());
assertTrue(Boolean.TRUE.equals(userCreationBean.getNewUser().getEnabled()));
assertFalse(Boolean.TRUE.equals(userCreationBean.getNewUser().getEmailVerified()));
assertEquals(StatutUser.ACTIF, userCreationBean.getNewUser().getStatut());
}
@Test
void testCancel() {
String result = userCreationBean.cancel();
assertEquals("userListPage", result);
assertNotNull(userCreationBean.getNewUser());
}
}

View File

@@ -0,0 +1,86 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.service.UserServiceClient;
import dev.lions.user.manager.dto.user.UserDTO;
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserListBeanTest {
@Mock
UserServiceClient userServiceClient;
@Mock
FacesContext facesContext;
@InjectMocks
UserListBean userListBean;
MockedStatic<FacesContext> facesContextMock;
@BeforeEach
void setUp() {
facesContextMock = mockStatic(FacesContext.class);
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
}
@AfterEach
void tearDown() {
facesContextMock.close();
}
@Test
void testInit() {
UserSearchResultDTO result = new UserSearchResultDTO();
result.setUsers(Collections.singletonList(new UserDTO()));
result.setTotalCount(1L);
when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(result);
userListBean.init();
assertEquals(1, userListBean.getTotalRecords());
}
@Test
void testSearch() {
UserSearchResultDTO result = new UserSearchResultDTO();
result.setUsers(Collections.singletonList(new UserDTO()));
result.setTotalCount(10L);
when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(result);
userListBean.setSearchText("test");
userListBean.search();
assertEquals(0, userListBean.getCurrentPage()); // Should reset to 0
}
// onPageChange removed as it does not exist in UserListBean
@Test
void testActivateUser() {
doNothing().when(userServiceClient).activateUser(anyString(), anyString());
// mock loadUsers calls searchUsers
when(userServiceClient.searchUsers(any(UserSearchCriteriaDTO.class))).thenReturn(new UserSearchResultDTO());
userListBean.activateUser("1");
verify(userServiceClient).activateUser(eq("1"), anyString());
verify(facesContext).addMessage(any(), any(FacesMessage.class));
}
}

View File

@@ -0,0 +1,356 @@
package dev.lions.user.manager.client.view;
import dev.lions.user.manager.client.service.RestClientExceptionMapper;
import dev.lions.user.manager.client.service.UserServiceClient;
import dev.lions.user.manager.dto.user.UserDTO;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class UserProfilBeanTest {
@Mock
UserServiceClient userServiceClient;
@Mock
RoleGestionBean roleGestionBean;
@Mock
FacesContext facesContext;
@Mock
ExternalContext externalContext;
@InjectMocks
UserProfilBean userProfilBean;
MockedStatic<FacesContext> facesContextMock;
private static final String USER_ID = "test-user-id";
private static final String REALM_NAME = "lions-user-manager";
@BeforeEach
void setUp() {
facesContextMock = mockStatic(FacesContext.class);
facesContextMock.when(FacesContext::getCurrentInstance).thenReturn(facesContext);
when(facesContext.getExternalContext()).thenReturn(externalContext);
Map<String, String> params = new HashMap<>();
params.put("userId", USER_ID);
when(externalContext.getRequestParameterMap()).thenReturn(params);
}
@AfterEach
void tearDown() {
facesContextMock.close();
}
@Test
void testInitWithUserId() {
UserDTO user = UserDTO.builder()
.id(USER_ID)
.username("testuser")
.build();
when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user);
userProfilBean.init();
assertNotNull(userProfilBean.getUser());
assertEquals(USER_ID, userProfilBean.getUserId());
assertEquals(REALM_NAME, userProfilBean.getRealmName());
verify(roleGestionBean).setRealmName(REALM_NAME);
verify(roleGestionBean).loadRealmRoles();
}
@Test
void testInitWithoutUserId() {
Map<String, String> emptyParams = new HashMap<>();
when(externalContext.getRequestParameterMap()).thenReturn(emptyParams);
userProfilBean.init();
assertNull(userProfilBean.getUser());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLoadUser() {
UserDTO user = UserDTO.builder()
.id(USER_ID)
.username("testuser")
.build();
when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.loadUser();
assertNotNull(userProfilBean.getUser());
assertEquals("testuser", userProfilBean.getUser().getUsername());
}
@Test
void testLoadUserNotFound() {
when(userServiceClient.getUserById(USER_ID, REALM_NAME))
.thenThrow(new RestClientExceptionMapper.NotFoundException("User not found"));
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.loadUser();
assertNull(userProfilBean.getUser());
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLoadUserError() {
when(userServiceClient.getUserById(USER_ID, REALM_NAME))
.thenThrow(new RuntimeException("Error"));
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.loadUser();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testEnableEditMode() {
userProfilBean.enableEditMode();
assertTrue(userProfilBean.isEditMode());
}
@Test
void testCancelEdit() {
UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build();
when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.setEditMode(true);
userProfilBean.cancelEdit();
assertFalse(userProfilBean.isEditMode());
verify(userServiceClient).getUserById(USER_ID, REALM_NAME);
}
@Test
void testUpdateUser() {
UserDTO user = UserDTO.builder()
.id(USER_ID)
.username("testuser")
.email("test@example.com")
.build();
UserDTO updatedUser = UserDTO.builder()
.id(USER_ID)
.username("testuser")
.email("updated@example.com")
.build();
when(userServiceClient.updateUser(eq(USER_ID), any(UserDTO.class), eq(REALM_NAME)))
.thenReturn(updatedUser);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.setUser(user);
userProfilBean.setEditMode(true);
userProfilBean.updateUser();
assertFalse(userProfilBean.isEditMode());
verify(userServiceClient).updateUser(eq(USER_ID), any(UserDTO.class), eq(REALM_NAME));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testUpdateUserError() {
UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build();
when(userServiceClient.updateUser(eq(USER_ID), any(UserDTO.class), eq(REALM_NAME)))
.thenThrow(new RuntimeException("Error"));
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.setUser(user);
userProfilBean.updateUser();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testResetPassword() {
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.setNewPassword("newPassword123");
userProfilBean.setNewPasswordConfirm("newPassword123");
doNothing().when(userServiceClient).resetPassword(eq(USER_ID), eq(REALM_NAME),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
userProfilBean.resetPassword();
assertNull(userProfilBean.getNewPassword());
assertNull(userProfilBean.getNewPasswordConfirm());
verify(userServiceClient).resetPassword(eq(USER_ID), eq(REALM_NAME),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testResetPasswordEmpty() {
userProfilBean.setNewPassword("");
userProfilBean.resetPassword();
verify(userServiceClient, never()).resetPassword(anyString(), anyString(),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testResetPasswordMismatch() {
userProfilBean.setNewPassword("password1");
userProfilBean.setNewPasswordConfirm("password2");
userProfilBean.resetPassword();
verify(userServiceClient, never()).resetPassword(anyString(), anyString(),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testResetPasswordError() {
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.setNewPassword("newPassword123");
userProfilBean.setNewPasswordConfirm("newPassword123");
doThrow(new RuntimeException("Error"))
.when(userServiceClient)
.resetPassword(eq(USER_ID), eq(REALM_NAME),
any(dev.lions.user.manager.dto.user.PasswordResetRequestDTO.class));
userProfilBean.resetPassword();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testActivateUser() {
UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build();
when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user);
doNothing().when(userServiceClient).activateUser(USER_ID, REALM_NAME);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.activateUser();
verify(userServiceClient).activateUser(USER_ID, REALM_NAME);
verify(userServiceClient).getUserById(USER_ID, REALM_NAME);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testActivateUserError() {
doThrow(new RuntimeException("Error"))
.when(userServiceClient).activateUser(USER_ID, REALM_NAME);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.activateUser();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testDeactivateUser() {
UserDTO user = UserDTO.builder().id(USER_ID).username("testuser").build();
when(userServiceClient.getUserById(USER_ID, REALM_NAME)).thenReturn(user);
doNothing().when(userServiceClient).deactivateUser(eq(USER_ID), eq(REALM_NAME), anyString());
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.deactivateUser();
verify(userServiceClient).deactivateUser(eq(USER_ID), eq(REALM_NAME), anyString());
verify(userServiceClient).getUserById(USER_ID, REALM_NAME);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testDeactivateUserError() {
doThrow(new RuntimeException("Error"))
.when(userServiceClient).deactivateUser(eq(USER_ID), eq(REALM_NAME), anyString());
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.deactivateUser();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testSendVerificationEmail() {
doNothing().when(userServiceClient).sendVerificationEmail(USER_ID, REALM_NAME);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.sendVerificationEmail();
verify(userServiceClient).sendVerificationEmail(USER_ID, REALM_NAME);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testSendVerificationEmailError() {
doThrow(new RuntimeException("Error"))
.when(userServiceClient).sendVerificationEmail(USER_ID, REALM_NAME);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.sendVerificationEmail();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLogoutAllSessions() {
doNothing().when(userServiceClient).logoutAllSessions(USER_ID, REALM_NAME);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.logoutAllSessions();
verify(userServiceClient).logoutAllSessions(USER_ID, REALM_NAME);
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
@Test
void testLogoutAllSessionsError() {
doThrow(new RuntimeException("Error"))
.when(userServiceClient).logoutAllSessions(USER_ID, REALM_NAME);
userProfilBean.setUserId(USER_ID);
userProfilBean.setRealmName(REALM_NAME);
userProfilBean.logoutAllSessions();
verify(facesContext).addMessage(isNull(), any(FacesMessage.class));
}
}

View File

@@ -0,0 +1,298 @@
package dev.lions.user.manager.client.view;
import io.quarkus.oidc.IdToken;
import io.quarkus.oidc.OidcSession;
import io.quarkus.security.identity.SecurityIdentity;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class UserSessionBeanTest {
@Mock
SecurityIdentity securityIdentity;
@Mock
@IdToken
JsonWebToken idToken;
@Mock
OidcSession oidcSession;
@InjectMocks
UserSessionBean userSessionBean;
@BeforeEach
void setUp() {
// Configuration par défaut pour les tests
}
@Test
void testLoadUserInfoWithToken() {
when(securityIdentity.isAnonymous()).thenReturn(false);
when(idToken.getClaim("preferred_username")).thenReturn("testuser");
when(idToken.getClaim("email")).thenReturn("test@example.com");
when(idToken.getClaim("given_name")).thenReturn("John");
when(idToken.getClaim("family_name")).thenReturn("Doe");
when(idToken.getClaim("name")).thenReturn("John Doe");
userSessionBean.loadUserInfo();
assertEquals("testuser", userSessionBean.getUsername());
assertEquals("test@example.com", userSessionBean.getEmail());
assertEquals("John", userSessionBean.getFirstName());
assertEquals("Doe", userSessionBean.getLastName());
assertEquals("John Doe", userSessionBean.getFullName());
assertEquals("JD", userSessionBean.getInitials());
}
@Test
void testLoadUserInfoAnonymous() {
when(securityIdentity.isAnonymous()).thenReturn(true);
userSessionBean.loadUserInfo();
assertEquals("Utilisateur", userSessionBean.getUsername());
assertEquals("utilisateur@lions.dev", userSessionBean.getEmail());
assertEquals("Utilisateur", userSessionBean.getFullName());
assertEquals("U", userSessionBean.getInitials());
}
@Test
void testLoadUserInfoNullToken() {
when(securityIdentity.isAnonymous()).thenReturn(true);
// idToken is null by default when securityIdentity.isAnonymous() is true
userSessionBean.loadUserInfo();
assertEquals("Utilisateur", userSessionBean.getUsername());
}
@Test
void testGenerateInitials() {
// Test avec nom complet
when(securityIdentity.isAnonymous()).thenReturn(false);
when(idToken.getClaim("name")).thenReturn("John Doe");
when(idToken.getClaim("preferred_username")).thenReturn("testuser");
when(idToken.getClaim("email")).thenReturn("test@example.com");
when(idToken.getClaim("given_name")).thenReturn("John");
when(idToken.getClaim("family_name")).thenReturn("Doe");
userSessionBean.loadUserInfo();
assertEquals("JD", userSessionBean.getInitials());
}
@Test
void testGenerateInitialsSingleName() {
when(securityIdentity.isAnonymous()).thenReturn(false);
when(idToken.getClaim("name")).thenReturn("John");
when(idToken.getClaim("preferred_username")).thenReturn("testuser");
when(idToken.getClaim("email")).thenReturn("test@example.com");
when(idToken.getClaim("given_name")).thenReturn("John");
when(idToken.getClaim("family_name")).thenReturn(null);
userSessionBean.loadUserInfo();
assertEquals("JO", userSessionBean.getInitials());
}
@Test
void testGetPrimaryRole() {
when(securityIdentity.isAnonymous()).thenReturn(false);
when(securityIdentity.getRoles()).thenReturn(Set.of("admin", "user_manager"));
// Load user info first to initialize the bean
userSessionBean.loadUserInfo();
String primaryRole = userSessionBean.getPrimaryRole();
assertEquals("Administrateur", primaryRole);
}
@Test
void testGetPrimaryRoleUserManager() {
when(securityIdentity.isAnonymous()).thenReturn(false);
when(securityIdentity.getRoles()).thenReturn(Set.of("user_manager"));
// Load user info first to initialize the bean
userSessionBean.loadUserInfo();
String primaryRole = userSessionBean.getPrimaryRole();
assertEquals("Gestionnaire", primaryRole);
}
@Test
void testGetPrimaryRoleUserViewer() {
when(securityIdentity.isAnonymous()).thenReturn(false);
when(securityIdentity.getRoles()).thenReturn(Set.of("user_viewer"));
// Load user info first to initialize the bean
userSessionBean.loadUserInfo();
String primaryRole = userSessionBean.getPrimaryRole();
assertEquals("Consultant", primaryRole);
}
@Test
void testGetRoles() {
when(securityIdentity.isAnonymous()).thenReturn(false);
when(securityIdentity.getRoles()).thenReturn(Set.of("admin", "user_manager"));
// Load user info first to initialize the bean
userSessionBean.loadUserInfo();
Set<String> roles = userSessionBean.getRoles();
assertFalse(roles.isEmpty());
assertTrue(roles.contains("admin"));
assertTrue(roles.contains("user_manager"));
}
@Test
void testGetRolesAnonymous() {
when(securityIdentity.isAnonymous()).thenReturn(true);
// Load user info first to initialize the bean
userSessionBean.loadUserInfo();
Set<String> roles = userSessionBean.getRoles();
assertFalse(roles.isEmpty());
assertTrue(roles.contains("Utilisateur"));
}
@Test
void testIsAuthenticated() {
when(securityIdentity.isAnonymous()).thenReturn(false);
assertTrue(userSessionBean.isAuthenticated());
}
@Test
void testIsAuthenticatedAnonymous() {
when(securityIdentity.isAnonymous()).thenReturn(true);
assertFalse(userSessionBean.isAuthenticated());
}
@Test
void testHasRole() {
when(securityIdentity.isAnonymous()).thenReturn(false);
when(securityIdentity.getRoles()).thenReturn(Set.of("admin", "user_manager"));
// Load user info first to initialize the bean
userSessionBean.loadUserInfo();
assertTrue(userSessionBean.hasRole("admin"));
assertTrue(userSessionBean.hasRole("user_manager"));
assertFalse(userSessionBean.hasRole("auditor"));
}
@Test
void testHasRoleAnonymous() {
when(securityIdentity.isAnonymous()).thenReturn(true);
// Load user info first to initialize the bean
userSessionBean.loadUserInfo();
assertFalse(userSessionBean.hasRole("admin"));
}
@Test
void testGetIssuer() {
when(idToken.getIssuer()).thenReturn("https://security.lions.dev/realms/master");
String issuer = userSessionBean.getIssuer();
assertEquals("https://security.lions.dev/realms/master", issuer);
}
@Test
void testGetIssuerNull() {
// Mock idToken.getIssuer() to throw an exception to simulate null token
when(idToken.getIssuer()).thenThrow(new RuntimeException("Token is null"));
String issuer = userSessionBean.getIssuer();
assertEquals("Non disponible", issuer);
}
@Test
void testGetSubject() {
when(idToken.getSubject()).thenReturn("user-123");
String subject = userSessionBean.getSubject();
assertEquals("user-123", subject);
}
@Test
void testGetSessionId() {
when(idToken.getClaim("sid")).thenReturn("session-123");
String sessionId = userSessionBean.getSessionId();
assertEquals("session-123", sessionId);
}
@Test
void testGetExpirationTime() {
when(idToken.getExpirationTime()).thenReturn(1735689600L); // 2025-01-01 00:00:00 UTC
java.util.Date expiration = userSessionBean.getExpirationTime();
assertNotNull(expiration);
}
@Test
void testGetIssuedAt() {
when(idToken.getIssuedAtTime()).thenReturn(1735603200L); // 2024-12-31 00:00:00 UTC
java.util.Date issuedAt = userSessionBean.getIssuedAt();
assertNotNull(issuedAt);
}
@Test
void testGetAudience() {
when(idToken.getAudience()).thenReturn(Set.of("client1", "client2"));
String audience = userSessionBean.getAudience();
assertTrue(audience.contains("client1"));
assertTrue(audience.contains("client2"));
}
@Test
void testGetAuthorizedParty() {
when(idToken.getClaim("azp")).thenReturn("lions-user-manager-client");
String azp = userSessionBean.getAuthorizedParty();
assertEquals("lions-user-manager-client", azp);
}
@Test
void testIsEmailVerified() {
when(idToken.getClaim("email_verified")).thenReturn(true);
assertTrue(userSessionBean.isEmailVerified());
}
@Test
void testIsEmailVerifiedFalse() {
when(idToken.getClaim("email_verified")).thenReturn(false);
assertFalse(userSessionBean.isEmailVerified());
}
}