diff --git a/lions-user-manager-server-api/.gitignore b/lions-user-manager-server-api/.gitignore deleted file mode 100644 index 8494236..0000000 --- a/lions-user-manager-server-api/.gitignore +++ /dev/null @@ -1,109 +0,0 @@ -# ============================================================================ -# Lions User Manager - Server API - .gitignore -# ============================================================================ - -# Maven -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -.mvn/wrapper/maven-wrapper.jar - -# Build artifacts -*.jar -*.war -*.ear -*.class -*.idx - -# Eclipse -.project -.classpath -.settings/ -.metadata/ -bin/ - -# IntelliJ IDEA -.idea/ -*.iml -*.iws -*.ipr -out/ - -# NetBeans -nbproject/ -nbbuild/ -nbdist/ -.nb-gradle/ - -# VS Code -.vscode/ -*.code-workspace - -# Mac -.DS_Store - -# Windows -Thumbs.db -ehthumbs.db -Desktop.ini - -# Logs -logs/ -*.log -*.log.* -hs_err_pid*.log - -# Temporary files -*.tmp -*.bak -*.swp -*~ -*.orig - -# Test files and reports -test_output*.txt -surefire-reports/ -failsafe-reports/ -*.dump -*.dumpstream - -# Test coverage -.jacoco/ -jacoco.exec -coverage/ -target/site/jacoco/ - -# Generated sources -generated-sources/ -generated-test-sources/ - -# Maven status -maven-status/ - -# Build metrics -build-metrics.json - -# IDE specific -*.sublime-project -*.sublime-workspace - -# OS specific -.DS_Store? -._* -.Spotlight-V100 -.Trashes - -# Lombok configuration (généré automatiquement) -lombok.config - -# Environment files -.env -.env.local -.env.*.local - diff --git a/lions-user-manager-server-api/lombok.config b/lions-user-manager-server-api/lombok.config new file mode 100644 index 0000000..2b45606 --- /dev/null +++ b/lions-user-manager-server-api/lombok.config @@ -0,0 +1,6 @@ +# This file configures Lombok for the project +# See https://projectlombok.org/features/configuration + +# Add @Generated annotation to all generated code +# This allows JaCoCo to automatically exclude Lombok-generated code from coverage +lombok.addLombokGeneratedAnnotation = true diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/AuditResourceApi.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/AuditResourceApi.java new file mode 100644 index 0000000..a9d04e2 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/AuditResourceApi.java @@ -0,0 +1,111 @@ +package dev.lions.user.manager.api; + +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.dto.common.CountDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +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; +import java.util.Map; + +/** + * Interface JAX-RS pour l'API d'audit. + */ +@Path("/api/audit") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Audit", description = "Consultation des logs d'audit et statistiques") +public interface AuditResourceApi { + + @POST + @Path("/search") + @Operation(summary = "Rechercher des logs d'audit", description = "Recherche avancée de logs selon critères") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Résultats de recherche", content = @Content(schema = @Schema(implementation = AuditLogDTO.class, type = org.eclipse.microprofile.openapi.annotations.enums.SchemaType.ARRAY))), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + List searchLogs( + @QueryParam("acteur") String acteurUsername, + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin, + @QueryParam("typeAction") TypeActionAudit typeAction, + @QueryParam("ressourceType") String ressourceType, + @QueryParam("succes") Boolean succes, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("pageSize") @DefaultValue("50") int pageSize); + + @GET + @Path("/actor/{acteurUsername}") + @Operation(summary = "Récupérer les logs d'un acteur", description = "Liste les derniers logs d'un utilisateur") + List getLogsByActor( + @Parameter(description = "Username de l'acteur") @PathParam("acteurUsername") String acteurUsername, + @Parameter(description = "Nombre de logs à retourner") @QueryParam("limit") @DefaultValue("100") int limit); + + @GET + @Path("/resource/{ressourceType}/{ressourceId}") + @Operation(summary = "Récupérer les logs d'une ressource", description = "Liste les derniers logs d'une ressource spécifique") + List getLogsByResource( + @PathParam("ressourceType") String ressourceType, + @PathParam("ressourceId") String ressourceId, + @QueryParam("limit") @DefaultValue("100") int limit); + + @GET + @Path("/action/{typeAction}") + @Operation(summary = "Récupérer les logs par type d'action", description = "Liste les logs d'un type d'action spécifique") + List getLogsByAction( + @PathParam("typeAction") TypeActionAudit typeAction, + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin, + @QueryParam("limit") @DefaultValue("100") int limit); + + @GET + @Path("/stats/actions") + @Operation(summary = "Statistiques par type d'action", description = "Retourne le nombre de logs par type d'action") + Map getActionStatistics( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @GET + @Path("/stats/users") + @Operation(summary = "Statistiques par utilisateur", description = "Retourne le nombre d'actions par utilisateur") + Map getUserActivityStatistics( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @GET + @Path("/stats/failures") + @Operation(summary = "Comptage des échecs", description = "Retourne le nombre d'échecs sur une période") + CountDTO getFailureCount( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @GET + @Path("/stats/success") + @Operation(summary = "Comptage des succès", description = "Retourne le nombre de succès sur une période") + CountDTO getSuccessCount( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @GET + @Path("/export/csv") + @Produces(MediaType.TEXT_PLAIN) + @Operation(summary = "Exporter les logs en CSV", description = "Génère un fichier CSV des logs d'audit") + Response exportLogsToCSV( + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin); + + @DELETE + @Path("/purge") + @Operation(summary = "Purger les anciens logs", description = "Supprime les logs de plus de X jours") + void purgeOldLogs( + @QueryParam("joursAnciennete") @DefaultValue("90") int joursAnciennete); +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RealmAssignmentResourceApi.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RealmAssignmentResourceApi.java new file mode 100644 index 0000000..5cde415 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RealmAssignmentResourceApi.java @@ -0,0 +1,103 @@ +package dev.lions.user.manager.api; + +import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO; +import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO; +import dev.lions.user.manager.dto.realm.RealmAssignmentDTO; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +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; + +/** + * Interface JAX-RS pour l'API de gestion des affectations de realms. + */ +@Path("/api/realm-assignments") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Realm Assignments", description = "Gestion des affectations de realms (contrôle d'accès multi-tenant)") +public interface RealmAssignmentResourceApi { + + @GET + @Operation(summary = "Lister toutes les affectations", description = "Liste toutes les affectations de realms") + List getAllAssignments(); + + @GET + @Path("/user/{userId}") + @Operation(summary = "Affectations par utilisateur", description = "Liste les realms assignés à un utilisateur") + List getAssignmentsByUser( + @Parameter(description = "ID de l'utilisateur") @PathParam("userId") String userId); + + @GET + @Path("/realm/{realmName}") + @Operation(summary = "Affectations par realm", description = "Liste les utilisateurs ayant accès à un realm") + List getAssignmentsByRealm( + @Parameter(description = "Nom du realm") @PathParam("realmName") String realmName); + + @GET + @Path("/{assignmentId}") + @Operation(summary = "Récupérer une affectation", description = "Récupère une affectation par son ID") + RealmAssignmentDTO getAssignmentById( + @Parameter(description = "ID de l'affectation") @PathParam("assignmentId") String assignmentId); + + @GET + @Path("/check") + @Operation(summary = "Vérifier l'accès", description = "Vérifie si un utilisateur peut gérer un realm") + RealmAccessCheckDTO canManageRealm( + @Parameter(description = "ID de l'utilisateur") @QueryParam("userId") String userId, + @Parameter(description = "Nom du realm") @QueryParam("realmName") String realmName); + + @GET + @Path("/authorized-realms/{userId}") + @Operation(summary = "Realms autorisés", description = "Liste les realms qu'un utilisateur peut gérer") + AuthorizedRealmsDTO getAuthorizedRealms( + @Parameter(description = "ID de l'utilisateur") @PathParam("userId") String userId); + + @POST + @Operation(summary = "Assigner un realm", description = "Assigne un realm à un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Affectation créée", content = @Content(schema = @Schema(implementation = RealmAssignmentDTO.class))) + }) + Response assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment); + + @DELETE + @Path("/user/{userId}/realm/{realmName}") + @Operation(summary = "Révoquer un realm", description = "Retire l'accès d'un utilisateur à un realm") + void revokeRealmFromUser( + @Parameter(description = "ID de l'utilisateur") @PathParam("userId") String userId, + @Parameter(description = "Nom du realm") @PathParam("realmName") String realmName); + + @DELETE + @Path("/user/{userId}") + @Operation(summary = "Révoquer tous les realms", description = "Retire tous les accès d'un utilisateur") + void revokeAllRealmsFromUser( + @Parameter(description = "ID de l'utilisateur") @PathParam("userId") String userId); + + @PUT + @Path("/{assignmentId}/deactivate") + @Operation(summary = "Désactiver une affectation", description = "Désactive une affectation sans la supprimer") + void deactivateAssignment( + @Parameter(description = "ID de l'affectation") @PathParam("assignmentId") String assignmentId); + + @PUT + @Path("/{assignmentId}/activate") + @Operation(summary = "Activer une affectation", description = "Réactive une affectation") + void activateAssignment( + @Parameter(description = "ID de l'affectation") @PathParam("assignmentId") String assignmentId); + + @PUT + @Path("/super-admin/{userId}") + @Operation(summary = "Définir super admin", description = "Définit ou retire le statut de super admin") + void setSuperAdmin( + @Parameter(description = "ID de l'utilisateur") @PathParam("userId") String userId, + @Parameter(description = "Super admin (true/false)") @QueryParam("superAdmin") @NotNull Boolean superAdmin); +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RealmResourceApi.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RealmResourceApi.java new file mode 100644 index 0000000..0b86bd3 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RealmResourceApi.java @@ -0,0 +1,44 @@ +package dev.lions.user.manager.api; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +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.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.List; + +/** + * Interface JAX-RS pour la gestion des realms Keycloak. + */ +@Path("/api/realms") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "Realms", description = "Gestion des realms Keycloak") +public interface RealmResourceApi { + + @GET + @Path("/list") + @Operation(summary = "Lister tous les realms", description = "Récupère la liste de tous les realms disponibles dans Keycloak") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des realms", content = @Content(schema = @Schema(type = org.eclipse.microprofile.openapi.annotations.enums.SchemaType.ARRAY, implementation = String.class))), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + List getAllRealms(); + + @GET + @Path("/{realm}/clients") + @Operation(summary = "Lister les clients d'un realm", description = "Récupère la liste des clientId de tous les clients d'un realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des clients"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + List getRealmClients( + @Parameter(description = "Nom du realm") @PathParam("realm") String realmName); +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RoleResourceApi.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RoleResourceApi.java new file mode 100644 index 0000000..3e4b60b --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/RoleResourceApi.java @@ -0,0 +1,163 @@ +package dev.lions.user.manager.api; + +import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +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; + +/** + * Interface JAX-RS pour la gestion des rôles Keycloak. + */ +@Path("/api/roles") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Roles", description = "Gestion des rôles Keycloak (realm et client)") +public interface RoleResourceApi { + + // ==================== 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))) + }) + Response createRealmRole( + @Valid @NotNull RoleDTO roleDTO, + @Parameter(description = "Nom du realm") @QueryParam("realm") String realmName); + + @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") + RoleDTO getRealmRole( + @Parameter(description = "Nom du rôle") @PathParam("roleName") String roleName, + @Parameter(description = "Nom du realm") @QueryParam("realm") String realmName); + + @GET + @Path("/realm") + @Operation(summary = "Lister tous les rôles realm", description = "Liste tous les rôles du realm") + List getAllRealmRoles( + @Parameter(description = "Nom du realm") @QueryParam("realm") String realmName); + + @PUT + @Path("/realm/{roleName}") + @Operation(summary = "Mettre à jour un rôle realm", description = "Met à jour les informations d'un rôle realm") + RoleDTO updateRealmRole( + @PathParam("roleName") String roleName, + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") String realmName); + + @DELETE + @Path("/realm/{roleName}") + @Operation(summary = "Supprimer un rôle realm", description = "Supprime un rôle realm") + void deleteRealmRole( + @PathParam("roleName") String roleName, + @QueryParam("realm") String realmName); + + // ==================== 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))) + }) + Response createClientRole( + @PathParam("clientId") String clientId, + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") String realmName); + + @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") + RoleDTO getClientRole( + @PathParam("clientId") String clientId, + @PathParam("roleName") String roleName, + @QueryParam("realm") String realmName); + + @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") + List getAllClientRoles( + @PathParam("clientId") String clientId, + @QueryParam("realm") String realmName); + + @DELETE + @Path("/client/{clientId}/{roleName}") + @Operation(summary = "Supprimer un rôle client", description = "Supprime un rôle d'un client") + void deleteClientRole( + @PathParam("clientId") String clientId, + @PathParam("roleName") String roleName, + @QueryParam("realm") String realmName); + + // ==================== 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") + void assignRealmRoles( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request); + + @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") + void revokeRealmRoles( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request); + + @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") + void assignClientRoles( + @PathParam("clientId") String clientId, + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request); + + @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") + List getUserRealmRoles( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName); + + @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") + List getUserClientRoles( + @PathParam("clientId") String clientId, + @PathParam("userId") String userId, + @QueryParam("realm") String realmName); + + // ==================== 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") + void addComposites( + @PathParam("roleName") String roleName, + @QueryParam("realm") String realmName, + @NotNull RoleAssignmentRequestDTO request); + + @GET + @Path("/composite/{roleName}") + @Operation(summary = "Récupérer les rôles composites", description = "Liste tous les rôles composites d'un rôle") + List getComposites( + @PathParam("roleName") String roleName, + @QueryParam("realm") String realmName); +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/SyncResourceApi.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/SyncResourceApi.java new file mode 100644 index 0000000..45d4670 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/SyncResourceApi.java @@ -0,0 +1,54 @@ +package dev.lions.user.manager.api; + +import dev.lions.user.manager.dto.sync.HealthStatusDTO; +import dev.lions.user.manager.dto.sync.SyncConsistencyDTO; +import dev.lions.user.manager.dto.sync.SyncHistoryDTO; +import dev.lions.user.manager.dto.sync.SyncResultDTO; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * Interface JAX-RS définissant le contrat de l'API de synchronisation. + * Cette interface doit être implémentée par le serveur et utilisée par le + * client (via MicroProfile Rest Client). + */ +@Path("/api/sync") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Sync", description = "Synchronisation avec Keycloak et health checks") +public interface SyncResourceApi { + + @GET + @Path("/health/keycloak") + @Operation(summary = "Vérifier la santé de Keycloak", description = "Retourne le statut de santé de Keycloak") + HealthStatusDTO checkKeycloakHealth(); + + @POST + @Path("/users") + @Operation(summary = "Synchroniser les utilisateurs", description = "Synchronise tous les utilisateurs depuis Keycloak") + SyncResultDTO syncUsers(@QueryParam("realm") String realmName); + + @POST + @Path("/roles") + @Operation(summary = "Synchroniser les rôles", description = "Synchronise les rôles (realm et/ou client)") + SyncResultDTO syncRoles( + @QueryParam("realm") String realmName, + @QueryParam("clientName") String clientName); + + @GET + @Path("/consistency") + @Operation(summary = "Vérifier la cohérence des données", description = "Compare le cache local et Keycloak pour un realm") + SyncConsistencyDTO checkDataConsistency(@QueryParam("realm") String realmName); + + @GET + @Path("/status") + @Operation(summary = "Statut de la dernière synchro", description = "Retourne le statut de la dernière synchronisation du realm") + SyncHistoryDTO getLastSyncStatus(@QueryParam("realm") String realmName); + + @POST + @Path("/force") + @Operation(summary = "Forcer une synchro", description = "Lance une synchronisation complète du realm (users + roles)") + SyncHistoryDTO forceSyncRealm(@QueryParam("realm") String realmName); +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/UserMetricsResourceApi.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/UserMetricsResourceApi.java new file mode 100644 index 0000000..90e5d80 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/UserMetricsResourceApi.java @@ -0,0 +1,32 @@ +package dev.lions.user.manager.api; + +import dev.lions.user.manager.dto.common.UserSessionStatsDTO; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * API de métriques liées aux utilisateurs (sessions, utilisateurs en ligne, etc.). + */ +@Path("/api/metrics/users") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "User Metrics", description = "Statistiques sur les utilisateurs et leurs sessions") +public interface UserMetricsResourceApi { + + @GET + @Path("/sessions") + @Operation(summary = "Statistiques de sessions utilisateurs", + description = "Retourne des statistiques agrégées sur les utilisateurs et leurs sessions pour un realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Statistiques retournées avec succès"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + UserSessionStatsDTO getUserSessionStats(@QueryParam("realm") String realmName); +} + diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/UserResourceApi.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/UserResourceApi.java new file mode 100644 index 0000000..723ad4d --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/api/UserResourceApi.java @@ -0,0 +1,134 @@ +package dev.lions.user.manager.api; + +import dev.lions.user.manager.dto.importexport.ImportResultDTO; +import dev.lions.user.manager.dto.user.*; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +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; + +/** + * Interface JAX-RS pour la gestion des utilisateurs. + */ +@Path("/api/users") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Users", description = "Gestion des utilisateurs Keycloak") +public interface UserResourceApi { + + @POST + @Path("/search") + @Operation(summary = "Rechercher des utilisateurs", description = "Recherche d'utilisateurs selon des critères") + UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria); + + @GET + @Path("/{userId}") + @Operation(summary = "Récupérer un utilisateur par ID", description = "Récupère les détails d'un utilisateur") + UserDTO getUserById( + @Parameter(description = "ID de l'utilisateur") @PathParam("userId") String userId, + @Parameter(description = "Nom du realm") @QueryParam("realm") String realmName); + + @GET + @Operation(summary = "Lister tous les utilisateurs", description = "Liste paginée de tous les utilisateurs") + UserSearchResultDTO getAllUsers( + @QueryParam("realm") String realmName, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("pageSize") @DefaultValue("20") int pageSize); + + @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))) + }) + Response createUser( + @Valid @NotNull UserDTO user, + @QueryParam("realm") String realmName); + + @PUT + @Path("/{userId}") + @Operation(summary = "Mettre à jour un utilisateur", description = "Met à jour les informations d'un utilisateur") + UserDTO updateUser( + @PathParam("userId") String userId, + @Valid @NotNull UserDTO user, + @QueryParam("realm") String realmName); + + @DELETE + @Path("/{userId}") + @Operation(summary = "Supprimer un utilisateur", description = "Supprime un utilisateur (soft ou hard delete)") + void deleteUser( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + @QueryParam("hardDelete") @DefaultValue("false") boolean hardDelete); + + @POST + @Path("/{userId}/activate") + @Operation(summary = "Activer un utilisateur", description = "Active un utilisateur désactivé") + void activateUser( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName); + + @POST + @Path("/{userId}/deactivate") + @Operation(summary = "Désactiver un utilisateur", description = "Désactive un utilisateur") + void deactivateUser( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + @QueryParam("raison") String raison); + + @POST + @Path("/{userId}/reset-password") + @Operation(summary = "Réinitialiser le mot de passe", description = "Définit un nouveau mot de passe pour l'utilisateur") + void resetPassword( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName, + @NotNull PasswordResetRequestDTO request); + + @POST + @Path("/{userId}/send-verification-email") + @Operation(summary = "Envoyer email de vérification", description = "Envoie un email de vérification à l'utilisateur") + void sendVerificationEmail( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName); + + @POST + @Path("/{userId}/logout-sessions") + @Operation(summary = "Déconnecter toutes les sessions", description = "Révoque toutes les sessions actives de l'utilisateur") + SessionsRevokedDTO logoutAllSessions( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName); + + @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", content = @Content(schema = @Schema(type = org.eclipse.microprofile.openapi.annotations.enums.SchemaType.ARRAY, implementation = String.class))) + }) + List getActiveSessions( + @PathParam("userId") String userId, + @QueryParam("realm") String realmName); + + @GET + @Path("/export/csv") + @Produces("text/csv") + @Operation(summary = "Exporter les utilisateurs en CSV", description = "Exporte la liste des utilisateurs du realm au format CSV") + Response exportUsersToCSV(@QueryParam("realm") String realmName); + + @POST + @Path("/import/csv") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Importer des utilisateurs depuis un CSV", description = "Importe des utilisateurs à partir d'un fichier CSV") + ImportResultDTO importUsersFromCSV( + @QueryParam("realm") String realmName, + String csvContent); +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/ApiErrorDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/ApiErrorDTO.java new file mode 100644 index 0000000..79ae915 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/ApiErrorDTO.java @@ -0,0 +1,21 @@ +package dev.lions.user.manager.dto.common; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Réponse d'erreur standard") +public class ApiErrorDTO implements Serializable { + @Schema(description = "Message d'erreur") + private String message; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/CountDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/CountDTO.java new file mode 100644 index 0000000..de80897 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/CountDTO.java @@ -0,0 +1,21 @@ +package dev.lions.user.manager.dto.common; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Réponse de comptage générique") +public class CountDTO implements Serializable { + @Schema(description = "Nombre d'éléments") + private long count; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/UserSessionStatsDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/UserSessionStatsDTO.java new file mode 100644 index 0000000..01840e2 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/common/UserSessionStatsDTO.java @@ -0,0 +1,32 @@ +package dev.lions.user.manager.dto.common; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Statistiques de sessions et d'utilisateurs en ligne pour un realm") +public class UserSessionStatsDTO implements Serializable { + + @Schema(description = "Nom du realm concerné") + private String realmName; + + @Schema(description = "Nombre total d'utilisateurs dans le realm") + private long totalUsers; + + @Schema(description = "Nombre total de sessions actives (approximation)") + private long activeSessions; + + @Schema(description = "Nombre d'utilisateurs considérés comme en ligne (approximation)") + private long onlineUsers; +} + diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/realm/AuthorizedRealmsDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/realm/AuthorizedRealmsDTO.java new file mode 100644 index 0000000..3b3f92c --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/realm/AuthorizedRealmsDTO.java @@ -0,0 +1,25 @@ +package dev.lions.user.manager.dto.realm; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Liste des realms autorisés pour un utilisateur") +public class AuthorizedRealmsDTO implements Serializable { + @Schema(description = "Liste des realms (peut être vide si super admin)") + private List realms; + + @Schema(description = "L'utilisateur est super admin") + private boolean isSuperAdmin; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/realm/RealmAccessCheckDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/realm/RealmAccessCheckDTO.java new file mode 100644 index 0000000..e9ac4ab --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/realm/RealmAccessCheckDTO.java @@ -0,0 +1,27 @@ +package dev.lions.user.manager.dto.realm; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Réponse de vérification d'accès au realm") +public class RealmAccessCheckDTO implements Serializable { + @Schema(description = "L'utilisateur peut gérer le realm") + private boolean canManage; + + @Schema(description = "ID de l'utilisateur") + private String userId; + + @Schema(description = "Nom du realm") + private String realmName; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/role/RoleAssignmentRequestDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/role/RoleAssignmentRequestDTO.java new file mode 100644 index 0000000..e90fa46 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/role/RoleAssignmentRequestDTO.java @@ -0,0 +1,22 @@ +package dev.lions.user.manager.dto.role; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Requête d'attribution ou de révocation de rôles") +public class RoleAssignmentRequestDTO implements Serializable { + @Schema(description = "Liste des noms de rôles", required = true) + private List roleNames; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/sync/SyncConsistencyDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/sync/SyncConsistencyDTO.java new file mode 100644 index 0000000..ac6a1d1 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/sync/SyncConsistencyDTO.java @@ -0,0 +1,29 @@ +package dev.lions.user.manager.dto.sync; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * DTO rapport de cohérence entre données Keycloak et cache local. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SyncConsistencyDTO { + private String realmName; + private String status; // OK, MISMATCH, ERROR + private Integer usersKeycloakCount; + private Integer usersLocalCount; + private Set missingUsersInLocal; + private Set missingUsersInKeycloak; + private Integer rolesKeycloakCount; + private Integer rolesLocalCount; + private Set missingRolesInLocal; + private Set missingRolesInKeycloak; + private String error; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/sync/SyncHistoryDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/sync/SyncHistoryDTO.java new file mode 100644 index 0000000..ca19650 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/sync/SyncHistoryDTO.java @@ -0,0 +1,23 @@ +package dev.lions.user.manager.dto.sync; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SyncHistoryDTO { + private String id; + private String realmName; + private LocalDateTime syncDate; + private String syncType; // FULL, PARTIAL, USER, ROLE + private String status; // SUCCESS, FAILURE + private Integer itemsProcessed; + private Long durationMs; + private String errorMessage; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/user/PasswordResetRequestDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/user/PasswordResetRequestDTO.java new file mode 100644 index 0000000..870ad48 --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/user/PasswordResetRequestDTO.java @@ -0,0 +1,25 @@ +package dev.lions.user.manager.dto.user; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Requête de réinitialisation de mot de passe") +public class PasswordResetRequestDTO implements Serializable { + @Schema(description = "Nouveau mot de passe", required = true) + private String password; + + @Schema(description = "Indique si le mot de passe est temporaire", defaultValue = "true") + @Builder.Default + private boolean temporary = true; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/user/SessionsRevokedDTO.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/user/SessionsRevokedDTO.java new file mode 100644 index 0000000..15dee6f --- /dev/null +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/dto/user/SessionsRevokedDTO.java @@ -0,0 +1,21 @@ +package dev.lions.user.manager.dto.user; + +import io.quarkus.runtime.annotations.RegisterForReflection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +@Schema(description = "Réponse de révocation de sessions") +public class SessionsRevokedDTO implements Serializable { + @Schema(description = "Nombre de sessions révoquées") + private int count; +} diff --git a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/enums/audit/TypeActionAudit.java b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/enums/audit/TypeActionAudit.java index 08c21df..b2c9a5a 100644 --- a/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/enums/audit/TypeActionAudit.java +++ b/lions-user-manager-server-api/src/main/java/dev/lions/user/manager/enums/audit/TypeActionAudit.java @@ -24,6 +24,7 @@ public enum TypeActionAudit { USER_PASSWORD_RESET("Réinitialisation mot de passe", "USER", "PASSWORD_RESET"), USER_EMAIL_VERIFY("Vérification email", "USER", "EMAIL_VERIFY"), USER_FORCE_LOGOUT("Déconnexion forcée", "USER", "FORCE_LOGOUT"), + USER_IMPORT("Import utilisateurs CSV", "USER", "IMPORT"), // Ajout // Actions Rôle ROLE_CREATE("Création rôle", "ROLE", "CREATE"), @@ -41,8 +42,10 @@ public enum TypeActionAudit { GROUP_ADD_MEMBER("Ajout membre groupe", "GROUP", "ADD_MEMBER"), GROUP_REMOVE_MEMBER("Retrait membre groupe", "GROUP", "REMOVE_MEMBER"), - // Actions Realm - REALM_SYNC("Synchronisation realm", "REALM", "SYNC"), + // Actions Realm & Sync + REALM_SYNC("Synchronisation realm (Général)", "REALM", "SYNC"), + SYNC_USERS("Synchronisation utilisateurs", "REALM", "SYNC_USERS"), // Ajout + SYNC_ROLES("Synchronisation rôles", "REALM", "SYNC_ROLES"), // Ajout REALM_EXPORT("Export realm", "REALM", "EXPORT"), REALM_IMPORT("Import realm", "REALM", "IMPORT"), REALM_ASSIGN("Assignation realm", "REALM", "ASSIGN"), @@ -105,9 +108,9 @@ public enum TypeActionAudit { */ public boolean isCritical() { return this == USER_DELETE - || this == ROLE_DELETE - || this == USER_SUSPEND - || this == SESSION_REVOKE_ALL - || this == SYSTEM_RESTORE; + || this == ROLE_DELETE + || this == USER_SUSPEND + || this == SESSION_REVOKE_ALL + || this == SYSTEM_RESTORE; } }