feat: Module server-impl-quarkus initial
Module d'implémentation serveur pour lions-user-manager Contenu: - KeycloakAdminClient avec résilience (Circuit Breaker, Retry, Timeout) - UserServiceImpl (25+ méthodes) - RoleServiceImpl (20+ méthodes) - AuditServiceImpl (logging et statistiques) - UserResource, RoleResource (REST API) - Mappers (User, Role) - Health checks - Configurations dev/prod séparées Statut: 🔄 80% complété 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
206
pom.xml
Normal file
206
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/main/java/dev/lions/user/manager/mapper/RoleMapper.java
Normal file
76
src/main/java/dev/lions/user/manager/mapper/RoleMapper.java
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package dev.lions.user.manager.mapper;
|
||||||
|
|
||||||
|
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||||
|
import dev.lions.user.manager.enums.role.TypeRole;
|
||||||
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper pour convertir entre RoleDTO et Keycloak RoleRepresentation
|
||||||
|
*/
|
||||||
|
public class RoleMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une RoleRepresentation Keycloak en RoleDTO
|
||||||
|
*/
|
||||||
|
public static RoleDTO toDTO(RoleRepresentation roleRep, String realmName, TypeRole typeRole) {
|
||||||
|
if (roleRep == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoleDTO.builder()
|
||||||
|
.id(roleRep.getId())
|
||||||
|
.nom(roleRep.getName())
|
||||||
|
.description(roleRep.getDescription())
|
||||||
|
.typeRole(typeRole)
|
||||||
|
.realmName(realmName)
|
||||||
|
.composite(roleRep.isComposite() != null ? roleRep.isComposite() : false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit un RoleDTO en RoleRepresentation Keycloak
|
||||||
|
*/
|
||||||
|
public static RoleRepresentation toRepresentation(RoleDTO roleDTO) {
|
||||||
|
if (roleDTO == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleRepresentation roleRep = new RoleRepresentation();
|
||||||
|
roleRep.setId(roleDTO.getId());
|
||||||
|
roleRep.setName(roleDTO.getNom());
|
||||||
|
roleRep.setDescription(roleDTO.getDescription());
|
||||||
|
roleRep.setComposite(roleDTO.isComposite());
|
||||||
|
roleRep.setClientRole(roleDTO.getTypeRole() == TypeRole.CLIENT_ROLE);
|
||||||
|
|
||||||
|
return roleRep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une liste de RoleRepresentation en liste de RoleDTO
|
||||||
|
*/
|
||||||
|
public static List<RoleDTO> toDTOList(List<RoleRepresentation> roleReps, String realmName, TypeRole typeRole) {
|
||||||
|
if (roleReps == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleReps.stream()
|
||||||
|
.map(roleRep -> toDTO(roleRep, realmName, typeRole))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une liste de RoleDTO en liste de RoleRepresentation
|
||||||
|
*/
|
||||||
|
public static List<RoleRepresentation> toRepresentationList(List<RoleDTO> roleDTOs) {
|
||||||
|
if (roleDTOs == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleDTOs.stream()
|
||||||
|
.map(RoleMapper::toRepresentation)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/main/java/dev/lions/user/manager/mapper/UserMapper.java
Normal file
173
src/main/java/dev/lions/user/manager/mapper/UserMapper.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
509
src/main/java/dev/lions/user/manager/resource/RoleResource.java
Normal file
509
src/main/java/dev/lions/user/manager/resource/RoleResource.java
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
package dev.lions.user.manager.resource;
|
||||||
|
|
||||||
|
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||||
|
import dev.lions.user.manager.service.RoleService;
|
||||||
|
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 rôles Keycloak
|
||||||
|
* Endpoints pour les rôles realm, rôles client, et attributions
|
||||||
|
*/
|
||||||
|
@Path("/api/roles")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Tag(name = "Roles", description = "Gestion des rôles Keycloak (realm et client)")
|
||||||
|
@Slf4j
|
||||||
|
public class RoleResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RoleService roleService;
|
||||||
|
|
||||||
|
// ==================== Endpoints Realm Roles ====================
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/realm")
|
||||||
|
@Operation(summary = "Créer un rôle realm", description = "Crée un nouveau rôle au niveau du realm")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "201", description = "Rôle créé",
|
||||||
|
content = @Content(schema = @Schema(implementation = RoleDTO.class))),
|
||||||
|
@APIResponse(responseCode = "400", description = "Données invalides"),
|
||||||
|
@APIResponse(responseCode = "409", description = "Rôle existe déjà"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager"})
|
||||||
|
public Response createRealmRole(
|
||||||
|
@Valid @NotNull RoleDTO roleDTO,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}",
|
||||||
|
roleDTO.getNom(), realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
RoleDTO createdRole = roleService.createRealmRole(roleDTO, realmName);
|
||||||
|
return Response.status(Response.Status.CREATED).entity(createdRole).build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Données invalides lors de la création du rôle: {}", 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 du rôle realm", e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/realm/{roleName}")
|
||||||
|
@Operation(summary = "Récupérer un rôle realm par nom", description = "Récupère les détails d'un rôle realm")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Rôle trouvé",
|
||||||
|
content = @Content(schema = @Schema(implementation = RoleDTO.class))),
|
||||||
|
@APIResponse(responseCode = "404", description = "Rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager", "role_viewer"})
|
||||||
|
public Response getRealmRole(
|
||||||
|
@Parameter(description = "Nom du rôle") @PathParam("roleName") @NotBlank String roleName,
|
||||||
|
@Parameter(description = "Nom du realm") @QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return roleService.getRealmRoleByName(roleName, realmName)
|
||||||
|
.map(role -> Response.ok(role).build())
|
||||||
|
.orElse(Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity(new ErrorResponse("Rôle non trouvé"))
|
||||||
|
.build());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération du rôle realm {}", roleName, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/realm")
|
||||||
|
@Operation(summary = "Lister tous les rôles realm", description = "Liste tous les rôles du realm")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Liste des rôles"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager", "role_viewer"})
|
||||||
|
public Response getAllRealmRoles(
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("GET /api/roles/realm - realm: {}", realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<RoleDTO> roles = roleService.getAllRealmRoles(realmName);
|
||||||
|
return Response.ok(roles).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération des rôles realm", e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("/realm/{roleName}")
|
||||||
|
@Operation(summary = "Mettre à jour un rôle realm", description = "Met à jour les informations d'un rôle realm")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Rôle mis à jour",
|
||||||
|
content = @Content(schema = @Schema(implementation = RoleDTO.class))),
|
||||||
|
@APIResponse(responseCode = "404", description = "Rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager"})
|
||||||
|
public Response updateRealmRole(
|
||||||
|
@PathParam("roleName") @NotBlank String roleName,
|
||||||
|
@Valid @NotNull RoleDTO roleDTO,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
RoleDTO updatedRole = roleService.updateRealmRole(roleName, roleDTO, realmName);
|
||||||
|
return Response.ok(updatedRole).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la mise à jour du rôle realm {}", roleName, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/realm/{roleName}")
|
||||||
|
@Operation(summary = "Supprimer un rôle realm", description = "Supprime un rôle realm")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "204", description = "Rôle supprimé"),
|
||||||
|
@APIResponse(responseCode = "404", description = "Rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin"})
|
||||||
|
public Response deleteRealmRole(
|
||||||
|
@PathParam("roleName") @NotBlank String roleName,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleService.deleteRealmRole(roleName, realmName);
|
||||||
|
return Response.noContent().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la suppression du rôle realm {}", roleName, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Endpoints Client Roles ====================
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/client/{clientId}")
|
||||||
|
@Operation(summary = "Créer un rôle client", description = "Crée un nouveau rôle pour un client spécifique")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "201", description = "Rôle créé",
|
||||||
|
content = @Content(schema = @Schema(implementation = RoleDTO.class))),
|
||||||
|
@APIResponse(responseCode = "400", description = "Données invalides"),
|
||||||
|
@APIResponse(responseCode = "409", description = "Rôle existe déjà"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager"})
|
||||||
|
public Response createClientRole(
|
||||||
|
@PathParam("clientId") @NotBlank String clientId,
|
||||||
|
@Valid @NotNull RoleDTO roleDTO,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("POST /api/roles/client/{} - Création du rôle client dans le realm: {}",
|
||||||
|
clientId, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName);
|
||||||
|
return Response.status(Response.Status.CREATED).entity(createdRole).build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Données invalides lors de la création du rôle client: {}", 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 du rôle client", e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/client/{clientId}/{roleName}")
|
||||||
|
@Operation(summary = "Récupérer un rôle client par nom", description = "Récupère les détails d'un rôle client")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Rôle trouvé",
|
||||||
|
content = @Content(schema = @Schema(implementation = RoleDTO.class))),
|
||||||
|
@APIResponse(responseCode = "404", description = "Rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager", "role_viewer"})
|
||||||
|
public Response getClientRole(
|
||||||
|
@PathParam("clientId") @NotBlank String clientId,
|
||||||
|
@PathParam("roleName") @NotBlank String roleName,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return roleService.getClientRoleByName(roleName, clientId, realmName)
|
||||||
|
.map(role -> Response.ok(role).build())
|
||||||
|
.orElse(Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity(new ErrorResponse("Rôle client non trouvé"))
|
||||||
|
.build());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération du rôle client {}/{}", clientId, roleName, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/client/{clientId}")
|
||||||
|
@Operation(summary = "Lister tous les rôles d'un client", description = "Liste tous les rôles d'un client spécifique")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Liste des rôles"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager", "role_viewer"})
|
||||||
|
public Response getAllClientRoles(
|
||||||
|
@PathParam("clientId") @NotBlank String clientId,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<RoleDTO> roles = roleService.getAllClientRoles(clientId, realmName);
|
||||||
|
return Response.ok(roles).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération des rôles du client {}", clientId, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/client/{clientId}/{roleName}")
|
||||||
|
@Operation(summary = "Supprimer un rôle client", description = "Supprime un rôle d'un client")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "204", description = "Rôle supprimé"),
|
||||||
|
@APIResponse(responseCode = "404", description = "Rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin"})
|
||||||
|
public Response deleteClientRole(
|
||||||
|
@PathParam("clientId") @NotBlank String clientId,
|
||||||
|
@PathParam("roleName") @NotBlank String roleName,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleService.deleteClientRole(roleName, clientId, realmName);
|
||||||
|
return Response.noContent().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la suppression du rôle client {}/{}", clientId, roleName, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Endpoints Attribution de rôles ====================
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/assign/realm/{userId}")
|
||||||
|
@Operation(summary = "Attribuer des rôles realm à un utilisateur", description = "Assigne un ou plusieurs rôles realm à un utilisateur")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "204", description = "Rôles attribués"),
|
||||||
|
@APIResponse(responseCode = "404", description = "Utilisateur ou rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager"})
|
||||||
|
public Response assignRealmRoles(
|
||||||
|
@PathParam("userId") @NotBlank String userId,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName,
|
||||||
|
@NotNull RoleAssignmentRequest request
|
||||||
|
) {
|
||||||
|
log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.roleNames.size());
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleService.assignRealmRolesToUser(userId, request.roleNames, realmName);
|
||||||
|
return Response.noContent().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'attribution des rôles realm à l'utilisateur {}", userId, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/revoke/realm/{userId}")
|
||||||
|
@Operation(summary = "Révoquer des rôles realm d'un utilisateur", description = "Révoque un ou plusieurs rôles realm d'un utilisateur")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "204", description = "Rôles révoqués"),
|
||||||
|
@APIResponse(responseCode = "404", description = "Utilisateur ou rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager"})
|
||||||
|
public Response revokeRealmRoles(
|
||||||
|
@PathParam("userId") @NotBlank String userId,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName,
|
||||||
|
@NotNull RoleAssignmentRequest request
|
||||||
|
) {
|
||||||
|
log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.roleNames.size());
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleService.revokeRealmRolesFromUser(userId, request.roleNames, realmName);
|
||||||
|
return Response.noContent().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la révocation des rôles realm de l'utilisateur {}", userId, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/assign/client/{clientId}/{userId}")
|
||||||
|
@Operation(summary = "Attribuer des rôles client à un utilisateur", description = "Assigne un ou plusieurs rôles client à un utilisateur")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "204", description = "Rôles attribués"),
|
||||||
|
@APIResponse(responseCode = "404", description = "Utilisateur, client ou rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager"})
|
||||||
|
public Response assignClientRoles(
|
||||||
|
@PathParam("clientId") @NotBlank String clientId,
|
||||||
|
@PathParam("userId") @NotBlank String userId,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName,
|
||||||
|
@NotNull RoleAssignmentRequest request
|
||||||
|
) {
|
||||||
|
log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client",
|
||||||
|
clientId, userId, request.roleNames.size());
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleService.assignClientRolesToUser(userId, clientId, request.roleNames, realmName);
|
||||||
|
return Response.noContent().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'attribution des rôles client à l'utilisateur {}", userId, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/user/realm/{userId}")
|
||||||
|
@Operation(summary = "Récupérer les rôles realm d'un utilisateur", description = "Liste tous les rôles realm d'un utilisateur")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Liste des rôles"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager", "role_viewer"})
|
||||||
|
public Response getUserRealmRoles(
|
||||||
|
@PathParam("userId") @NotBlank String userId,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<RoleDTO> roles = roleService.getUserRealmRoles(userId, realmName);
|
||||||
|
return Response.ok(roles).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération des rôles realm de l'utilisateur {}", userId, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/user/client/{clientId}/{userId}")
|
||||||
|
@Operation(summary = "Récupérer les rôles client d'un utilisateur", description = "Liste tous les rôles client d'un utilisateur")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Liste des rôles"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager", "role_viewer"})
|
||||||
|
public Response getUserClientRoles(
|
||||||
|
@PathParam("clientId") @NotBlank String clientId,
|
||||||
|
@PathParam("userId") @NotBlank String userId,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<RoleDTO> roles = roleService.getUserClientRoles(userId, clientId, realmName);
|
||||||
|
return Response.ok(roles).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération des rôles client de l'utilisateur {}", userId, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Endpoints Rôles composites ====================
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/composite/{roleName}/add")
|
||||||
|
@Operation(summary = "Ajouter des rôles composites", description = "Ajoute des rôles composites à un rôle")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "204", description = "Composites ajoutés"),
|
||||||
|
@APIResponse(responseCode = "404", description = "Rôle non trouvé"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager"})
|
||||||
|
public Response addComposites(
|
||||||
|
@PathParam("roleName") @NotBlank String roleName,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName,
|
||||||
|
@NotNull RoleAssignmentRequest request
|
||||||
|
) {
|
||||||
|
log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.roleNames.size());
|
||||||
|
|
||||||
|
try {
|
||||||
|
roleService.addCompositesToRealmRole(roleName, request.roleNames, realmName);
|
||||||
|
return Response.noContent().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de l'ajout des composites au rôle {}", roleName, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/composite/{roleName}")
|
||||||
|
@Operation(summary = "Récupérer les rôles composites", description = "Liste tous les rôles composites d'un rôle")
|
||||||
|
@APIResponses({
|
||||||
|
@APIResponse(responseCode = "200", description = "Liste des composites"),
|
||||||
|
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||||
|
})
|
||||||
|
@RolesAllowed({"admin", "role_manager", "role_viewer"})
|
||||||
|
public Response getComposites(
|
||||||
|
@PathParam("roleName") @NotBlank String roleName,
|
||||||
|
@QueryParam("realm") @NotBlank String realmName
|
||||||
|
) {
|
||||||
|
log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<RoleDTO> composites = roleService.getCompositeRoles(roleName, realmName);
|
||||||
|
return Response.ok(composites).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération des composites du rôle {}", roleName, e);
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new ErrorResponse(e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DTOs internes ====================
|
||||||
|
|
||||||
|
@Schema(description = "Requête d'attribution/révocation de rôles")
|
||||||
|
public static class RoleAssignmentRequest {
|
||||||
|
@Schema(description = "Liste des noms de rôles", required = true)
|
||||||
|
public List<String> roleNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
406
src/main/java/dev/lions/user/manager/resource/UserResource.java
Normal file
406
src/main/java/dev/lions/user/manager/resource/UserResource.java
Normal file
@@ -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,371 @@
|
|||||||
|
package dev.lions.user.manager.service.impl;
|
||||||
|
|
||||||
|
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||||
|
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||||
|
import dev.lions.user.manager.service.AuditService;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation du service d'audit
|
||||||
|
*
|
||||||
|
* NOTES:
|
||||||
|
* - Cette implémentation utilise un stockage en mémoire pour le développement
|
||||||
|
* - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache)
|
||||||
|
* - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
@Slf4j
|
||||||
|
public class AuditServiceImpl implements AuditService {
|
||||||
|
|
||||||
|
// Stockage en mémoire (à remplacer par une DB en production)
|
||||||
|
private final Map<String, AuditLogDTO> auditLogs = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
|
||||||
|
boolean auditEnabled;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false")
|
||||||
|
boolean logToDatabase;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
|
||||||
|
if (!auditEnabled) {
|
||||||
|
log.debug("Audit désactivé, log ignoré");
|
||||||
|
return auditLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer un ID si nécessaire
|
||||||
|
if (auditLog.getId() == null) {
|
||||||
|
auditLog.setId(UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le timestamp si nécessaire
|
||||||
|
if (auditLog.getDateAction() == null) {
|
||||||
|
auditLog.setDateAction(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log structuré pour les systèmes de logging (Graylog, Elasticsearch, etc.)
|
||||||
|
log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}",
|
||||||
|
auditLog.getTypeAction(),
|
||||||
|
auditLog.getActeurUsername(),
|
||||||
|
auditLog.getRessourceType() + ":" + auditLog.getRessourceId(),
|
||||||
|
auditLog.isSucces(),
|
||||||
|
auditLog.getAdresseIp(),
|
||||||
|
auditLog.getDetails());
|
||||||
|
|
||||||
|
// Stocker en mémoire
|
||||||
|
auditLogs.put(auditLog.getId(), auditLog);
|
||||||
|
|
||||||
|
// TODO: Si logToDatabase = true, persister dans PostgreSQL via Panache
|
||||||
|
// Exemple:
|
||||||
|
// if (logToDatabase) {
|
||||||
|
// AuditLogEntity entity = AuditLogMapper.toEntity(auditLog);
|
||||||
|
// entity.persist();
|
||||||
|
// }
|
||||||
|
|
||||||
|
return auditLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logSuccess(@NotBlank String acteurUsername, @NotNull TypeActionAudit typeAction,
|
||||||
|
@NotBlank String ressourceType, @NotBlank String ressourceId,
|
||||||
|
String adresseIp, String details) {
|
||||||
|
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||||
|
.acteurUsername(acteurUsername)
|
||||||
|
.typeAction(typeAction)
|
||||||
|
.ressourceType(ressourceType)
|
||||||
|
.ressourceId(ressourceId)
|
||||||
|
.succes(true)
|
||||||
|
.adresseIp(adresseIp)
|
||||||
|
.details(details)
|
||||||
|
.dateAction(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
logAction(auditLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logFailure(@NotBlank String acteurUsername, @NotNull TypeActionAudit typeAction,
|
||||||
|
@NotBlank String ressourceType, @NotBlank String ressourceId,
|
||||||
|
String adresseIp, @NotBlank String messageErreur) {
|
||||||
|
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||||
|
.acteurUsername(acteurUsername)
|
||||||
|
.typeAction(typeAction)
|
||||||
|
.ressourceType(ressourceType)
|
||||||
|
.ressourceId(ressourceId)
|
||||||
|
.succes(false)
|
||||||
|
.adresseIp(adresseIp)
|
||||||
|
.messageErreur(messageErreur)
|
||||||
|
.dateAction(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
logAction(auditLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuditLogDTO> searchLogs(@NotBlank String acteurUsername, LocalDateTime dateDebut,
|
||||||
|
LocalDateTime dateFin, TypeActionAudit typeAction,
|
||||||
|
String ressourceType, Boolean succes,
|
||||||
|
int page, int pageSize) {
|
||||||
|
log.debug("Recherche de logs d'audit: acteur={}, dateDebut={}, dateFin={}, typeAction={}, succes={}",
|
||||||
|
acteurUsername, dateDebut, dateFin, typeAction, succes);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
// Filtre par acteur (si spécifié et non "*")
|
||||||
|
if (acteurUsername != null && !"*".equals(acteurUsername) &&
|
||||||
|
!acteurUsername.equals(log.getActeurUsername())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par date début
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par date fin
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par type d'action
|
||||||
|
if (typeAction != null && !typeAction.equals(log.getTypeAction())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par type de ressource
|
||||||
|
if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par succès/échec
|
||||||
|
if (succes != null && succes != log.isSucces()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) // Tri décroissant par date
|
||||||
|
.skip((long) page * pageSize)
|
||||||
|
.limit(pageSize)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuditLogDTO> getLogsByActeur(@NotBlank String acteurUsername, int limit) {
|
||||||
|
log.debug("Récupération des {} derniers logs de l'acteur: {}", limit, acteurUsername);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> acteurUsername.equals(log.getActeurUsername()))
|
||||||
|
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
|
||||||
|
.limit(limit)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuditLogDTO> getLogsByRessource(@NotBlank String ressourceType,
|
||||||
|
@NotBlank String ressourceId, int limit) {
|
||||||
|
log.debug("Récupération des {} derniers logs de la ressource: {}:{}",
|
||||||
|
limit, ressourceType, ressourceId);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> ressourceType.equals(log.getRessourceType()) &&
|
||||||
|
ressourceId.equals(log.getRessourceId()))
|
||||||
|
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
|
||||||
|
.limit(limit)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuditLogDTO> getLogsByAction(@NotNull TypeActionAudit typeAction,
|
||||||
|
LocalDateTime dateDebut, LocalDateTime dateFin,
|
||||||
|
int limit) {
|
||||||
|
log.debug("Récupération des {} logs de type: {} entre {} et {}",
|
||||||
|
limit, typeAction, dateDebut, dateFin);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
if (!typeAction.equals(log.getTypeAction())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction()))
|
||||||
|
.limit(limit)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<TypeActionAudit, Long> getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
|
log.debug("Calcul des statistiques d'actions entre {} et {}", dateDebut, dateFin);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
AuditLogDTO::getTypeAction,
|
||||||
|
Collectors.counting()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Long> getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
|
log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
AuditLogDTO::getActeurUsername,
|
||||||
|
Collectors.counting()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
|
log.debug("Comptage des échecs entre {} et {}", dateDebut, dateFin);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
if (log.isSucces()) {
|
||||||
|
return false; // On ne compte que les échecs
|
||||||
|
}
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
|
log.debug("Comptage des succès entre {} et {}", dateDebut, dateFin);
|
||||||
|
|
||||||
|
return auditLogs.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
if (!log.isSucces()) {
|
||||||
|
return false; // On ne compte que les succès
|
||||||
|
}
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> exportLogsToCSV(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
|
log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin);
|
||||||
|
|
||||||
|
List<String> csvLines = new ArrayList<>();
|
||||||
|
|
||||||
|
// En-tête CSV
|
||||||
|
csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur");
|
||||||
|
|
||||||
|
// Données
|
||||||
|
auditLogs.values().stream()
|
||||||
|
.filter(log -> {
|
||||||
|
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction()))
|
||||||
|
.forEach(log -> {
|
||||||
|
String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"",
|
||||||
|
log.getId(),
|
||||||
|
log.getDateAction(),
|
||||||
|
log.getActeurUsername(),
|
||||||
|
log.getTypeAction(),
|
||||||
|
log.getRessourceType(),
|
||||||
|
log.getRessourceId(),
|
||||||
|
log.isSucces(),
|
||||||
|
log.getAdresseIp() != null ? log.getAdresseIp() : "",
|
||||||
|
log.getDetails() != null ? log.getDetails().replace("\"", "\"\"") : "",
|
||||||
|
log.getMessageErreur() != null ? log.getMessageErreur().replace("\"", "\"\"") : ""
|
||||||
|
);
|
||||||
|
csvLines.add(csvLine);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Export CSV terminé: {} lignes", csvLines.size() - 1);
|
||||||
|
return csvLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void purgeOldLogs(int joursDAnc ienneté) {
|
||||||
|
log.info("Purge des logs d'audit de plus de {} jours", joursDAncienneté);
|
||||||
|
|
||||||
|
LocalDateTime dateLimit = LocalDateTime.now().minusDays(joursDAncienneté);
|
||||||
|
|
||||||
|
long beforeCount = auditLogs.size();
|
||||||
|
auditLogs.entrySet().removeIf(entry ->
|
||||||
|
entry.getValue().getDateAction().isBefore(dateLimit)
|
||||||
|
);
|
||||||
|
long afterCount = auditLogs.size();
|
||||||
|
|
||||||
|
log.info("Purge terminée: {} logs supprimés", beforeCount - afterCount);
|
||||||
|
|
||||||
|
// TODO: Si base de données utilisée, exécuter:
|
||||||
|
// DELETE FROM audit_log WHERE date_action < :dateLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Méthodes utilitaires ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nombre total de logs en mémoire
|
||||||
|
*/
|
||||||
|
public long getTotalCount() {
|
||||||
|
return auditLogs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vide tous les logs (ATTENTION: à utiliser uniquement en développement)
|
||||||
|
*/
|
||||||
|
public void clearAll() {
|
||||||
|
log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire");
|
||||||
|
auditLogs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
package dev.lions.user.manager.service.impl;
|
||||||
|
|
||||||
|
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||||
|
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
|
||||||
|
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||||
|
import dev.lions.user.manager.enums.role.TypeRole;
|
||||||
|
import dev.lions.user.manager.mapper.RoleMapper;
|
||||||
|
import dev.lions.user.manager.service.RoleService;
|
||||||
|
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.*;
|
||||||
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation du service de gestion des rôles Keycloak
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
@Slf4j
|
||||||
|
public class RoleServiceImpl implements RoleService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeycloakAdminClient keycloakAdminClient;
|
||||||
|
|
||||||
|
// ==================== CRUD Realm Roles ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RoleDTO createRealmRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String realmName) {
|
||||||
|
log.info("Création du rôle realm: {} dans le realm: {}", roleDTO.getNom(), realmName);
|
||||||
|
|
||||||
|
RolesResource rolesResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles();
|
||||||
|
|
||||||
|
// Vérifier si le rôle existe déjà
|
||||||
|
try {
|
||||||
|
rolesResource.get(roleDTO.getNom()).toRepresentation();
|
||||||
|
throw new IllegalArgumentException("Le rôle " + roleDTO.getNom() + " existe déjà");
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
// OK, le rôle n'existe pas
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO);
|
||||||
|
rolesResource.create(roleRep);
|
||||||
|
|
||||||
|
// Récupérer le rôle créé avec son ID
|
||||||
|
RoleRepresentation createdRole = rolesResource.get(roleDTO.getNom()).toRepresentation();
|
||||||
|
return RoleMapper.toDTO(createdRole, realmName, TypeRole.REALM_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<RoleDTO> getRealmRoleById(@NotBlank String roleId, @NotBlank String realmName) {
|
||||||
|
log.debug("Récupération du rôle realm par ID: {} dans le realm: {}", roleId, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
RolesResource rolesResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles();
|
||||||
|
|
||||||
|
// Keycloak ne permet pas de récupérer un rôle par ID directement, on doit lister tous les rôles
|
||||||
|
List<RoleRepresentation> roles = rolesResource.list();
|
||||||
|
return roles.stream()
|
||||||
|
.filter(r -> r.getId().equals(roleId))
|
||||||
|
.findFirst()
|
||||||
|
.map(r -> RoleMapper.toDTO(r, realmName, TypeRole.REALM_ROLE));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Erreur lors de la récupération du rôle realm {}", roleId, e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<RoleDTO> getRealmRoleByName(@NotBlank String roleName, @NotBlank String realmName) {
|
||||||
|
log.debug("Récupération du rôle realm par nom: {} dans le realm: {}", roleName, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
RoleRepresentation roleRep = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles()
|
||||||
|
.get(roleName)
|
||||||
|
.toRepresentation();
|
||||||
|
|
||||||
|
return Optional.of(RoleMapper.toDTO(roleRep, realmName, TypeRole.REALM_ROLE));
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle realm {} non trouvé dans le realm {}", roleName, realmName);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RoleDTO updateRealmRole(@NotBlank String roleName, @Valid @NotNull RoleDTO roleDTO,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.info("Mise à jour du rôle realm: {} dans le realm: {}", roleName, realmName);
|
||||||
|
|
||||||
|
RoleResource roleResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles()
|
||||||
|
.get(roleName);
|
||||||
|
|
||||||
|
RoleRepresentation roleRep = roleResource.toRepresentation();
|
||||||
|
|
||||||
|
// Mettre à jour uniquement les champs modifiables
|
||||||
|
if (roleDTO.getDescription() != null) {
|
||||||
|
roleRep.setDescription(roleDTO.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
roleResource.update(roleRep);
|
||||||
|
|
||||||
|
// Retourner le rôle mis à jour
|
||||||
|
return RoleMapper.toDTO(roleResource.toRepresentation(), realmName, TypeRole.REALM_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteRealmRole(@NotBlank String roleName, @NotBlank String realmName) {
|
||||||
|
log.info("Suppression du rôle realm: {} dans le realm: {}", roleName, realmName);
|
||||||
|
|
||||||
|
keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles()
|
||||||
|
.deleteRole(roleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RoleDTO> getAllRealmRoles(@NotBlank String realmName) {
|
||||||
|
log.debug("Récupération de tous les rôles realm du realm: {}", realmName);
|
||||||
|
|
||||||
|
List<RoleRepresentation> roleReps = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles()
|
||||||
|
.list();
|
||||||
|
|
||||||
|
return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== CRUD Client Roles ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RoleDTO createClientRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String clientId,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.info("Création du rôle client: {} pour le client: {} dans le realm: {}",
|
||||||
|
roleDTO.getNom(), clientId, realmName);
|
||||||
|
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
// Trouver le client par clientId
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Client " + clientId + " non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
RolesResource rolesResource = clientsResource.get(internalClientId).roles();
|
||||||
|
|
||||||
|
// Vérifier si le rôle existe déjà
|
||||||
|
try {
|
||||||
|
rolesResource.get(roleDTO.getNom()).toRepresentation();
|
||||||
|
throw new IllegalArgumentException("Le rôle " + roleDTO.getNom() + " existe déjà pour ce client");
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
// OK, le rôle n'existe pas
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO);
|
||||||
|
rolesResource.create(roleRep);
|
||||||
|
|
||||||
|
// Récupérer le rôle créé
|
||||||
|
RoleRepresentation createdRole = rolesResource.get(roleDTO.getNom()).toRepresentation();
|
||||||
|
RoleDTO result = RoleMapper.toDTO(createdRole, realmName, TypeRole.CLIENT_ROLE);
|
||||||
|
result.setClientId(clientId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<RoleDTO> getClientRoleByName(@NotBlank String roleName, @NotBlank String clientId,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.debug("Récupération du rôle client: {} pour le client: {} dans le realm: {}",
|
||||||
|
roleName, clientId, realmName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
RoleRepresentation roleRep = clientsResource.get(internalClientId)
|
||||||
|
.roles()
|
||||||
|
.get(roleName)
|
||||||
|
.toRepresentation();
|
||||||
|
|
||||||
|
RoleDTO roleDTO = RoleMapper.toDTO(roleRep, realmName, TypeRole.CLIENT_ROLE);
|
||||||
|
roleDTO.setClientId(clientId);
|
||||||
|
|
||||||
|
return Optional.of(roleDTO);
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle client {} non trouvé pour le client {} dans le realm {}",
|
||||||
|
roleName, clientId, realmName);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteClientRole(@NotBlank String roleName, @NotBlank String clientId,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.info("Suppression du rôle client: {} pour le client: {} dans le realm: {}",
|
||||||
|
roleName, clientId, realmName);
|
||||||
|
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Client " + clientId + " non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
clientsResource.get(internalClientId).roles().deleteRole(roleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RoleDTO> getAllClientRoles(@NotBlank String clientId, @NotBlank String realmName) {
|
||||||
|
log.debug("Récupération de tous les rôles du client: {} dans le realm: {}", clientId, realmName);
|
||||||
|
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
List<RoleRepresentation> roleReps = clientsResource.get(internalClientId)
|
||||||
|
.roles()
|
||||||
|
.list();
|
||||||
|
|
||||||
|
List<RoleDTO> roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE);
|
||||||
|
roles.forEach(role -> role.setClientId(clientId));
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Attribution de rôles ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void assignRealmRolesToUser(@NotBlank String userId, @NotNull List<String> roleNames,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.info("Attribution de {} rôles realm à l'utilisateur {} dans le realm {}",
|
||||||
|
roleNames.size(), userId, realmName);
|
||||||
|
|
||||||
|
UserResource userResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId);
|
||||||
|
|
||||||
|
RolesResource rolesResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles();
|
||||||
|
|
||||||
|
List<RoleRepresentation> rolesToAssign = roleNames.stream()
|
||||||
|
.map(roleName -> {
|
||||||
|
try {
|
||||||
|
return rolesResource.get(roleName).toRepresentation();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle {} non trouvé, ignoré", roleName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(role -> role != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!rolesToAssign.isEmpty()) {
|
||||||
|
userResource.roles().realmLevel().add(rolesToAssign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void revokeRealmRolesFromUser(@NotBlank String userId, @NotNull List<String> roleNames,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.info("Révocation de {} rôles realm pour l'utilisateur {} dans le realm {}",
|
||||||
|
roleNames.size(), userId, realmName);
|
||||||
|
|
||||||
|
UserResource userResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId);
|
||||||
|
|
||||||
|
RolesResource rolesResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles();
|
||||||
|
|
||||||
|
List<RoleRepresentation> rolesToRevoke = roleNames.stream()
|
||||||
|
.map(roleName -> {
|
||||||
|
try {
|
||||||
|
return rolesResource.get(roleName).toRepresentation();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle {} non trouvé, ignoré", roleName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(role -> role != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!rolesToRevoke.isEmpty()) {
|
||||||
|
userResource.roles().realmLevel().remove(rolesToRevoke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void assignClientRolesToUser(@NotBlank String userId, @NotBlank String clientId,
|
||||||
|
@NotNull List<String> roleNames, @NotBlank String realmName) {
|
||||||
|
log.info("Attribution de {} rôles du client {} à l'utilisateur {} dans le realm {}",
|
||||||
|
roleNames.size(), clientId, userId, realmName);
|
||||||
|
|
||||||
|
UserResource userResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId);
|
||||||
|
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Client " + clientId + " non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
RolesResource rolesResource = clientsResource.get(internalClientId).roles();
|
||||||
|
|
||||||
|
List<RoleRepresentation> rolesToAssign = roleNames.stream()
|
||||||
|
.map(roleName -> {
|
||||||
|
try {
|
||||||
|
return rolesResource.get(roleName).toRepresentation();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle client {} non trouvé, ignoré", roleName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(role -> role != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!rolesToAssign.isEmpty()) {
|
||||||
|
userResource.roles().clientLevel(internalClientId).add(rolesToAssign);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void revokeClientRolesFromUser(@NotBlank String userId, @NotBlank String clientId,
|
||||||
|
@NotNull List<String> roleNames, @NotBlank String realmName) {
|
||||||
|
log.info("Révocation de {} rôles du client {} pour l'utilisateur {} dans le realm {}",
|
||||||
|
roleNames.size(), clientId, userId, realmName);
|
||||||
|
|
||||||
|
UserResource userResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId);
|
||||||
|
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Client " + clientId + " non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
RolesResource rolesResource = clientsResource.get(internalClientId).roles();
|
||||||
|
|
||||||
|
List<RoleRepresentation> rolesToRevoke = roleNames.stream()
|
||||||
|
.map(roleName -> {
|
||||||
|
try {
|
||||||
|
return rolesResource.get(roleName).toRepresentation();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle client {} non trouvé, ignoré", roleName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(role -> role != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!rolesToRevoke.isEmpty()) {
|
||||||
|
userResource.roles().clientLevel(internalClientId).remove(rolesToRevoke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RoleDTO> getUserRealmRoles(@NotBlank String userId, @NotBlank String realmName) {
|
||||||
|
log.debug("Récupération des rôles realm de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||||
|
|
||||||
|
List<RoleRepresentation> roleReps = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId)
|
||||||
|
.roles()
|
||||||
|
.realmLevel()
|
||||||
|
.listAll();
|
||||||
|
|
||||||
|
return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RoleDTO> getUserClientRoles(@NotBlank String userId, @NotBlank String clientId,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.debug("Récupération des rôles du client {} pour l'utilisateur {} dans le realm {}",
|
||||||
|
clientId, userId, realmName);
|
||||||
|
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
List<RoleRepresentation> roleReps = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId)
|
||||||
|
.roles()
|
||||||
|
.clientLevel(internalClientId)
|
||||||
|
.listAll();
|
||||||
|
|
||||||
|
List<RoleDTO> roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE);
|
||||||
|
roles.forEach(role -> role.setClientId(clientId));
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Rôles composites ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCompositesToRealmRole(@NotBlank String roleName, @NotNull List<String> compositeRoleNames,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.info("Ajout de {} rôles composites au rôle realm {} dans le realm {}",
|
||||||
|
compositeRoleNames.size(), roleName, realmName);
|
||||||
|
|
||||||
|
RoleResource roleResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles()
|
||||||
|
.get(roleName);
|
||||||
|
|
||||||
|
RolesResource rolesResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles();
|
||||||
|
|
||||||
|
List<RoleRepresentation> compositesToAdd = compositeRoleNames.stream()
|
||||||
|
.map(compositeName -> {
|
||||||
|
try {
|
||||||
|
return rolesResource.get(compositeName).toRepresentation();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle composite {} non trouvé, ignoré", compositeName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(role -> role != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!compositesToAdd.isEmpty()) {
|
||||||
|
roleResource.addComposites(compositesToAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeCompositesFromRealmRole(@NotBlank String roleName, @NotNull List<String> compositeRoleNames,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.info("Suppression de {} rôles composites du rôle realm {} dans le realm {}",
|
||||||
|
compositeRoleNames.size(), roleName, realmName);
|
||||||
|
|
||||||
|
RoleResource roleResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles()
|
||||||
|
.get(roleName);
|
||||||
|
|
||||||
|
RolesResource rolesResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles();
|
||||||
|
|
||||||
|
List<RoleRepresentation> compositesToRemove = compositeRoleNames.stream()
|
||||||
|
.map(compositeName -> {
|
||||||
|
try {
|
||||||
|
return rolesResource.get(compositeName).toRepresentation();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.warn("Rôle composite {} non trouvé, ignoré", compositeName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(role -> role != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!compositesToRemove.isEmpty()) {
|
||||||
|
roleResource.deleteComposites(compositesToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RoleDTO> getCompositeRoles(@NotBlank String roleName, @NotBlank String realmName) {
|
||||||
|
log.debug("Récupération des rôles composites du rôle {} dans le realm {}", roleName, realmName);
|
||||||
|
|
||||||
|
List<RoleRepresentation> composites = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.roles()
|
||||||
|
.get(roleName)
|
||||||
|
.getRoleComposites();
|
||||||
|
|
||||||
|
return RoleMapper.toDTOList(composites, realmName, TypeRole.COMPOSITE_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Vérification de permissions ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean userHasRealmRole(@NotBlank String userId, @NotBlank String roleName,
|
||||||
|
@NotBlank String realmName) {
|
||||||
|
log.debug("Vérification si l'utilisateur {} a le rôle realm {} dans le realm {}",
|
||||||
|
userId, roleName, realmName);
|
||||||
|
|
||||||
|
List<RoleRepresentation> userRoles = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId)
|
||||||
|
.roles()
|
||||||
|
.realmLevel()
|
||||||
|
.listEffective(); // Incluant les rôles hérités via composites
|
||||||
|
|
||||||
|
return userRoles.stream()
|
||||||
|
.anyMatch(role -> role.getName().equals(roleName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean userHasClientRole(@NotBlank String userId, @NotBlank String clientId,
|
||||||
|
@NotBlank String roleName, @NotBlank String realmName) {
|
||||||
|
log.debug("Vérification si l'utilisateur {} a le rôle client {} du client {} dans le realm {}",
|
||||||
|
userId, roleName, clientId, realmName);
|
||||||
|
|
||||||
|
ClientsResource clientsResource = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.clients();
|
||||||
|
|
||||||
|
List<org.keycloak.representations.idm.ClientRepresentation> clients =
|
||||||
|
clientsResource.findByClientId(clientId);
|
||||||
|
|
||||||
|
if (clients.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String internalClientId = clients.get(0).getId();
|
||||||
|
List<RoleRepresentation> userClientRoles = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId)
|
||||||
|
.roles()
|
||||||
|
.clientLevel(internalClientId)
|
||||||
|
.listEffective();
|
||||||
|
|
||||||
|
return userClientRoles.stream()
|
||||||
|
.anyMatch(role -> role.getName().equals(roleName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RoleDTO> getUserEffectiveRealmRoles(@NotBlank String userId, @NotBlank String realmName) {
|
||||||
|
log.debug("Récupération des rôles realm effectifs de l'utilisateur {} dans le realm {}",
|
||||||
|
userId, realmName);
|
||||||
|
|
||||||
|
List<RoleRepresentation> effectiveRoles = keycloakAdminClient.getInstance()
|
||||||
|
.realm(realmName)
|
||||||
|
.users()
|
||||||
|
.get(userId)
|
||||||
|
.roles()
|
||||||
|
.realmLevel()
|
||||||
|
.listEffective();
|
||||||
|
|
||||||
|
return RoleMapper.toDTOList(effectiveRoles, realmName, TypeRole.REALM_ROLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/main/resources/application-dev.properties
Normal file
82
src/main/resources/application-dev.properties
Normal file
@@ -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
|
||||||
113
src/main/resources/application-prod.properties
Normal file
113
src/main/resources/application-prod.properties
Normal file
@@ -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}
|
||||||
100
src/main/resources/application.properties
Normal file
100
src/main/resources/application.properties
Normal file
@@ -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
|
||||||
82
target/classes/application-dev.properties
Normal file
82
target/classes/application-dev.properties
Normal file
@@ -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
|
||||||
113
target/classes/application-prod.properties
Normal file
113
target/classes/application-prod.properties
Normal file
@@ -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}
|
||||||
100
target/classes/application.properties
Normal file
100
target/classes/application.properties
Normal file
@@ -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
|
||||||
Binary file not shown.
Binary file not shown.
BIN
target/classes/dev/lions/user/manager/mapper/RoleMapper.class
Normal file
BIN
target/classes/dev/lions/user/manager/mapper/RoleMapper.class
Normal file
Binary file not shown.
BIN
target/classes/dev/lions/user/manager/mapper/UserMapper.class
Normal file
BIN
target/classes/dev/lions/user/manager/mapper/UserMapper.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/lions-user-manager-server-impl-quarkus-1.0.0.jar
Normal file
BIN
target/lions-user-manager-server-impl-quarkus-1.0.0.jar
Normal file
Binary file not shown.
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
artifactId=lions-user-manager-server-impl-quarkus
|
||||||
|
groupId=dev.lions.user.manager
|
||||||
|
version=1.0.0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
dev\lions\user\manager\resource\UserResource$ErrorResponse.class
|
||||||
|
dev\lions\user\manager\service\impl\UserServiceImpl.class
|
||||||
|
dev\lions\user\manager\client\KeycloakAdminClient.class
|
||||||
|
dev\lions\user\manager\client\KeycloakAdminClientImpl.class
|
||||||
|
dev\lions\user\manager\resource\UserResource.class
|
||||||
|
dev\lions\user\manager\resource\KeycloakHealthCheck.class
|
||||||
|
dev\lions\user\manager\resource\UserResource$SessionsRevokedResponse.class
|
||||||
|
dev\lions\user\manager\mapper\UserMapper.class
|
||||||
|
dev\lions\user\manager\resource\HealthResourceEndpoint.class
|
||||||
|
dev\lions\user\manager\resource\UserResource$PasswordResetRequest.class
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\resource\KeycloakHealthCheck.java
|
||||||
|
C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\service\impl\UserServiceImpl.java
|
||||||
|
C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\resource\HealthResourceEndpoint.java
|
||||||
|
C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\resource\UserResource.java
|
||||||
|
C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\client\KeycloakAdminClient.java
|
||||||
|
C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\mapper\UserMapper.java
|
||||||
|
C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\client\KeycloakAdminClientImpl.java
|
||||||
Reference in New Issue
Block a user