feat: Initial lions-user-manager project structure
Phase 1 & 2 Implementation (40% completion) Module server-api (✅ COMPLETED - 15 files): - DTOs complets (User, Role, Audit, Search) - Enums (StatutUser, TypeRole, TypeActionAudit) - Service interfaces (User, Role, Audit, Sync) - ValidationConstants - 100% compilé et testé Module server-impl-quarkus (🔄 EN COURS - 7 files): - KeycloakAdminClient avec Circuit Breaker, Retry, Timeout - UserServiceImpl avec 25+ méthodes - UserResource REST API (12 endpoints) - Health checks Keycloak - Configurations dev/prod séparées - Mappers UserDTO <-> Keycloak UserRepresentation Module client (⏳ À FAIRE - 0 files): - Configuration PrimeFaces Freya à venir - Interface utilisateur JSF à venir Infrastructure: - Maven multi-modules (parent + 3 enfants) - Quarkus 3.15.1 - Keycloak Admin Client 23.0.3 - PrimeFaces 14.0.5 - Documentation complète (README, PROGRESS_REPORT) Contraintes respectées: - ZÉRO accès direct DB Keycloak (Admin API uniquement) - Multi-realm avec délégation - Résilience (Circuit Breaker, Retry) - Sécurité (@RolesAllowed, OIDC) - Observabilité (Health, Metrics) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
206
lions-user-manager-server-impl-quarkus/pom.xml
Normal file
206
lions-user-manager-server-impl-quarkus/pom.xml
Normal file
@@ -0,0 +1,206 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>dev.lions.user.manager</groupId>
|
||||
<artifactId>lions-user-manager-parent</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>lions-user-manager-server-impl-quarkus</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Lions User Manager - Server Implementation (Quarkus)</name>
|
||||
<description>Implémentation serveur: Resources REST, Services, Keycloak Admin Client</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Module API -->
|
||||
<dependency>
|
||||
<groupId>dev.lions.user.manager</groupId>
|
||||
<artifactId>lions-user-manager-server-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Quarkus Extensions -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-oidc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-health</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-client-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Keycloak Admin Client -->
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-admin-client</artifactId>
|
||||
<version>23.0.3</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-client</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-multipart-provider</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jackson2-provider</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Optional: Database for audit logs -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-flyway</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.quarkus.platform</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,63 @@
|
||||
package dev.lions.user.manager.client;
|
||||
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
|
||||
/**
|
||||
* Interface pour le client Keycloak Admin
|
||||
* Abstraction pour faciliter les tests et la gestion du cycle de vie
|
||||
*/
|
||||
public interface KeycloakAdminClient {
|
||||
|
||||
/**
|
||||
* Récupère l'instance Keycloak
|
||||
* @return instance Keycloak
|
||||
*/
|
||||
Keycloak getInstance();
|
||||
|
||||
/**
|
||||
* Récupère une ressource Realm
|
||||
* @param realmName nom du realm
|
||||
* @return RealmResource
|
||||
*/
|
||||
RealmResource getRealm(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la ressource Users d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return UsersResource
|
||||
*/
|
||||
UsersResource getUsers(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la ressource Roles d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return RolesResource
|
||||
*/
|
||||
RolesResource getRoles(String realmName);
|
||||
|
||||
/**
|
||||
* Vérifie si la connexion à Keycloak est active
|
||||
* @return true si connecté
|
||||
*/
|
||||
boolean isConnected();
|
||||
|
||||
/**
|
||||
* Vérifie si un realm existe
|
||||
* @param realmName nom du realm
|
||||
* @return true si le realm existe
|
||||
*/
|
||||
boolean realmExists(String realmName);
|
||||
|
||||
/**
|
||||
* Ferme la connexion Keycloak
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Force la reconnexion
|
||||
*/
|
||||
void reconnect();
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package dev.lions.user.manager.client;
|
||||
|
||||
import io.quarkus.runtime.Startup;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
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.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* Implémentation du client Keycloak Admin
|
||||
* Utilise Circuit Breaker, Retry et Timeout pour la résilience
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Startup
|
||||
@Slf4j
|
||||
public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String serverUrl;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.admin-realm")
|
||||
String adminRealm;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.admin-client-id")
|
||||
String adminClientId;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.admin-username")
|
||||
String adminUsername;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.admin-password")
|
||||
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() {
|
||||
log.info("Initialisation du client Keycloak Admin...");
|
||||
log.info("Server URL: {}", serverUrl);
|
||||
log.info("Admin Realm: {}", adminRealm);
|
||||
log.info("Admin Client ID: {}", adminClientId);
|
||||
log.info("Admin Username: {}", adminUsername);
|
||||
|
||||
try {
|
||||
this.keycloak = KeycloakBuilder.builder()
|
||||
.serverUrl(serverUrl)
|
||||
.realm(adminRealm)
|
||||
.clientId(adminClientId)
|
||||
.username(adminUsername)
|
||||
.password(adminPassword)
|
||||
.build();
|
||||
|
||||
// Test de connexion
|
||||
keycloak.serverInfo().getInfo();
|
||||
log.info("✅ Connexion à Keycloak réussie!");
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Échec de la connexion à Keycloak: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("Impossible de se connecter à Keycloak", 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 Keycloak getInstance() {
|
||||
if (keycloak == null) {
|
||||
log.warn("Instance Keycloak null, tentative de réinitialisation...");
|
||||
init();
|
||||
}
|
||||
return keycloak;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
||||
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
||||
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
||||
public RealmResource getRealm(String realmName) {
|
||||
try {
|
||||
return getInstance().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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
||||
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
||||
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
||||
public UsersResource getUsers(String realmName) {
|
||||
return getRealm(realmName).users();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
||||
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
||||
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
||||
public RolesResource getRoles(String realmName) {
|
||||
return getRealm(realmName).roles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
try {
|
||||
if (keycloak == null) {
|
||||
return false;
|
||||
}
|
||||
keycloak.serverInfo().getInfo();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("Keycloak non connecté: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean realmExists(String realmName) {
|
||||
try {
|
||||
getRealm(realmName).toRepresentation();
|
||||
return true;
|
||||
} catch (NotFoundException e) {
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la vérification de l'existence du realm {}: {}", realmName, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@Override
|
||||
public void close() {
|
||||
if (keycloak != null) {
|
||||
log.info("Fermeture de la connexion Keycloak...");
|
||||
keycloak.close();
|
||||
keycloak = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reconnect() {
|
||||
log.info("Reconnexion à Keycloak...");
|
||||
close();
|
||||
init();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.enums.user.StatutUser;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Mapper pour convertir UserRepresentation (Keycloak) -> UserDTO
|
||||
* Utilisé pour transformer les objets de l'API Keycloak vers nos DTOs
|
||||
*/
|
||||
public class UserMapper {
|
||||
|
||||
private UserMapper() {
|
||||
// Classe utilitaire
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit UserRepresentation vers UserDTO
|
||||
* @param userRep UserRepresentation de Keycloak
|
||||
* @param realmName nom du realm
|
||||
* @return UserDTO
|
||||
*/
|
||||
public static UserDTO toDTO(UserRepresentation userRep, String realmName) {
|
||||
if (userRep == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return UserDTO.builder()
|
||||
.id(userRep.getId())
|
||||
.username(userRep.getUsername())
|
||||
.email(userRep.getEmail())
|
||||
.emailVerified(userRep.isEmailVerified())
|
||||
.prenom(userRep.getFirstName())
|
||||
.nom(userRep.getLastName())
|
||||
.statut(StatutUser.fromEnabled(userRep.isEnabled()))
|
||||
.enabled(userRep.isEnabled())
|
||||
.realmName(realmName)
|
||||
.attributes(userRep.getAttributes())
|
||||
.requiredActions(userRep.getRequiredActions())
|
||||
.dateCreation(convertTimestamp(userRep.getCreatedTimestamp()))
|
||||
.telephone(getAttributeValue(userRep, "phone_number"))
|
||||
.organisation(getAttributeValue(userRep, "organization"))
|
||||
.departement(getAttributeValue(userRep, "department"))
|
||||
.fonction(getAttributeValue(userRep, "job_title"))
|
||||
.pays(getAttributeValue(userRep, "country"))
|
||||
.ville(getAttributeValue(userRep, "city"))
|
||||
.langue(getAttributeValue(userRep, "locale"))
|
||||
.timezone(getAttributeValue(userRep, "timezone"))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit UserDTO vers UserRepresentation
|
||||
* @param userDTO UserDTO
|
||||
* @return UserRepresentation
|
||||
*/
|
||||
public static UserRepresentation toRepresentation(UserDTO userDTO) {
|
||||
if (userDTO == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(userDTO.getId());
|
||||
userRep.setUsername(userDTO.getUsername());
|
||||
userRep.setEmail(userDTO.getEmail());
|
||||
userRep.setEmailVerified(userDTO.getEmailVerified());
|
||||
userRep.setFirstName(userDTO.getPrenom());
|
||||
userRep.setLastName(userDTO.getNom());
|
||||
userRep.setEnabled(userDTO.getEnabled() != null ? userDTO.getEnabled() : true);
|
||||
|
||||
// Attributs personnalisés
|
||||
Map<String, List<String>> attributes = new HashMap<>();
|
||||
|
||||
if (userDTO.getTelephone() != null) {
|
||||
attributes.put("phone_number", List.of(userDTO.getTelephone()));
|
||||
}
|
||||
if (userDTO.getOrganisation() != null) {
|
||||
attributes.put("organization", List.of(userDTO.getOrganisation()));
|
||||
}
|
||||
if (userDTO.getDepartement() != null) {
|
||||
attributes.put("department", List.of(userDTO.getDepartement()));
|
||||
}
|
||||
if (userDTO.getFonction() != null) {
|
||||
attributes.put("job_title", List.of(userDTO.getFonction()));
|
||||
}
|
||||
if (userDTO.getPays() != null) {
|
||||
attributes.put("country", List.of(userDTO.getPays()));
|
||||
}
|
||||
if (userDTO.getVille() != null) {
|
||||
attributes.put("city", List.of(userDTO.getVille()));
|
||||
}
|
||||
if (userDTO.getLangue() != null) {
|
||||
attributes.put("locale", List.of(userDTO.getLangue()));
|
||||
}
|
||||
if (userDTO.getTimezone() != null) {
|
||||
attributes.put("timezone", List.of(userDTO.getTimezone()));
|
||||
}
|
||||
|
||||
// Ajouter les attributs existants du DTO
|
||||
if (userDTO.getAttributes() != null) {
|
||||
attributes.putAll(userDTO.getAttributes());
|
||||
}
|
||||
|
||||
userRep.setAttributes(attributes);
|
||||
|
||||
// Actions requises
|
||||
if (userDTO.getRequiredActions() != null) {
|
||||
userRep.setRequiredActions(userDTO.getRequiredActions());
|
||||
}
|
||||
|
||||
return userRep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une liste de UserRepresentation vers UserDTO
|
||||
* @param userReps liste de UserRepresentation
|
||||
* @param realmName nom du realm
|
||||
* @return liste de UserDTO
|
||||
*/
|
||||
public static List<UserDTO> toDTOList(List<UserRepresentation> userReps, String realmName) {
|
||||
if (userReps == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return userReps.stream()
|
||||
.map(userRep -> toDTO(userRep, realmName))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la valeur d'un attribut Keycloak
|
||||
* @param userRep UserRepresentation
|
||||
* @param attributeName nom de l'attribut
|
||||
* @return valeur de l'attribut ou null
|
||||
*/
|
||||
private static String getAttributeValue(UserRepresentation userRep, String attributeName) {
|
||||
if (userRep.getAttributes() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> values = userRep.getAttributes().get(attributeName);
|
||||
if (values == null || values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un timestamp (millisecondes) vers LocalDateTime
|
||||
* @param timestamp timestamp en millisecondes
|
||||
* @return LocalDateTime ou null
|
||||
*/
|
||||
private static LocalDateTime convertTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(timestamp),
|
||||
ZoneId.systemDefault()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Resource REST pour health et readiness
|
||||
*/
|
||||
@Path("/api/health")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Slf4j
|
||||
public class HealthResourceEndpoint {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@GET
|
||||
@Path("/keycloak")
|
||||
public Map<String, Object> getKeycloakHealth() {
|
||||
Map<String, Object> health = new HashMap<>();
|
||||
|
||||
try {
|
||||
boolean connected = keycloakAdminClient.isConnected();
|
||||
health.put("status", connected ? "UP" : "DOWN");
|
||||
health.put("connected", connected);
|
||||
health.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
if (connected) {
|
||||
// Récupérer info serveur Keycloak
|
||||
var serverInfo = keycloakAdminClient.getInstance().serverInfo().getInfo();
|
||||
health.put("keycloakVersion", serverInfo.getSystemInfo().getVersion());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur health check Keycloak", e);
|
||||
health.put("status", "ERROR");
|
||||
health.put("connected", false);
|
||||
health.put("error", e.getMessage());
|
||||
health.put("timestamp", System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return health;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/status")
|
||||
public Map<String, Object> getServiceStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
status.put("service", "lions-user-manager-server");
|
||||
status.put("version", "1.0.0");
|
||||
status.put("status", "UP");
|
||||
status.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// Health Keycloak
|
||||
try {
|
||||
boolean keycloakConnected = keycloakAdminClient.isConnected();
|
||||
status.put("keycloak", keycloakConnected ? "CONNECTED" : "DISCONNECTED");
|
||||
} catch (Exception e) {
|
||||
status.put("keycloak", "ERROR");
|
||||
status.put("keycloakError", e.getMessage());
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.health.HealthCheck;
|
||||
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||
import org.eclipse.microprofile.health.Readiness;
|
||||
|
||||
/**
|
||||
* Health check pour Keycloak
|
||||
*/
|
||||
@Readiness
|
||||
@Slf4j
|
||||
public class KeycloakHealthCheck implements HealthCheck {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Override
|
||||
public HealthCheckResponse call() {
|
||||
try {
|
||||
boolean connected = keycloakAdminClient.isConnected();
|
||||
|
||||
if (connected) {
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.up()
|
||||
.withData("status", "connected")
|
||||
.withData("message", "Keycloak est disponible")
|
||||
.build();
|
||||
} else {
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.down()
|
||||
.withData("status", "disconnected")
|
||||
.withData("message", "Keycloak n'est pas disponible")
|
||||
.build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du health check Keycloak", e);
|
||||
return HealthCheckResponse.builder()
|
||||
.name("keycloak-connection")
|
||||
.down()
|
||||
.withData("status", "error")
|
||||
.withData("message", e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
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.service.UserService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des utilisateurs
|
||||
* Endpoints exposés pour les opérations CRUD sur les utilisateurs Keycloak
|
||||
*/
|
||||
@Path("/api/users")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Users", description = "Gestion des utilisateurs Keycloak")
|
||||
@Slf4j
|
||||
public class UserResource {
|
||||
|
||||
@Inject
|
||||
UserService userService;
|
||||
|
||||
@POST
|
||||
@Path("/search")
|
||||
@Operation(summary = "Rechercher des utilisateurs", description = "Recherche d'utilisateurs selon des critères")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Résultats de recherche",
|
||||
content = @Content(schema = @Schema(implementation = UserSearchResultDTO.class))),
|
||||
@APIResponse(responseCode = "400", description = "Critères invalides"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) {
|
||||
log.info("POST /api/users/search - Recherche d'utilisateurs");
|
||||
|
||||
try {
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
return Response.ok(result).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la recherche d'utilisateurs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{userId}")
|
||||
@Operation(summary = "Récupérer un utilisateur par ID", description = "Récupère les détails d'un utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Utilisateur trouvé",
|
||||
content = @Content(schema = @Schema(implementation = UserDTO.class))),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager", "user_viewer"})
|
||||
public Response getUserById(
|
||||
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId,
|
||||
@Parameter(description = "Nom du realm") @QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("GET /api/users/{} - realm: {}", userId, realmName);
|
||||
|
||||
try {
|
||||
return userService.getUserById(userId, realmName)
|
||||
.map(user -> Response.ok(user).build())
|
||||
.orElse(Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Utilisateur non trouvé"))
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Operation(summary = "Lister tous les utilisateurs", description = "Liste paginée de tous les utilisateurs")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Liste des utilisateurs",
|
||||
content = @Content(schema = @Schema(implementation = UserSearchResultDTO.class))),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager", "user_viewer"})
|
||||
public Response getAllUsers(
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("pageSize") @DefaultValue("20") int pageSize
|
||||
) {
|
||||
log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize);
|
||||
|
||||
try {
|
||||
UserSearchResultDTO result = userService.getAllUsers(realmName, page, pageSize);
|
||||
return Response.ok(result).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des utilisateurs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Operation(summary = "Créer un utilisateur", description = "Crée un nouvel utilisateur dans Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "201", description = "Utilisateur créé",
|
||||
content = @Content(schema = @Schema(implementation = UserDTO.class))),
|
||||
@APIResponse(responseCode = "400", description = "Données invalides"),
|
||||
@APIResponse(responseCode = "409", description = "Utilisateur existe déjà"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response createUser(
|
||||
@Valid @NotNull UserDTO user,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/users - Création d'un utilisateur: {}", user.getUsername());
|
||||
|
||||
try {
|
||||
UserDTO createdUser = userService.createUser(user, realmName);
|
||||
return Response.status(Response.Status.CREATED).entity(createdUser).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création de l'utilisateur", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{userId}")
|
||||
@Operation(summary = "Mettre à jour un utilisateur", description = "Met à jour les informations d'un utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Utilisateur mis à jour",
|
||||
content = @Content(schema = @Schema(implementation = UserDTO.class))),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "400", description = "Données invalides"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response updateUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@Valid @NotNull UserDTO user,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("PUT /api/users/{} - Mise à jour", userId);
|
||||
|
||||
try {
|
||||
UserDTO updatedUser = userService.updateUser(userId, user, realmName);
|
||||
return Response.ok(updatedUser).build();
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getMessage().contains("non trouvé")) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
log.error("Erreur lors de la mise à jour de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{userId}")
|
||||
@Operation(summary = "Supprimer un utilisateur", description = "Supprime un utilisateur (soft ou hard delete)")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Utilisateur supprimé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin"})
|
||||
public Response deleteUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@QueryParam("hardDelete") @DefaultValue("false") boolean hardDelete
|
||||
) {
|
||||
log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete);
|
||||
|
||||
try {
|
||||
userService.deleteUser(userId, realmName, hardDelete);
|
||||
return Response.noContent().build();
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getMessage().contains("non trouvé")) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
log.error("Erreur lors de la suppression de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/activate")
|
||||
@Operation(summary = "Activer un utilisateur", description = "Active un utilisateur désactivé")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Utilisateur activé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response activateUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/users/{}/activate", userId);
|
||||
|
||||
try {
|
||||
userService.activateUser(userId, realmName);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'activation de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/deactivate")
|
||||
@Operation(summary = "Désactiver un utilisateur", description = "Désactive un utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Utilisateur désactivé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response deactivateUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@QueryParam("raison") String raison
|
||||
) {
|
||||
log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison);
|
||||
|
||||
try {
|
||||
userService.deactivateUser(userId, realmName, raison);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la désactivation de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/reset-password")
|
||||
@Operation(summary = "Réinitialiser le mot de passe", description = "Définit un nouveau mot de passe pour l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Mot de passe réinitialisé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response resetPassword(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@NotNull PasswordResetRequest request
|
||||
) {
|
||||
log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.temporary);
|
||||
|
||||
try {
|
||||
userService.resetPassword(userId, realmName, request.password, request.temporary);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la réinitialisation du mot de passe pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/send-verification-email")
|
||||
@Operation(summary = "Envoyer email de vérification", description = "Envoie un email de vérification à l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Email envoyé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response sendVerificationEmail(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/users/{}/send-verification-email", userId);
|
||||
|
||||
try {
|
||||
userService.sendVerificationEmail(userId, realmName);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'envoi de l'email de vérification pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/logout-sessions")
|
||||
@Operation(summary = "Déconnecter toutes les sessions", description = "Révoque toutes les sessions actives de l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Sessions révoquées"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response logoutAllSessions(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/users/{}/logout-sessions", userId);
|
||||
|
||||
try {
|
||||
int count = userService.logoutAllSessions(userId, realmName);
|
||||
return Response.ok(new SessionsRevokedResponse(count)).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la déconnexion des sessions pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{userId}/sessions")
|
||||
@Operation(summary = "Récupérer les sessions actives", description = "Liste les sessions actives de l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Liste des sessions"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager", "user_viewer"})
|
||||
public Response getActiveSessions(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("GET /api/users/{}/sessions", userId);
|
||||
|
||||
try {
|
||||
List<String> sessions = userService.getActiveSessions(userId, realmName);
|
||||
return Response.ok(sessions).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des sessions pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DTOs internes ====================
|
||||
|
||||
@Schema(description = "Requête de réinitialisation de mot de passe")
|
||||
public static class PasswordResetRequest {
|
||||
@Schema(description = "Nouveau mot de passe", required = true)
|
||||
public String password;
|
||||
|
||||
@Schema(description = "Indique si le mot de passe est temporaire", defaultValue = "true")
|
||||
public boolean temporary = true;
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse de révocation de sessions")
|
||||
public static class SessionsRevokedResponse {
|
||||
@Schema(description = "Nombre de sessions révoquées")
|
||||
public int count;
|
||||
|
||||
public SessionsRevokedResponse(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse d'erreur")
|
||||
public static class ErrorResponse {
|
||||
@Schema(description = "Message d'erreur")
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
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.mapper.UserMapper;
|
||||
import dev.lions.user.manager.service.UserService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation du service de gestion des utilisateurs
|
||||
* Utilise UNIQUEMENT l'API Admin Keycloak (ZERO accès direct à la DB Keycloak)
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Override
|
||||
public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) {
|
||||
log.info("Recherche d'utilisateurs avec critères: {}", criteria);
|
||||
|
||||
try {
|
||||
String realmName = criteria.getRealmName();
|
||||
UsersResource usersResource = keycloakAdminClient.getUsers(realmName);
|
||||
|
||||
// Construire la requête de recherche
|
||||
List<UserRepresentation> users;
|
||||
|
||||
if (criteria.getSearchTerm() != null && !criteria.getSearchTerm().isBlank()) {
|
||||
// Recherche globale
|
||||
users = usersResource.search(
|
||||
criteria.getSearchTerm(),
|
||||
criteria.getOffset(),
|
||||
criteria.getPageSize()
|
||||
);
|
||||
} else if (criteria.getUsername() != null) {
|
||||
// Recherche par username exact
|
||||
users = usersResource.search(
|
||||
criteria.getUsername(),
|
||||
criteria.getOffset(),
|
||||
criteria.getPageSize(),
|
||||
true // exact match
|
||||
);
|
||||
} else if (criteria.getEmail() != null) {
|
||||
// Recherche par email
|
||||
users = usersResource.searchByEmail(
|
||||
criteria.getEmail(),
|
||||
true // exact match
|
||||
);
|
||||
} else {
|
||||
// Liste tous les utilisateurs
|
||||
users = usersResource.list(
|
||||
criteria.getOffset(),
|
||||
criteria.getPageSize()
|
||||
);
|
||||
}
|
||||
|
||||
// Filtrer selon les critères supplémentaires
|
||||
users = filterUsers(users, criteria);
|
||||
|
||||
// Convertir en DTOs
|
||||
List<UserDTO> userDTOs = UserMapper.toDTOList(users, realmName);
|
||||
|
||||
// Compter le total
|
||||
long totalCount = usersResource.count();
|
||||
|
||||
// Construire le résultat paginé
|
||||
return UserSearchResultDTO.of(userDTOs, criteria, totalCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la recherche d'utilisateurs", e);
|
||||
throw new RuntimeException("Impossible de rechercher les utilisateurs", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserDTO> getUserById(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Récupération de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
UserRepresentation userRep = userResource.toRepresentation();
|
||||
return Optional.of(UserMapper.toDTO(userRep, realmName));
|
||||
} catch (NotFoundException e) {
|
||||
log.warn("Utilisateur {} non trouvé dans le realm {}", userId, realmName);
|
||||
return Optional.empty();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération de l'utilisateur {}", userId, e);
|
||||
throw new RuntimeException("Impossible de récupérer l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserDTO> getUserByUsername(@NotBlank String username, @NotBlank String realmName) {
|
||||
log.info("Récupération de l'utilisateur par username {} dans le realm {}", username, realmName);
|
||||
|
||||
try {
|
||||
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
|
||||
.search(username, 0, 1, true);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(UserMapper.toDTO(users.get(0), realmName));
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération de l'utilisateur par username {}", username, e);
|
||||
throw new RuntimeException("Impossible de récupérer l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UserDTO> getUserByEmail(@NotBlank String email, @NotBlank String realmName) {
|
||||
log.info("Récupération de l'utilisateur par email {} dans le realm {}", email, realmName);
|
||||
|
||||
try {
|
||||
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
|
||||
.searchByEmail(email, true);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(UserMapper.toDTO(users.get(0), realmName));
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération de l'utilisateur par email {}", email, e);
|
||||
throw new RuntimeException("Impossible de récupérer l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDTO createUser(@Valid @NotNull UserDTO user, @NotBlank String realmName) {
|
||||
log.info("Création de l'utilisateur {} dans le realm {}", user.getUsername(), realmName);
|
||||
|
||||
try {
|
||||
// Vérifier si l'utilisateur existe déjà
|
||||
if (usernameExists(user.getUsername(), realmName)) {
|
||||
throw new IllegalArgumentException("Le nom d'utilisateur existe déjà: " + user.getUsername());
|
||||
}
|
||||
|
||||
if (user.getEmail() != null && emailExists(user.getEmail(), realmName)) {
|
||||
throw new IllegalArgumentException("L'email existe déjà: " + user.getEmail());
|
||||
}
|
||||
|
||||
// Convertir DTO vers UserRepresentation
|
||||
UserRepresentation userRep = UserMapper.toRepresentation(user);
|
||||
|
||||
// Créer l'utilisateur
|
||||
UsersResource usersResource = keycloakAdminClient.getUsers(realmName);
|
||||
var response = usersResource.create(userRep);
|
||||
|
||||
if (response.getStatus() != 201) {
|
||||
throw new RuntimeException("Échec de la création de l'utilisateur: " + response.getStatusInfo());
|
||||
}
|
||||
|
||||
// Récupérer l'ID de l'utilisateur créé
|
||||
String userId = response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1");
|
||||
|
||||
// Définir le mot de passe si fourni
|
||||
if (user.getTemporaryPassword() != null) {
|
||||
setPassword(userId, realmName, user.getTemporaryPassword(),
|
||||
user.getTemporaryPasswordFlag() != null && user.getTemporaryPasswordFlag());
|
||||
}
|
||||
|
||||
// Récupérer l'utilisateur créé
|
||||
UserResource userResource = usersResource.get(userId);
|
||||
UserRepresentation createdUser = userResource.toRepresentation();
|
||||
|
||||
log.info("✅ Utilisateur créé avec succès: {} (ID: {})", user.getUsername(), userId);
|
||||
return UserMapper.toDTO(createdUser, realmName);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la création de l'utilisateur {}", user.getUsername(), e);
|
||||
throw new RuntimeException("Impossible de créer l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDTO updateUser(@NotBlank String userId, @Valid @NotNull UserDTO user, @NotBlank String realmName) {
|
||||
log.info("Mise à jour de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
|
||||
// Récupérer l'utilisateur existant
|
||||
UserRepresentation existingUser = userResource.toRepresentation();
|
||||
|
||||
// Mettre à jour les champs
|
||||
if (user.getEmail() != null) {
|
||||
existingUser.setEmail(user.getEmail());
|
||||
}
|
||||
if (user.getPrenom() != null) {
|
||||
existingUser.setFirstName(user.getPrenom());
|
||||
}
|
||||
if (user.getNom() != null) {
|
||||
existingUser.setLastName(user.getNom());
|
||||
}
|
||||
if (user.getEnabled() != null) {
|
||||
existingUser.setEnabled(user.getEnabled());
|
||||
}
|
||||
if (user.getEmailVerified() != null) {
|
||||
existingUser.setEmailVerified(user.getEmailVerified());
|
||||
}
|
||||
if (user.getAttributes() != null) {
|
||||
existingUser.setAttributes(user.getAttributes());
|
||||
}
|
||||
|
||||
// Envoyer la mise à jour
|
||||
userResource.update(existingUser);
|
||||
|
||||
// Récupérer l'utilisateur mis à jour
|
||||
UserRepresentation updatedUser = userResource.toRepresentation();
|
||||
|
||||
log.info("✅ Utilisateur mis à jour avec succès: {}", userId);
|
||||
return UserMapper.toDTO(updatedUser, realmName);
|
||||
|
||||
} catch (NotFoundException e) {
|
||||
log.error("❌ Utilisateur {} non trouvé", userId);
|
||||
throw new RuntimeException("Utilisateur non trouvé", e);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la mise à jour de l'utilisateur {}", userId, e);
|
||||
throw new RuntimeException("Impossible de mettre à jour l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUser(@NotBlank String userId, @NotBlank String realmName, boolean hardDelete) {
|
||||
log.info("Suppression de l'utilisateur {} dans le realm {} (hard: {})", userId, realmName, hardDelete);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
|
||||
if (hardDelete) {
|
||||
// Suppression définitive
|
||||
userResource.remove();
|
||||
log.info("✅ Utilisateur supprimé définitivement: {}", userId);
|
||||
} else {
|
||||
// Soft delete: désactiver l'utilisateur
|
||||
UserRepresentation user = userResource.toRepresentation();
|
||||
user.setEnabled(false);
|
||||
userResource.update(user);
|
||||
log.info("✅ Utilisateur désactivé (soft delete): {}", userId);
|
||||
}
|
||||
|
||||
} catch (NotFoundException e) {
|
||||
log.error("❌ Utilisateur {} non trouvé", userId);
|
||||
throw new RuntimeException("Utilisateur non trouvé", e);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la suppression de l'utilisateur {}", userId, e);
|
||||
throw new RuntimeException("Impossible de supprimer l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activateUser(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Activation de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
UserRepresentation user = userResource.toRepresentation();
|
||||
user.setEnabled(true);
|
||||
userResource.update(user);
|
||||
|
||||
log.info("✅ Utilisateur activé: {}", userId);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de l'activation de l'utilisateur {}", userId, e);
|
||||
throw new RuntimeException("Impossible d'activer l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivateUser(@NotBlank String userId, @NotBlank String realmName, String raison) {
|
||||
log.info("Désactivation de l'utilisateur {} dans le realm {} (raison: {})", userId, realmName, raison);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
UserRepresentation user = userResource.toRepresentation();
|
||||
user.setEnabled(false);
|
||||
userResource.update(user);
|
||||
|
||||
log.info("✅ Utilisateur désactivé: {}", userId);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la désactivation de l'utilisateur {}", userId, e);
|
||||
throw new RuntimeException("Impossible de désactiver l'utilisateur", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void suspendUser(@NotBlank String userId, @NotBlank String realmName, String raison, int duree) {
|
||||
log.info("Suspension de l'utilisateur {} dans le realm {} (raison: {}, durée: {} jours)",
|
||||
userId, realmName, raison, duree);
|
||||
|
||||
deactivateUser(userId, realmName, raison);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlockUser(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Déverrouillage de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||
activateUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetPassword(@NotBlank String userId, @NotBlank String realmName,
|
||||
@NotBlank String temporaryPassword, boolean temporary) {
|
||||
log.info("Réinitialisation du mot de passe pour l'utilisateur {} (temporaire: {})", userId, temporary);
|
||||
|
||||
setPassword(userId, realmName, temporaryPassword, temporary);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendVerificationEmail(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Envoi d'un email de vérification à l'utilisateur {}", userId);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
userResource.sendVerifyEmail();
|
||||
|
||||
log.info("✅ Email de vérification envoyé: {}", userId);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de l'envoi de l'email de vérification pour {}", userId, e);
|
||||
throw new RuntimeException("Impossible d'envoyer l'email de vérification", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int logoutAllSessions(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Déconnexion de toutes les sessions pour l'utilisateur {}", userId);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
int sessionsCount = userResource.getUserSessions().size();
|
||||
userResource.logout();
|
||||
|
||||
log.info("✅ {} sessions révoquées pour l'utilisateur {}", sessionsCount, userId);
|
||||
return sessionsCount;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la déconnexion des sessions pour {}", userId, e);
|
||||
throw new RuntimeException("Impossible de déconnecter les sessions", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getActiveSessions(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Récupération des sessions actives pour l'utilisateur {}", userId);
|
||||
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
return userResource.getUserSessions().stream()
|
||||
.map(session -> session.getId())
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la récupération des sessions pour {}", userId, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countUsers(@NotNull UserSearchCriteriaDTO criteria) {
|
||||
try {
|
||||
return keycloakAdminClient.getUsers(criteria.getRealmName()).count();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du comptage des utilisateurs", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSearchResultDTO getAllUsers(@NotBlank String realmName, int page, int pageSize) {
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(realmName)
|
||||
.page(page)
|
||||
.pageSize(pageSize)
|
||||
.build();
|
||||
|
||||
return searchUsers(criteria);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usernameExists(@NotBlank String username, @NotBlank String realmName) {
|
||||
try {
|
||||
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
|
||||
.search(username, 0, 1, true);
|
||||
return !users.isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la vérification de l'existence du username {}", username, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean emailExists(@NotBlank String email, @NotBlank String realmName) {
|
||||
try {
|
||||
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
|
||||
.searchByEmail(email, true);
|
||||
return !users.isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la vérification de l'existence de l'email {}", email, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String exportUsersToCSV(@NotNull UserSearchCriteriaDTO criteria) {
|
||||
// TODO: Implémenter l'export CSV
|
||||
throw new UnsupportedOperationException("Export CSV non implémenté");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) {
|
||||
// TODO: Implémenter l'import CSV
|
||||
throw new UnsupportedOperationException("Import CSV non implémenté");
|
||||
}
|
||||
|
||||
// ==================== Méthodes privées ====================
|
||||
|
||||
private void setPassword(String userId, String realmName, String password, boolean temporary) {
|
||||
try {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
|
||||
CredentialRepresentation credential = new CredentialRepresentation();
|
||||
credential.setType(CredentialRepresentation.PASSWORD);
|
||||
credential.setValue(password);
|
||||
credential.setTemporary(temporary);
|
||||
|
||||
userResource.resetPassword(credential);
|
||||
|
||||
log.info("✅ Mot de passe défini pour l'utilisateur {} (temporaire: {})", userId, temporary);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la définition du mot de passe pour {}", userId, e);
|
||||
throw new RuntimeException("Impossible de définir le mot de passe", e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<UserRepresentation> filterUsers(List<UserRepresentation> users, UserSearchCriteriaDTO criteria) {
|
||||
return users.stream()
|
||||
.filter(user -> {
|
||||
// Filtrer par enabled
|
||||
if (criteria.getEnabled() != null && !criteria.getEnabled().equals(user.isEnabled())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer par emailVerified
|
||||
if (criteria.getEmailVerified() != null && !criteria.getEmailVerified().equals(user.isEmailVerified())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Ajouter d'autres filtres selon les besoins
|
||||
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# ============================================================================
|
||||
# Lions User Manager - Server Implementation Configuration - DEV
|
||||
# ============================================================================
|
||||
|
||||
# HTTP Configuration
|
||||
quarkus.http.port=8081
|
||||
quarkus.http.host=localhost
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=http://localhost:3000,http://localhost:8080
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS
|
||||
quarkus.http.cors.headers=*
|
||||
|
||||
# Keycloak OIDC Configuration (DEV)
|
||||
quarkus.oidc.auth-server-url=http://localhost:8180/realms/master
|
||||
quarkus.oidc.client-id=lions-user-manager
|
||||
quarkus.oidc.credentials.secret=dev-secret-change-me
|
||||
quarkus.oidc.tls.verification=none
|
||||
quarkus.oidc.application-type=service
|
||||
|
||||
# Keycloak Admin Client Configuration (DEV)
|
||||
lions.keycloak.server-url=http://localhost:8180
|
||||
lions.keycloak.admin-realm=master
|
||||
lions.keycloak.admin-client-id=admin-cli
|
||||
lions.keycloak.admin-username=admin
|
||||
lions.keycloak.admin-password=admin
|
||||
lions.keycloak.connection-pool-size=5
|
||||
lions.keycloak.timeout-seconds=30
|
||||
|
||||
# Realms autorisés (DEV)
|
||||
lions.keycloak.authorized-realms=btpxpress,master,lions-realm,test-realm
|
||||
|
||||
# Circuit Breaker Configuration (DEV - plus permissif)
|
||||
quarkus.smallrye-fault-tolerance.enabled=true
|
||||
|
||||
# Retry Configuration (DEV)
|
||||
lions.keycloak.retry.max-attempts=3
|
||||
lions.keycloak.retry.delay-seconds=1
|
||||
|
||||
# Audit Configuration (DEV)
|
||||
lions.audit.enabled=true
|
||||
lions.audit.log-to-database=false
|
||||
lions.audit.log-to-file=true
|
||||
lions.audit.retention-days=30
|
||||
|
||||
# Database Configuration (DEV - optionnel)
|
||||
# Décommenter pour utiliser une DB locale
|
||||
#quarkus.datasource.db-kind=postgresql
|
||||
#quarkus.datasource.username=postgres
|
||||
#quarkus.datasource.password=postgres
|
||||
#quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/lions_audit_dev
|
||||
#quarkus.hibernate-orm.database.generation=update
|
||||
#quarkus.flyway.migrate-at-start=false
|
||||
|
||||
# Logging Configuration (DEV)
|
||||
quarkus.log.level=DEBUG
|
||||
quarkus.log.category."dev.lions.user.manager".level=DEBUG
|
||||
quarkus.log.category."org.keycloak".level=INFO
|
||||
quarkus.log.category."io.quarkus".level=INFO
|
||||
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
|
||||
quarkus.log.console.color=true
|
||||
|
||||
# File Logging pour Audit (DEV)
|
||||
quarkus.log.file.enable=true
|
||||
quarkus.log.file.path=logs/dev/lions-user-manager.log
|
||||
quarkus.log.file.rotation.max-file-size=10M
|
||||
quarkus.log.file.rotation.max-backup-index=3
|
||||
|
||||
# OpenAPI/Swagger Configuration (DEV - toujours activé)
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.path=/swagger-ui
|
||||
quarkus.swagger-ui.enable=true
|
||||
|
||||
# Dev Services (activé en DEV)
|
||||
quarkus.devservices.enabled=false
|
||||
|
||||
# Security Configuration (DEV - plus permissif)
|
||||
quarkus.security.jaxrs.deny-unannotated-endpoints=false
|
||||
|
||||
# Hot Reload
|
||||
quarkus.live-reload.instrumentation=true
|
||||
@@ -0,0 +1,113 @@
|
||||
# ============================================================================
|
||||
# Lions User Manager - Server Implementation Configuration - PRODUCTION
|
||||
# ============================================================================
|
||||
|
||||
# HTTP Configuration
|
||||
quarkus.http.port=8081
|
||||
quarkus.http.host=0.0.0.0
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=https://btpxpress.lions.dev,https://admin.lions.dev
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS
|
||||
quarkus.http.cors.headers=*
|
||||
|
||||
# Keycloak OIDC Configuration (PROD)
|
||||
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/master
|
||||
quarkus.oidc.client-id=lions-user-manager
|
||||
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
|
||||
quarkus.oidc.tls.verification=required
|
||||
quarkus.oidc.application-type=service
|
||||
|
||||
# Keycloak Admin Client Configuration (PROD)
|
||||
lions.keycloak.server-url=https://security.lions.dev
|
||||
lions.keycloak.admin-realm=master
|
||||
lions.keycloak.admin-client-id=admin-cli
|
||||
lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME}
|
||||
lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD}
|
||||
lions.keycloak.connection-pool-size=20
|
||||
lions.keycloak.timeout-seconds=60
|
||||
|
||||
# Realms autorisés (PROD)
|
||||
lions.keycloak.authorized-realms=btpxpress,lions-realm
|
||||
|
||||
# Circuit Breaker Configuration (PROD - strict)
|
||||
quarkus.smallrye-fault-tolerance.enabled=true
|
||||
|
||||
# Retry Configuration (PROD)
|
||||
lions.keycloak.retry.max-attempts=5
|
||||
lions.keycloak.retry.delay-seconds=3
|
||||
|
||||
# Audit Configuration (PROD)
|
||||
lions.audit.enabled=true
|
||||
lions.audit.log-to-database=true
|
||||
lions.audit.log-to-file=true
|
||||
lions.audit.retention-days=365
|
||||
|
||||
# Database Configuration (PROD - obligatoire pour audit)
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.username=${DB_USERNAME:audit_user}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:lions-db.lions.svc.cluster.local}:${DB_PORT:5432}/${DB_NAME:lions_audit}
|
||||
quarkus.datasource.jdbc.max-size=20
|
||||
quarkus.datasource.jdbc.min-size=5
|
||||
quarkus.hibernate-orm.database.generation=none
|
||||
quarkus.flyway.migrate-at-start=true
|
||||
quarkus.flyway.baseline-on-migrate=true
|
||||
quarkus.flyway.baseline-version=1.0.0
|
||||
|
||||
# Logging Configuration (PROD)
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."dev.lions.user.manager".level=INFO
|
||||
quarkus.log.category."org.keycloak".level=WARN
|
||||
quarkus.log.category."io.quarkus".level=WARN
|
||||
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
|
||||
quarkus.log.console.json=true
|
||||
|
||||
# File Logging pour Audit (PROD)
|
||||
quarkus.log.file.enable=true
|
||||
quarkus.log.file.path=/var/log/lions/lions-user-manager.log
|
||||
quarkus.log.file.rotation.max-file-size=50M
|
||||
quarkus.log.file.rotation.max-backup-index=30
|
||||
quarkus.log.file.rotation.rotate-on-boot=false
|
||||
|
||||
# OpenAPI/Swagger Configuration (PROD - désactivé par défaut)
|
||||
quarkus.swagger-ui.always-include=false
|
||||
quarkus.swagger-ui.path=/swagger-ui
|
||||
quarkus.swagger-ui.enable=false
|
||||
|
||||
# Dev Services (désactivé en PROD)
|
||||
quarkus.devservices.enabled=false
|
||||
|
||||
# Security Configuration (PROD - strict)
|
||||
quarkus.security.jaxrs.deny-unannotated-endpoints=true
|
||||
|
||||
# Health Check Configuration (PROD)
|
||||
quarkus.smallrye-health.root-path=/health
|
||||
quarkus.smallrye-health.liveness-path=/health/live
|
||||
quarkus.smallrye-health.readiness-path=/health/ready
|
||||
|
||||
# Metrics Configuration (PROD)
|
||||
quarkus.micrometer.enabled=true
|
||||
quarkus.micrometer.export.prometheus.enabled=true
|
||||
quarkus.micrometer.export.prometheus.path=/metrics
|
||||
|
||||
# Jackson Configuration (PROD)
|
||||
quarkus.jackson.fail-on-unknown-properties=false
|
||||
quarkus.jackson.write-dates-as-timestamps=false
|
||||
quarkus.jackson.serialization-inclusion=non_null
|
||||
|
||||
# Performance tuning (PROD)
|
||||
quarkus.thread-pool.core-threads=2
|
||||
quarkus.thread-pool.max-threads=16
|
||||
quarkus.thread-pool.queue-size=100
|
||||
|
||||
# SSL/TLS Configuration (PROD)
|
||||
quarkus.http.ssl.certificate.key-store-file=${SSL_KEYSTORE_FILE:/etc/ssl/keystore.p12}
|
||||
quarkus.http.ssl.certificate.key-store-password=${SSL_KEYSTORE_PASSWORD}
|
||||
quarkus.http.ssl.certificate.key-store-file-type=PKCS12
|
||||
|
||||
# Monitoring & Observability
|
||||
quarkus.log.handler.gelf.enabled=false
|
||||
quarkus.log.handler.gelf.host=${GRAYLOG_HOST:logs.lions.dev}
|
||||
quarkus.log.handler.gelf.port=${GRAYLOG_PORT:12201}
|
||||
@@ -0,0 +1,100 @@
|
||||
# ============================================================================
|
||||
# Lions User Manager - Server Implementation Configuration
|
||||
# ============================================================================
|
||||
|
||||
# Application Info
|
||||
quarkus.application.name=lions-user-manager-server
|
||||
quarkus.application.version=1.0.0
|
||||
|
||||
# HTTP Configuration
|
||||
quarkus.http.port=8081
|
||||
quarkus.http.host=0.0.0.0
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=*
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS
|
||||
quarkus.http.cors.headers=*
|
||||
|
||||
# Keycloak OIDC Configuration
|
||||
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/master
|
||||
quarkus.oidc.client-id=lions-user-manager
|
||||
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET:your-client-secret}
|
||||
quarkus.oidc.tls.verification=none
|
||||
quarkus.oidc.application-type=service
|
||||
|
||||
# Keycloak Admin Client Configuration
|
||||
lions.keycloak.server-url=https://security.lions.dev
|
||||
lions.keycloak.admin-realm=master
|
||||
lions.keycloak.admin-client-id=admin-cli
|
||||
lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME:admin}
|
||||
lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD:admin}
|
||||
lions.keycloak.connection-pool-size=10
|
||||
lions.keycloak.timeout-seconds=30
|
||||
|
||||
# Realms autorisés (séparés par virgule)
|
||||
lions.keycloak.authorized-realms=btpxpress,master,lions-realm
|
||||
|
||||
# Circuit Breaker Configuration
|
||||
quarkus.smallrye-fault-tolerance.enabled=true
|
||||
|
||||
# Retry Configuration (pour appels Keycloak)
|
||||
lions.keycloak.retry.max-attempts=3
|
||||
lions.keycloak.retry.delay-seconds=2
|
||||
|
||||
# Audit Configuration
|
||||
lions.audit.enabled=true
|
||||
lions.audit.log-to-database=false
|
||||
lions.audit.log-to-file=true
|
||||
lions.audit.retention-days=90
|
||||
|
||||
# Database Configuration (optionnel - pour logs d'audit)
|
||||
# Décommenter si vous voulez persister les logs d'audit en DB
|
||||
#quarkus.datasource.db-kind=postgresql
|
||||
#quarkus.datasource.username=${DB_USERNAME:audit_user}
|
||||
#quarkus.datasource.password=${DB_PASSWORD:audit_pass}
|
||||
#quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:lions_audit}
|
||||
#quarkus.hibernate-orm.database.generation=none
|
||||
#quarkus.flyway.migrate-at-start=true
|
||||
|
||||
# Logging Configuration
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."dev.lions.user.manager".level=DEBUG
|
||||
quarkus.log.category."org.keycloak".level=WARN
|
||||
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
|
||||
|
||||
# File Logging pour Audit
|
||||
quarkus.log.file.enable=true
|
||||
quarkus.log.file.path=logs/lions-user-manager.log
|
||||
quarkus.log.file.rotation.max-file-size=10M
|
||||
quarkus.log.file.rotation.max-backup-index=10
|
||||
|
||||
# OpenAPI/Swagger Configuration
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.path=/swagger-ui
|
||||
mp.openapi.extensions.smallrye.info.title=Lions User Manager API
|
||||
mp.openapi.extensions.smallrye.info.version=1.0.0
|
||||
mp.openapi.extensions.smallrye.info.description=API de gestion centralisée des utilisateurs Keycloak
|
||||
mp.openapi.extensions.smallrye.info.contact.name=Lions Dev Team
|
||||
mp.openapi.extensions.smallrye.info.contact.email=contact@lions.dev
|
||||
|
||||
# Health Check Configuration
|
||||
quarkus.smallrye-health.root-path=/health
|
||||
quarkus.smallrye-health.liveness-path=/health/live
|
||||
quarkus.smallrye-health.readiness-path=/health/ready
|
||||
|
||||
# Metrics Configuration
|
||||
quarkus.micrometer.enabled=true
|
||||
quarkus.micrometer.export.prometheus.enabled=true
|
||||
quarkus.micrometer.export.prometheus.path=/metrics
|
||||
|
||||
# Security Configuration
|
||||
quarkus.security.jaxrs.deny-unannotated-endpoints=false
|
||||
|
||||
# Jackson Configuration
|
||||
quarkus.jackson.fail-on-unknown-properties=false
|
||||
quarkus.jackson.write-dates-as-timestamps=false
|
||||
quarkus.jackson.serialization-inclusion=non_null
|
||||
|
||||
# Dev Services (désactivé en production)
|
||||
quarkus.devservices.enabled=false
|
||||
Reference in New Issue
Block a user