feat(server-impl): refactoring resources JAX-RS, corrections AuditService/SyncService/UserService, ajout entites Sync et scripts Docker

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
lionsdev
2026-02-18 03:27:55 +00:00
parent bf1e9e16d8
commit bbab8ca7ec
56 changed files with 2916 additions and 4696 deletions

View File

@@ -1,33 +1,37 @@
package dev.lions.user.manager.client;
import com.fasterxml.jackson.core.type.TypeReference;
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 jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.faulttolerance.Timeout;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.admin.client.resource.UsersResource;
import jakarta.ws.rs.NotFoundException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Implémentation du client Keycloak Admin
* Utilise le bean Keycloak géré par Quarkus (quarkus-keycloak-admin-rest-client)
* qui respecte la configuration Jackson (fail-on-unknown-properties=false)
* Utilise Circuit Breaker, Retry et Timeout pour la résilience
*/
@ApplicationScoped
@@ -35,38 +39,23 @@ import java.util.Map;
@Slf4j
public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@ConfigProperty(name = "lions.keycloak.server-url", defaultValue = "")
@Inject
Keycloak keycloak;
@ConfigProperty(name = "lions.keycloak.server-url")
String serverUrl;
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master")
@ConfigProperty(name = "lions.keycloak.admin-realm")
String adminRealm;
@ConfigProperty(name = "lions.keycloak.admin-client-id", defaultValue = "admin-cli")
@ConfigProperty(name = "lions.keycloak.admin-client-id")
String adminClientId;
@ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin")
@ConfigProperty(name = "lions.keycloak.admin-username")
String adminUsername;
@ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "")
String adminPassword;
@ConfigProperty(name = "lions.keycloak.connection-pool-size", defaultValue = "10")
Integer connectionPoolSize;
@ConfigProperty(name = "lions.keycloak.timeout-seconds", defaultValue = "30")
Integer timeoutSeconds;
private Keycloak keycloak;
@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("========================================");
@@ -74,29 +63,8 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
log.info("Admin Realm: {}", adminRealm);
log.info("Admin Client ID: {}", adminClientId);
log.info("Admin Username: {}", adminUsername);
log.info("Connection Pool Size: {}", connectionPoolSize);
log.info("Timeout: {} secondes", timeoutSeconds);
try {
this.keycloak = KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(adminRealm)
.clientId(adminClientId)
.username(adminUsername)
.password(adminPassword)
.build();
log.info("✅ Client Keycloak initialisé (connexion lazy)");
log.info("La connexion sera établie lors de la première requête API");
} catch (Exception e) {
log.warn("⚠️ Échec de l'initialisation du client Keycloak");
log.warn("URL: {}", serverUrl);
log.warn("Realm: {}", adminRealm);
log.warn("Username: {}", adminUsername);
log.warn("Message: {}", e.getMessage());
// Ne pas bloquer le démarrage - la connexion sera tentée lors du premier appel
this.keycloak = null;
}
log.info("✅ Client Keycloak initialisé via Quarkus CDI (connexion lazy)");
log.info("La connexion sera établie lors de la première requête API");
}
@Override
@@ -104,10 +72,6 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
public Keycloak getInstance() {
if (keycloak == null) {
log.warn("Instance Keycloak null, tentative de réinitialisation...");
init();
}
return keycloak;
}
@@ -117,7 +81,7 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
public RealmResource getRealm(String realmName) {
try {
return getInstance().realm(realmName);
return keycloak.realm(realmName);
} catch (Exception e) {
log.error("Erreur lors de la récupération du realm {}: {}", realmName, e.getMessage());
throw new RuntimeException("Impossible de récupérer le realm: " + realmName, e);
@@ -143,10 +107,9 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@Override
public boolean isConnected() {
try {
if (keycloak == null) {
return false;
}
keycloak.serverInfo().getInfo();
// getAccessTokenString() n'implique pas la désérialisation de ServerInfoRepresentation
// (qui échoue sur le champ inconnu "cpuInfo" avec Keycloak 26+)
keycloak.tokenManager().getAccessTokenString();
return true;
} catch (Exception e) {
log.warn("Keycloak non connecté: {}", e.getMessage());
@@ -157,17 +120,12 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@Override
public boolean realmExists(String realmName) {
try {
// 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) {
// 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;
@@ -180,64 +138,96 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
public List<String> 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<Map<String, Object>> realmsJson = client.target(realmsUrl)
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.get(List.class);
List<String> realmNames = new ArrayList<>();
if (realmsJson != null) {
for (Map<String, Object> realm : realmsJson) {
Object realmNameObj = realm.get("realm");
if (realmNameObj != null) {
String realmName = realmNameObj.toString();
if (!realmName.isEmpty()) {
realmNames.add(realmName);
}
}
}
realmNames.sort(String::compareTo);
}
log.info("Récupération réussie: {} realms trouvés", realmNames.size());
return realmNames;
} finally {
client.close();
log.debug("Récupération de tous les realms depuis Keycloak");
// Appel HTTP direct pour éviter l'erreur de désérialisation de RealmRepresentation
// (champ bruteForceStrategy inconnu dans la version de la librairie cliente)
String token = keycloak.tokenManager().getAccessTokenString();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(serverUrl + "/admin/realms"))
.header("Authorization", "Bearer " + token)
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Keycloak returned HTTP " + response.statusCode());
}
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<Map<String, Object>> realmMaps = mapper.readValue(
response.body(), new TypeReference<>() {});
List<String> realms = realmMaps.stream()
.map(r -> (String) r.get("realm"))
.filter(r -> r != null)
.collect(Collectors.toList());
log.debug("Realms récupérés: {}", realms);
return realms;
} catch (Exception e) {
log.error("Erreur lors de la récupération des realms: {}", e.getMessage(), e);
// En cas d'erreur, retourner une liste vide plutôt que des données fictives
return Collections.emptyList();
log.error("Erreur lors de la récupération de tous les realms: {}", e.getMessage());
throw new RuntimeException("Impossible de récupérer la liste des realms", e);
}
}
@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<String> getRealmClients(String realmName) {
try {
log.debug("Récupération des clients du realm {}", realmName);
String token = keycloak.tokenManager().getAccessTokenString();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(serverUrl + "/admin/realms/" + realmName + "/clients"))
.header("Authorization", "Bearer " + token)
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Keycloak returned HTTP " + response.statusCode());
}
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<Map<String, Object>> clientMaps = mapper.readValue(
response.body(), new TypeReference<>() {});
List<String> clients = clientMaps.stream()
.map(c -> (String) c.get("clientId"))
.filter(c -> c != null)
.collect(Collectors.toList());
log.debug("Clients récupérés pour {}: {}", realmName, clients);
return clients;
} catch (Exception e) {
log.error("Erreur lors de la récupération des clients du realm {}: {}", realmName, e.getMessage());
throw new RuntimeException("Impossible de récupérer les clients du realm: " + realmName, e);
}
}
@PreDestroy
@Override
public void close() {
if (keycloak != null) {
log.info("Fermeture de la connexion Keycloak...");
keycloak.close();
keycloak = null;
}
log.info("Fermeture de la connexion Keycloak...");
// Le cycle de vie est géré par Quarkus CDI
}
@Override
public void reconnect() {
log.info("Reconnexion à Keycloak...");
close();
init();
log.info("Reconnexion à Keycloak... (géré par Quarkus CDI)");
// Le bean Keycloak est géré par Quarkus, pas de reconnexion manuelle nécessaire
}
}