diff --git a/Dockerfile.prod b/Dockerfile.prod
new file mode 100644
index 0000000..ead8e81
--- /dev/null
+++ b/Dockerfile.prod
@@ -0,0 +1,86 @@
+####
+# Dockerfile de production pour Lions User Manager Server (Backend)
+# Multi-stage build optimisé avec sécurité renforcée
+# Basé sur la structure de btpxpress-server
+####
+
+## 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
+
+# Construire l'application avec profil production
+RUN mvn clean package -DskipTests -B -Dquarkus.profile=prod
+
+## Stage 2 : Image de production optimisée
+FROM registry.access.redhat.com/ubi8/openjdk-17:1.18
+
+ENV LANGUAGE='en_US:en'
+
+# Configuration des variables d'environnement pour production
+ENV QUARKUS_PROFILE=prod
+ENV DB_URL=jdbc:postgresql://postgresql:5432/lions_audit
+ENV DB_USERNAME=lions_audit_user
+ENV DB_PASSWORD=changeme
+ENV SERVER_PORT=8080
+
+# Configuration Keycloak/OIDC (production)
+ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/master
+ENV QUARKUS_OIDC_CLIENT_ID=lions-user-manager
+ENV KEYCLOAK_CLIENT_SECRET=changeme
+ENV QUARKUS_OIDC_TLS_VERIFICATION=required
+
+# Configuration Keycloak Admin Client
+ENV LIONS_KEYCLOAK_SERVER_URL=https://security.lions.dev
+ENV LIONS_KEYCLOAK_ADMIN_REALM=master
+ENV LIONS_KEYCLOAK_ADMIN_CLIENT_ID=admin-cli
+ENV LIONS_KEYCLOAK_ADMIN_USERNAME=admin
+ENV LIONS_KEYCLOAK_ADMIN_PASSWORD=changeme
+
+# Configuration CORS pour production
+ENV QUARKUS_HTTP_CORS_ORIGINS=https://user-manager.lions.dev,https://admin.lions.dev
+ENV QUARKUS_HTTP_CORS_ALLOW_CREDENTIALS=true
+
+# Installer curl pour les health checks
+USER root
+RUN microdnf install curl -y && microdnf clean all
+RUN mkdir -p /app/logs && chown -R 185:185 /app/logs
+USER 185
+
+# Copier l'application depuis le builder
+COPY --from=builder --chown=185 /app/target/quarkus-app/lib/ /deployments/lib/
+COPY --from=builder --chown=185 /app/target/quarkus-app/*.jar /deployments/
+COPY --from=builder --chown=185 /app/target/quarkus-app/app/ /deployments/app/
+COPY --from=builder --chown=185 /app/target/quarkus-app/quarkus/ /deployments/quarkus/
+
+# Exposer le port
+EXPOSE 8080
+
+# Variables JVM optimisées pour production avec sécurité
+ENV JAVA_OPTS="-Xmx1g -Xms512m \
+ -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}"
+
+# Point d'entrée avec profil production
+ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"]
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
+ CMD curl -f http://localhost:8080/q/health/ready || exit 1
+
diff --git a/pom.xml b/pom.xml
index 834730f..e82b319 100644
--- a/pom.xml
+++ b/pom.xml
@@ -141,6 +141,18 @@
mockito-core
test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-inline
+ 5.2.0
+ test
+
diff --git a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java
index 75693f4..de5f7c1 100644
--- a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java
+++ b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java
@@ -51,6 +51,12 @@ public interface KeycloakAdminClient {
*/
boolean realmExists(String realmName);
+ /**
+ * Récupère tous les realms disponibles dans Keycloak
+ * @return liste des noms de realms
+ */
+ java.util.List getAllRealms();
+
/**
* Ferme la connexion Keycloak
*/
diff --git a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java
index a6c5684..4070595 100644
--- a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java
+++ b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java
@@ -1,11 +1,13 @@
package dev.lions.user.manager.client;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.runtime.Startup;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
@@ -19,6 +21,10 @@ import org.keycloak.admin.client.resource.UsersResource;
import jakarta.ws.rs.NotFoundException;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
/**
* Implémentation du client Keycloak Admin
@@ -29,19 +35,19 @@ import java.time.temporal.ChronoUnit;
@Slf4j
public class KeycloakAdminClientImpl implements KeycloakAdminClient {
- @ConfigProperty(name = "lions.keycloak.server-url")
+ @ConfigProperty(name = "lions.keycloak.server-url", defaultValue = "")
String serverUrl;
- @ConfigProperty(name = "lions.keycloak.admin-realm")
+ @ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master")
String adminRealm;
- @ConfigProperty(name = "lions.keycloak.admin-client-id")
+ @ConfigProperty(name = "lions.keycloak.admin-client-id", defaultValue = "admin-cli")
String adminClientId;
- @ConfigProperty(name = "lions.keycloak.admin-username")
+ @ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin")
String adminUsername;
- @ConfigProperty(name = "lions.keycloak.admin-password")
+ @ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "")
String adminPassword;
@ConfigProperty(name = "lions.keycloak.connection-pool-size", defaultValue = "10")
@@ -54,6 +60,13 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@PostConstruct
void init() {
+ // Ne pas initialiser si les propriétés essentielles sont vides (ex: en mode test)
+ if (serverUrl == null || serverUrl.isEmpty()) {
+ log.debug("Configuration Keycloak non disponible - mode test ou configuration manquante");
+ this.keycloak = null;
+ return;
+ }
+
log.info("========================================");
log.info("Initialisation du client Keycloak Admin");
log.info("========================================");
@@ -144,13 +157,70 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@Override
public boolean realmExists(String realmName) {
try {
- getRealm(realmName).toRepresentation();
+ // Essayer d'obtenir simplement la liste des rôles du realm
+ // Si le realm n'existe pas, cela lancera une NotFoundException
+ // Si le realm existe mais a des problèmes de désérialisation, on suppose qu'il existe
+ getRealm(realmName).roles().list();
return true;
} catch (NotFoundException e) {
+ log.debug("Realm {} n'existe pas", realmName);
return false;
} catch (Exception e) {
- log.error("Erreur lors de la vérification de l'existence du realm {}: {}", realmName, e.getMessage());
- return false;
+ // En cas d'erreur (comme bruteForceStrategy lors de .toRepresentation()),
+ // on suppose que le realm existe car l'erreur indique qu'on a pu le contacter
+ log.debug("Erreur lors de la vérification du realm {} (probablement il existe): {}",
+ realmName, e.getMessage());
+ return true;
+ }
+ }
+
+ @Override
+ @Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
+ @Timeout(value = 30, unit = ChronoUnit.SECONDS)
+ @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
+ public List getAllRealms() {
+ try {
+ log.debug("Récupération de tous les realms depuis Keycloak via API REST directe");
+
+ // Obtenir un token d'accès pour l'API REST
+ Keycloak keycloakInstance = getInstance();
+ String accessToken = keycloakInstance.tokenManager().getAccessTokenString();
+
+ // Utiliser un client HTTP REST pour appeler directement l'API Keycloak
+ // et parser uniquement les noms des realms depuis le JSON
+ Client client = ClientBuilder.newClient();
+ try {
+ String realmsUrl = serverUrl + "/admin/realms";
+
+ @SuppressWarnings("unchecked")
+ List