From a5206eb7d93ca3643d9d44247d29007f26666ba4 Mon Sep 17 00:00:00 2001 From: dahoud Date: Tue, 7 Oct 2025 20:20:23 +0000 Subject: [PATCH] MAJOR: Complete OIDC migration - Remove JWT custom auth, integrate Keycloak OIDC - Remove quarkus-smallrye-jwt extension - Delete JwtService, AuthServiceImpl, AuthService interface - Replace AuthResource with OIDC-based implementation - Update SecurityService to use SecurityIdentity instead of JWT - Configure OIDC with Keycloak (bearer-only backend) - Add PostgreSQL configuration - Update entity relationships and database migrations - Remove JWT-related tests and configurations BREAKING CHANGE: Authentication now requires Keycloak OIDC tokens --- pom.xml | 2 +- .../com/gbcm/server/impl/entity/Client.java | 39 +- .../com/gbcm/server/impl/entity/Coach.java | 27 +- .../server/impl/resource/AuthResource.java | 425 +++++------------- .../server/impl/service/AuthServiceImpl.java | 213 --------- .../notification/EmailServiceSimple.java | 28 +- .../impl/service/security/JwtService.java | 313 ------------- .../service/security/PasswordService.java | 12 +- .../service/security/SecurityService.java | 61 +-- .../resources/application-local.properties | 5 +- src/main/resources/application.properties | 35 +- .../db/migration/V3__Create_coaches_table.sql | 4 +- .../V6__Add_status_column_to_users.sql | 14 + src/main/resources/import.sql | 90 ++-- .../impl/service/AuthServiceImplTest.java | 319 ------------- .../notification/EmailServiceSimpleTest.java | 20 +- 16 files changed, 352 insertions(+), 1255 deletions(-) delete mode 100644 src/main/java/com/gbcm/server/impl/service/AuthServiceImpl.java delete mode 100644 src/main/java/com/gbcm/server/impl/service/security/JwtService.java create mode 100644 src/main/resources/db/migration/V6__Add_status_column_to_users.sql delete mode 100644 src/test/java/com/gbcm/server/impl/service/AuthServiceImplTest.java diff --git a/pom.xml b/pom.xml index 1bf414f..984f3fc 100644 --- a/pom.xml +++ b/pom.xml @@ -85,7 +85,7 @@ io.quarkus - quarkus-smallrye-jwt + quarkus-oidc diff --git a/src/main/java/com/gbcm/server/impl/entity/Client.java b/src/main/java/com/gbcm/server/impl/entity/Client.java index 39d942e..55d3fe1 100644 --- a/src/main/java/com/gbcm/server/impl/entity/Client.java +++ b/src/main/java/com/gbcm/server/impl/entity/Client.java @@ -177,6 +177,18 @@ public class Client extends BaseEntity { @Size(max = 100, message = "La source d'acquisition ne peut pas dépasser 100 caractères") private String acquisitionSource; + /** + * Date de début de service. + */ + @Column(name = "service_start_date") + private LocalDate serviceStartDate; + + /** + * Date de fin de service. + */ + @Column(name = "service_end_date") + private LocalDate serviceEndDate; + /** * Constructeur par défaut. */ @@ -492,6 +504,22 @@ public class Client extends BaseEntity { this.acquisitionSource = acquisitionSource; } + public LocalDate getServiceStartDate() { + return serviceStartDate; + } + + public void setServiceStartDate(LocalDate serviceStartDate) { + this.serviceStartDate = serviceStartDate; + } + + public LocalDate getServiceEndDate() { + return serviceEndDate; + } + + public void setServiceEndDate(LocalDate serviceEndDate) { + this.serviceEndDate = serviceEndDate; + } + @Override public String toString() { return "Client{" + @@ -512,17 +540,22 @@ public class Client extends BaseEntity { * Prospect - pas encore client. */ PROSPECT, - + /** * Client actif. */ ACTIVE, - + /** * Client inactif. */ INACTIVE, - + + /** + * Client suspendu. + */ + SUSPENDED, + /** * Ancien client. */ diff --git a/src/main/java/com/gbcm/server/impl/entity/Coach.java b/src/main/java/com/gbcm/server/impl/entity/Coach.java index 28b22f2..7596aac 100644 --- a/src/main/java/com/gbcm/server/impl/entity/Coach.java +++ b/src/main/java/com/gbcm/server/impl/entity/Coach.java @@ -454,6 +454,22 @@ public class Coach extends BaseEntity { this.timezone = timezone; } + public String getTimeZone() { + return timezone; + } + + public void setTimeZone(String timeZone) { + this.timezone = timeZone; + } + + public String getLanguagesSpoken() { + return languages; + } + + public void setLanguagesSpoken(String languagesSpoken) { + this.languages = languagesSpoken; + } + public LocalDate getStartDate() { return startDate; } @@ -531,17 +547,22 @@ public class Coach extends BaseEntity { * Coach actif et disponible. */ ACTIVE, - + /** * Coach inactif temporairement. */ INACTIVE, - + /** * Coach en congé. */ ON_LEAVE, - + + /** + * Coach suspendu. + */ + SUSPENDED, + /** * Coach dont le contrat est terminé. */ diff --git a/src/main/java/com/gbcm/server/impl/resource/AuthResource.java b/src/main/java/com/gbcm/server/impl/resource/AuthResource.java index f3a7d1b..0432a83 100644 --- a/src/main/java/com/gbcm/server/impl/resource/AuthResource.java +++ b/src/main/java/com/gbcm/server/impl/resource/AuthResource.java @@ -1,114 +1,78 @@ package com.gbcm.server.impl.resource; +import java.util.HashMap; +import java.util.Map; + 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gbcm.server.api.dto.auth.LoginRequestDTO; -import com.gbcm.server.api.dto.auth.LoginResponseDTO; -import com.gbcm.server.api.dto.user.UserDTO; -import com.gbcm.server.api.exceptions.AuthenticationException; -import com.gbcm.server.api.exceptions.GBCMException; -import com.gbcm.server.api.interfaces.AuthService; - +import io.quarkus.oidc.OidcSession; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; /** - * Contrôleur REST pour les services d'authentification de la plateforme GBCM. - * Expose tous les endpoints d'authentification, autorisation et gestion des tokens. + * Contrôleur REST pour l'authentification OIDC avec Keycloak. + * Remplace l'ancien système JWT custom par une intégration Keycloak complète. * * @author GBCM Development Team - * @version 1.0 + * @version 2.0 - OIDC Keycloak * @since 1.0 */ @Path("/api/auth") @Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -@Tag(name = "Authentication", description = "Services d'authentification et autorisation GBCM") +@Tag(name = "Authentication", description = "Services d'authentification OIDC Keycloak pour GBCM") public class AuthResource { private static final Logger logger = LoggerFactory.getLogger(AuthResource.class); @Inject - AuthService authService; + SecurityIdentity securityIdentity; + + @Inject + OidcSession oidcSession; /** - * Endpoint de connexion utilisateur. - * - * @param loginRequest les informations de connexion - * @return la réponse de connexion avec token et informations utilisateur - * @throws AuthenticationException si l'authentification échoue - * @throws GBCMException si une erreur interne survient + * Endpoint d'information sur l'authentification. + * Redirige vers Keycloak pour l'authentification. */ - @POST - @Path("/login") + @GET + @Path("/info") @Operation( - summary = "Connexion utilisateur", - description = "Authentifie un utilisateur avec email et mot de passe et retourne un token JWT" + summary = "Informations d'authentification", + description = "Retourne les informations sur l'authentification OIDC Keycloak" ) @APIResponses({ - @APIResponse( - responseCode = "200", - description = "Connexion réussie", - content = @Content(schema = @Schema(implementation = LoginResponseDTO.class)) - ), - @APIResponse( - responseCode = "401", - description = "Identifiants invalides", - content = @Content(schema = @Schema(implementation = String.class)) - ), - @APIResponse( - responseCode = "400", - description = "Données de requête invalides", - content = @Content(schema = @Schema(implementation = String.class)) - ), - @APIResponse( - responseCode = "500", - description = "Erreur interne du serveur", - content = @Content(schema = @Schema(implementation = String.class)) - ) + @APIResponse(responseCode = "200", description = "Informations d'authentification"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") }) - public Response login( - @Parameter(description = "Informations de connexion", required = true) - @Valid LoginRequestDTO loginRequest - ) { + public Response getAuthInfo() { try { - logger.info("Demande de connexion reçue pour: {}", loginRequest.getEmail()); + Map authInfo = new HashMap<>(); + authInfo.put("authType", "OIDC"); + authInfo.put("provider", "Keycloak"); + authInfo.put("realm", "gbcm-llc"); + authInfo.put("authUrl", "http://localhost:8180/realms/gbcm-llc/protocol/openid-connect/auth"); + authInfo.put("tokenUrl", "http://localhost:8180/realms/gbcm-llc/protocol/openid-connect/token"); + authInfo.put("logoutUrl", "http://localhost:8180/realms/gbcm-llc/protocol/openid-connect/logout"); + authInfo.put("clientId", "gbcm-backend-api"); - LoginResponseDTO response = authService.login(loginRequest); + logger.debug("Informations d'authentification OIDC retournées"); + return Response.ok(authInfo).build(); - logger.info("Connexion réussie pour: {}", loginRequest.getEmail()); - return Response.ok(response).build(); - - } catch (AuthenticationException e) { - logger.warn("Échec d'authentification pour {}: {}", loginRequest.getEmail(), e.getMessage()); - return Response.status(Response.Status.UNAUTHORIZED) - .entity("Erreur d'authentification: " + e.getMessage()) - .build(); - } catch (GBCMException e) { - logger.error("Erreur GBCM lors de la connexion pour {}: {}", loginRequest.getEmail(), e.getMessage()); - return Response.status(Response.Status.BAD_REQUEST) - .entity("Erreur: " + e.getMessage()) - .build(); } catch (Exception e) { - logger.error("Erreur inattendue lors de la connexion pour {}: {}", loginRequest.getEmail(), e.getMessage()); + logger.error("Erreur lors de la récupération des informations d'authentification: {}", e.getMessage()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur interne du serveur") .build(); @@ -116,88 +80,87 @@ public class AuthResource { } /** - * Endpoint de déconnexion utilisateur. - * - * @param authToken le token d'authentification - * @return confirmation de déconnexion + * Endpoint de validation du token OIDC. + * Retourne les informations de l'utilisateur authentifié. + */ + @GET + @Path("/me") + @Authenticated + @Operation( + summary = "Informations utilisateur", + description = "Retourne les informations de l'utilisateur authentifié via OIDC" + ) + @APIResponses({ + @APIResponse(responseCode = "200", description = "Informations utilisateur"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getCurrentUser() { + try { + Map userInfo = new HashMap<>(); + userInfo.put("name", securityIdentity.getPrincipal().getName()); + userInfo.put("roles", securityIdentity.getRoles()); + userInfo.put("authenticated", securityIdentity.isAnonymous() == false); + + // Ajouter les claims OIDC si disponibles (filtrer les objets non sérialisables) + if (securityIdentity.getAttributes() != null) { + Map serializableAttributes = new HashMap<>(); + securityIdentity.getAttributes().forEach((key, value) -> { + if (value instanceof String || value instanceof Number || value instanceof Boolean) { + serializableAttributes.put(key, value); + } + }); + if (!serializableAttributes.isEmpty()) { + userInfo.put("attributes", serializableAttributes); + } + } + + logger.debug("Informations utilisateur retournées pour: {}", securityIdentity.getPrincipal().getName()); + return Response.ok(userInfo).build(); + + } catch (Exception e) { + logger.error("Erreur lors de la récupération des informations utilisateur: {}", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Erreur interne du serveur") + .build(); + } + } + + /** + * Endpoint de déconnexion OIDC. + * Invalide la session OIDC. */ @POST @Path("/logout") + @Authenticated @Operation( - summary = "Déconnexion utilisateur", - description = "Invalide le token d'authentification de l'utilisateur" + summary = "Déconnexion OIDC", + description = "Déconnecte l'utilisateur et invalide la session OIDC" ) @APIResponses({ @APIResponse(responseCode = "200", description = "Déconnexion réussie"), - @APIResponse(responseCode = "401", description = "Token invalide"), + @APIResponse(responseCode = "401", description = "Non authentifié"), @APIResponse(responseCode = "500", description = "Erreur interne du serveur") }) - public Response logout( - @Parameter(description = "Token d'authentification", required = true) - @HeaderParam("Authorization") String authToken - ) { + public Response logout() { try { - logger.info("Demande de déconnexion reçue"); + String userName = securityIdentity.getPrincipal().getName(); + logger.info("Demande de déconnexion OIDC pour: {}", userName); - authService.logout(authToken); + // Invalider la session OIDC + if (oidcSession != null) { + oidcSession.logout().await().indefinitely(); + } - logger.info("Déconnexion réussie"); - return Response.ok("Déconnexion réussie").build(); + Map response = new HashMap<>(); + response.put("message", "Déconnexion réussie"); + response.put("logoutUrl", "http://localhost:8180/realms/gbcm-llc/protocol/openid-connect/logout"); - } catch (AuthenticationException e) { - logger.warn("Échec de déconnexion: {}", e.getMessage()); - return Response.status(Response.Status.UNAUTHORIZED) - .entity("Erreur d'authentification: " + e.getMessage()) - .build(); - } catch (Exception e) { - logger.error("Erreur inattendue lors de la déconnexion: {}", e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("Erreur interne du serveur") - .build(); - } - } - - /** - * Endpoint de rafraîchissement de token. - * - * @param refreshToken le token de rafraîchissement - * @return nouveau token d'accès - */ - @POST - @Path("/refresh") - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Operation( - summary = "Rafraîchissement du token", - description = "Génère un nouveau token d'authentification à partir d'un refresh token" - ) - @APIResponses({ - @APIResponse( - responseCode = "200", - description = "Token rafraîchi", - content = @Content(schema = @Schema(implementation = LoginResponseDTO.class)) - ), - @APIResponse(responseCode = "401", description = "Token de rafraîchissement invalide"), - @APIResponse(responseCode = "500", description = "Erreur interne du serveur") - }) - public Response refreshToken( - @Parameter(description = "Token de rafraîchissement", required = true) - @FormParam("refreshToken") String refreshToken - ) { - try { - logger.info("Demande de rafraîchissement de token reçue"); - - LoginResponseDTO response = authService.refreshToken(refreshToken); - - logger.info("Token rafraîchi avec succès"); + logger.info("Déconnexion OIDC réussie pour: {}", userName); return Response.ok(response).build(); - } catch (AuthenticationException e) { - logger.warn("Échec de rafraîchissement de token: {}", e.getMessage()); - return Response.status(Response.Status.UNAUTHORIZED) - .entity("Erreur d'authentification: " + e.getMessage()) - .build(); } catch (Exception e) { - logger.error("Erreur inattendue lors du rafraîchissement: {}", e.getMessage()); + logger.error("Erreur lors de la déconnexion OIDC: {}", e.getMessage()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur interne du serveur") .build(); @@ -205,189 +168,33 @@ public class AuthResource { } /** - * Endpoint de validation de token. - * - * @param authToken le token d'authentification à valider - * @return les informations de l'utilisateur si le token est valide + * Endpoint de test pour les administrateurs. */ @GET - @Path("/validate") + @Path("/admin-test") + @RolesAllowed("ADMIN") @Operation( - summary = "Validation du token", - description = "Vérifie la validité d'un token d'authentification et retourne les informations utilisateur" + summary = "Test administrateur", + description = "Endpoint de test réservé aux administrateurs" ) @APIResponses({ - @APIResponse( - responseCode = "200", - description = "Token valide", - content = @Content(schema = @Schema(implementation = UserDTO.class)) - ), - @APIResponse(responseCode = "401", description = "Token invalide ou expiré"), + @APIResponse(responseCode = "200", description = "Accès autorisé"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Accès refusé"), @APIResponse(responseCode = "500", description = "Erreur interne du serveur") }) - public Response validateToken( - @Parameter(description = "Token d'authentification", required = true) - @HeaderParam("Authorization") String authToken - ) { + public Response adminTest() { try { - logger.debug("Demande de validation de token reçue"); + Map response = new HashMap<>(); + response.put("message", "Accès administrateur autorisé"); + response.put("user", securityIdentity.getPrincipal().getName()); + response.put("roles", securityIdentity.getRoles()); - UserDTO user = authService.validateToken(authToken); + logger.debug("Test administrateur réussi pour: {}", securityIdentity.getPrincipal().getName()); + return Response.ok(response).build(); - logger.debug("Token validé avec succès pour l'utilisateur: {}", user.getEmail()); - return Response.ok(user).build(); - - } catch (AuthenticationException e) { - logger.warn("Échec de validation de token: {}", e.getMessage()); - return Response.status(Response.Status.UNAUTHORIZED) - .entity("Erreur d'authentification: " + e.getMessage()) - .build(); } catch (Exception e) { - logger.error("Erreur inattendue lors de la validation: {}", e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("Erreur interne du serveur") - .build(); - } - } - - /** - * Endpoint de demande de réinitialisation de mot de passe. - * - * @param email l'adresse email de l'utilisateur - * @return confirmation d'envoi de l'email - */ - @POST - @Path("/forgot-password") - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Operation( - summary = "Mot de passe oublié", - description = "Envoie un email de réinitialisation de mot de passe à l'utilisateur" - ) - @APIResponses({ - @APIResponse(responseCode = "200", description = "Email envoyé"), - @APIResponse(responseCode = "400", description = "Email invalide"), - @APIResponse(responseCode = "500", description = "Erreur interne du serveur") - }) - public Response forgotPassword( - @Parameter(description = "Adresse email", required = true) - @FormParam("email") String email - ) { - try { - logger.info("Demande de réinitialisation de mot de passe pour: {}", email); - - authService.forgotPassword(email); - - logger.info("Email de réinitialisation envoyé pour: {}", email); - return Response.ok("Email de réinitialisation envoyé").build(); - - } catch (GBCMException e) { - logger.warn("Erreur lors de la demande de réinitialisation pour {}: {}", email, e.getMessage()); - return Response.status(Response.Status.BAD_REQUEST) - .entity("Erreur: " + e.getMessage()) - .build(); - } catch (Exception e) { - logger.error("Erreur inattendue lors de la demande de réinitialisation pour {}: {}", email, e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("Erreur interne du serveur") - .build(); - } - } - - /** - * Endpoint de réinitialisation de mot de passe. - * - * @param resetToken le token de réinitialisation - * @param newPassword le nouveau mot de passe - * @return confirmation de réinitialisation - */ - @POST - @Path("/reset-password") - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Operation( - summary = "Réinitialisation du mot de passe", - description = "Réinitialise le mot de passe avec un token de réinitialisation" - ) - @APIResponses({ - @APIResponse(responseCode = "200", description = "Mot de passe réinitialisé"), - @APIResponse(responseCode = "400", description = "Token invalide ou expiré"), - @APIResponse(responseCode = "500", description = "Erreur interne du serveur") - }) - public Response resetPassword( - @Parameter(description = "Token de réinitialisation", required = true) - @FormParam("resetToken") String resetToken, - @Parameter(description = "Nouveau mot de passe", required = true) - @FormParam("newPassword") String newPassword - ) { - try { - logger.info("Demande de réinitialisation de mot de passe avec token"); - - authService.resetPassword(resetToken, newPassword); - - logger.info("Mot de passe réinitialisé avec succès"); - return Response.ok("Mot de passe réinitialisé avec succès").build(); - - } catch (GBCMException e) { - logger.warn("Erreur lors de la réinitialisation: {}", e.getMessage()); - return Response.status(Response.Status.BAD_REQUEST) - .entity("Erreur: " + e.getMessage()) - .build(); - } catch (Exception e) { - logger.error("Erreur inattendue lors de la réinitialisation: {}", e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity("Erreur interne du serveur") - .build(); - } - } - - /** - * Endpoint de changement de mot de passe. - * - * @param authToken le token d'authentification - * @param oldPassword l'ancien mot de passe - * @param newPassword le nouveau mot de passe - * @return confirmation de changement - */ - @PUT - @Path("/change-password") - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Operation( - summary = "Changement de mot de passe", - description = "Change le mot de passe d'un utilisateur authentifié" - ) - @APIResponses({ - @APIResponse(responseCode = "200", description = "Mot de passe changé"), - @APIResponse(responseCode = "401", description = "Non autorisé"), - @APIResponse(responseCode = "400", description = "Ancien mot de passe incorrect"), - @APIResponse(responseCode = "500", description = "Erreur interne du serveur") - }) - public Response changePassword( - @Parameter(description = "Token d'authentification", required = true) - @HeaderParam("Authorization") String authToken, - @Parameter(description = "Ancien mot de passe", required = true) - @FormParam("oldPassword") String oldPassword, - @Parameter(description = "Nouveau mot de passe", required = true) - @FormParam("newPassword") String newPassword - ) { - try { - logger.info("Demande de changement de mot de passe"); - - authService.changePassword(authToken, oldPassword, newPassword); - - logger.info("Mot de passe changé avec succès"); - return Response.ok("Mot de passe changé avec succès").build(); - - } catch (AuthenticationException e) { - logger.warn("Échec de changement de mot de passe: {}", e.getMessage()); - return Response.status(Response.Status.UNAUTHORIZED) - .entity("Erreur d'authentification: " + e.getMessage()) - .build(); - } catch (GBCMException e) { - logger.warn("Erreur lors du changement de mot de passe: {}", e.getMessage()); - return Response.status(Response.Status.BAD_REQUEST) - .entity("Erreur: " + e.getMessage()) - .build(); - } catch (Exception e) { - logger.error("Erreur inattendue lors du changement de mot de passe: {}", e.getMessage()); + logger.error("Erreur lors du test administrateur: {}", e.getMessage()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("Erreur interne du serveur") .build(); diff --git a/src/main/java/com/gbcm/server/impl/service/AuthServiceImpl.java b/src/main/java/com/gbcm/server/impl/service/AuthServiceImpl.java deleted file mode 100644 index b22abcf..0000000 --- a/src/main/java/com/gbcm/server/impl/service/AuthServiceImpl.java +++ /dev/null @@ -1,213 +0,0 @@ -package com.gbcm.server.impl.service; - -import com.gbcm.server.api.dto.auth.LoginRequestDTO; -import com.gbcm.server.api.dto.auth.LoginResponseDTO; -import com.gbcm.server.api.dto.user.UserDTO; -import com.gbcm.server.api.exceptions.AuthenticationException; -import com.gbcm.server.api.exceptions.GBCMException; -import com.gbcm.server.api.interfaces.AuthService; -import com.gbcm.server.impl.entity.User; -import com.gbcm.server.impl.service.security.JwtService; -import com.gbcm.server.impl.service.security.PasswordService; -import com.gbcm.server.impl.service.security.SecurityService; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implémentation simplifiée du service d'authentification pour la plateforme GBCM. - * Version basique pour les tests et le développement initial. - * - * @author GBCM Development Team - * @version 1.0 - * @since 1.0 - */ -@ApplicationScoped -public class AuthServiceImpl implements AuthService { - - private static final Logger logger = LoggerFactory.getLogger(AuthServiceImpl.class); - - @Inject - JwtService jwtService; - - @Inject - PasswordService passwordService; - - @Inject - SecurityService securityService; - - /** - * {@inheritDoc} - */ - @Override - @Transactional - public LoginResponseDTO login(LoginRequestDTO loginRequest) throws AuthenticationException, GBCMException { - // Validation basique - if (loginRequest == null || loginRequest.getEmail() == null || loginRequest.getPassword() == null) { - throw new AuthenticationException("Email et mot de passe requis", "AUTH_INVALID_REQUEST"); - } - - logger.info("SIMULATION - Tentative de connexion pour: {}", loginRequest.getEmail()); - - // Rechercher l'utilisateur - User user = User.findByEmail(loginRequest.getEmail().trim().toLowerCase()); - if (user == null) { - logger.warn("Utilisateur non trouvé: {}", loginRequest.getEmail()); - throw new AuthenticationException("Email ou mot de passe incorrect", "AUTH_INVALID_CREDENTIALS"); - } - - // Vérifier le mot de passe - if (!passwordService.verifyPassword(loginRequest.getPassword(), user.getPasswordHash())) { - logger.warn("Mot de passe incorrect pour: {}", loginRequest.getEmail()); - throw new AuthenticationException("Email ou mot de passe incorrect", "AUTH_INVALID_CREDENTIALS"); - } - - // Générer le token - String token = jwtService.generateAccessToken(user).getToken(); - - // Créer la réponse - LoginResponseDTO response = new LoginResponseDTO(); - response.setToken(token); - response.setRefreshToken("refresh_" + token.substring(0, 20)); // Token de refresh simulé - - // Créer le DTO utilisateur - UserDTO userDTO = new UserDTO(); - userDTO.setId(user.getId()); - userDTO.setFirstName(user.getFirstName()); - userDTO.setLastName(user.getLastName()); - userDTO.setEmail(user.getEmail()); - userDTO.setRole(user.getRole()); - userDTO.setActive(user.isActive()); - response.setUser(userDTO); - - logger.info("Connexion réussie pour: {}", user.getEmail()); - return response; - } - - /** - * {@inheritDoc} - */ - @Override - @Transactional - public void logout(String authToken) throws AuthenticationException { - logger.info("SIMULATION - Déconnexion utilisateur"); - // Dans une vraie implémentation, on invaliderait le token - } - - /** - * {@inheritDoc} - */ - @Override - @Transactional - public LoginResponseDTO refreshToken(String refreshToken) throws AuthenticationException { - logger.info("SIMULATION - Rafraîchissement de token"); - - if (refreshToken == null || refreshToken.trim().isEmpty()) { - throw new AuthenticationException("Refresh token requis", "AUTH_REFRESH_TOKEN_REQUIRED"); - } - - // Simulation - dans une vraie implémentation, on validerait le refresh token - // et on générerait un nouveau token d'accès - LoginResponseDTO response = new LoginResponseDTO(); - response.setToken("new_access_token_" + System.currentTimeMillis()); - response.setRefreshToken(refreshToken); // Garder le même refresh token - - logger.info("Token rafraîchi avec succès"); - return response; - } - - /** - * {@inheritDoc} - */ - @Override - public UserDTO validateToken(String authToken) throws AuthenticationException { - logger.info("SIMULATION - Validation de token"); - - if (authToken == null || authToken.trim().isEmpty()) { - throw new AuthenticationException("Token requis", "AUTH_TOKEN_REQUIRED"); - } - - // Simulation - retourner un utilisateur fictif - UserDTO userDTO = new UserDTO(); - userDTO.setId(1L); - userDTO.setFirstName("Admin"); - userDTO.setLastName("GBCM"); - userDTO.setEmail("admin@gbcm.com"); - userDTO.setRole(com.gbcm.server.api.enums.UserRole.ADMIN); - userDTO.setActive(true); - - return userDTO; - } - - /** - * {@inheritDoc} - */ - @Override - @Transactional - public void forgotPassword(String email) throws GBCMException { - logger.info("SIMULATION - Demande de réinitialisation pour: {}", email); - - if (email == null || email.trim().isEmpty()) { - throw new GBCMException("Email requis", "AUTH_EMAIL_REQUIRED"); - } - - // Simulation - dans une vraie implémentation, on enverrait un email - logger.info("Email de réinitialisation simulé envoyé à: {}", email); - } - - /** - * {@inheritDoc} - */ - @Override - @Transactional - public void resetPassword(String resetToken, String newPassword) throws GBCMException { - logger.info("SIMULATION - Réinitialisation de mot de passe"); - - if (resetToken == null || resetToken.trim().isEmpty()) { - throw new GBCMException("Token de réinitialisation requis", "AUTH_RESET_TOKEN_REQUIRED"); - } - - if (newPassword == null || newPassword.trim().isEmpty()) { - throw new GBCMException("Nouveau mot de passe requis", "AUTH_NEW_PASSWORD_REQUIRED"); - } - - // Validation du mot de passe - if (!passwordService.isPasswordValid(newPassword)) { - throw new GBCMException("Le mot de passe ne respecte pas les critères de sécurité", "AUTH_PASSWORD_INVALID"); - } - - logger.info("Mot de passe réinitialisé avec succès"); - } - - /** - * {@inheritDoc} - */ - @Override - @Transactional - public void changePassword(String authToken, String oldPassword, String newPassword) - throws AuthenticationException, GBCMException { - logger.info("SIMULATION - Changement de mot de passe"); - - if (authToken == null || authToken.trim().isEmpty()) { - throw new AuthenticationException("Token d'authentification requis", "AUTH_TOKEN_REQUIRED"); - } - - if (oldPassword == null || oldPassword.trim().isEmpty()) { - throw new GBCMException("Ancien mot de passe requis", "AUTH_OLD_PASSWORD_REQUIRED"); - } - - if (newPassword == null || newPassword.trim().isEmpty()) { - throw new GBCMException("Nouveau mot de passe requis", "AUTH_NEW_PASSWORD_REQUIRED"); - } - - // Validation du nouveau mot de passe - if (!passwordService.isPasswordValid(newPassword)) { - throw new GBCMException("Le nouveau mot de passe ne respecte pas les critères de sécurité", "AUTH_PASSWORD_INVALID"); - } - - logger.info("Mot de passe changé avec succès"); - } -} diff --git a/src/main/java/com/gbcm/server/impl/service/notification/EmailServiceSimple.java b/src/main/java/com/gbcm/server/impl/service/notification/EmailServiceSimple.java index 59b950e..f432dff 100644 --- a/src/main/java/com/gbcm/server/impl/service/notification/EmailServiceSimple.java +++ b/src/main/java/com/gbcm/server/impl/service/notification/EmailServiceSimple.java @@ -204,7 +204,7 @@ public class EmailServiceSimple { /** * Génère le contenu HTML de l'email de verrouillage de compte. - * + * * @param user l'utilisateur * @param lockDurationMinutes la durée du verrouillage * @return le contenu HTML @@ -227,7 +227,31 @@ public class EmailServiceSimple { html.append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy à HH:mm"))); html.append("

"); html.append(""); - + return html.toString(); } + + /** + * Simule l'envoi d'un email de notification de changement de mot de passe. + * + * @param user l'utilisateur qui a changé son mot de passe + */ + public void sendPasswordChangeNotification(User user) { + if (!emailNotificationsEnabled) { + logger.debug("Notifications email désactivées, email de changement de mot de passe non envoyé"); + return; + } + + if (user == null || user.getEmail() == null) { + logger.error("Impossible d'envoyer l'email de changement de mot de passe: utilisateur ou email null"); + throw new IllegalArgumentException("Utilisateur requis"); + } + + logger.info("SIMULATION - Envoi de l'email de changement de mot de passe à: {}", user.getEmail()); + logger.debug("Contenu de l'email de changement de mot de passe:"); + logger.debug("Destinataire: {} {}", user.getFirstName(), user.getLastName()); + logger.debug("Email: {}", user.getEmail()); + + logger.info("Email de changement de mot de passe simulé avec succès pour: {}", user.getEmail()); + } } diff --git a/src/main/java/com/gbcm/server/impl/service/security/JwtService.java b/src/main/java/com/gbcm/server/impl/service/security/JwtService.java deleted file mode 100644 index d4f5ba4..0000000 --- a/src/main/java/com/gbcm/server/impl/service/security/JwtService.java +++ /dev/null @@ -1,313 +0,0 @@ -package com.gbcm.server.impl.service.security; - -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Set; -import java.util.UUID; - -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.jwt.JsonWebToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gbcm.server.api.dto.auth.TokenDTO; -import com.gbcm.server.impl.entity.User; - -import io.smallrye.jwt.build.Jwt; -import io.smallrye.jwt.build.JwtClaimsBuilder; -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Service de gestion des tokens JWT pour l'authentification GBCM. - * Fournit les fonctionnalités de création, validation et rafraîchissement des tokens. - * - * @author GBCM Development Team - * @version 1.0 - * @since 1.0 - */ -@ApplicationScoped -public class JwtService { - - private static final Logger logger = LoggerFactory.getLogger(JwtService.class); - - /** - * Durée de vie par défaut d'un token d'accès (1 heure). - */ - private static final Duration DEFAULT_ACCESS_TOKEN_DURATION = Duration.ofHours(1); - - /** - * Durée de vie par défaut d'un token de rafraîchissement (7 jours). - */ - private static final Duration DEFAULT_REFRESH_TOKEN_DURATION = Duration.ofDays(7); - - @ConfigProperty(name = "smallrye.jwt.new-token.issuer") - String issuer; - - @ConfigProperty(name = "smallrye.jwt.new-token.audience") - String audience; - - @ConfigProperty(name = "smallrye.jwt.new-token.lifespan", defaultValue = "3600") - Long defaultLifespan; - - /** - * Génère un token d'accès JWT pour un utilisateur. - * - * @param user l'utilisateur pour lequel générer le token - * @return le DTO contenant le token et ses métadonnées - * @throws IllegalArgumentException si l'utilisateur est null ou inactif - */ - public TokenDTO generateAccessToken(User user) { - if (user == null) { - throw new IllegalArgumentException("L'utilisateur ne peut pas être null"); - } - - if (!user.isActive()) { - throw new IllegalArgumentException("L'utilisateur doit être actif pour générer un token"); - } - - logger.debug("Génération d'un token d'accès pour l'utilisateur: {}", user.getEmail()); - - Instant now = Instant.now(); - Instant expiresAt = now.plus(DEFAULT_ACCESS_TOKEN_DURATION); - - String jti = UUID.randomUUID().toString(); - - String token = Jwt.issuer(issuer) - .audience(audience) - .subject(user.getEmail()) - .groups(Set.of(user.getRole().name())) - .claim("userId", user.getId()) - .claim("firstName", user.getFirstName()) - .claim("lastName", user.getLastName()) - .claim("email", user.getEmail()) - .claim("role", user.getRole().name()) - .claim("iat", now.getEpochSecond()) - .claim("exp", expiresAt.getEpochSecond()) - .claim("jti", jti) - .sign(); - - // Générer aussi un refresh token - String refreshToken = generateRefreshToken(user); - - TokenDTO tokenDTO = new TokenDTO(); - tokenDTO.setToken(token); - tokenDTO.setTokenType("Bearer"); - tokenDTO.setExpiresAt(LocalDateTime.ofInstant(expiresAt, ZoneOffset.UTC)); - tokenDTO.setExpiresIn(DEFAULT_ACCESS_TOKEN_DURATION.toSeconds()); - tokenDTO.setRefreshToken(refreshToken); - - logger.info("Token d'accès généré avec succès pour l'utilisateur: {}", user.getEmail()); - return tokenDTO; - } - - /** - * Génère un token de rafraîchissement pour un utilisateur. - * - * @param user l'utilisateur pour lequel générer le refresh token - * @return le refresh token JWT - * @throws IllegalArgumentException si l'utilisateur est null - */ - public String generateRefreshToken(User user) { - if (user == null) { - throw new IllegalArgumentException("L'utilisateur ne peut pas être null"); - } - - logger.debug("Génération d'un refresh token pour l'utilisateur: {}", user.getEmail()); - - Instant now = Instant.now(); - Instant expiresAt = now.plus(DEFAULT_REFRESH_TOKEN_DURATION); - - String jti = UUID.randomUUID().toString(); - - return Jwt.issuer(issuer) - .audience(audience + "-refresh") - .subject(user.getEmail()) - .claim("userId", user.getId()) - .claim("tokenType", "refresh") - .claim("iat", now.getEpochSecond()) - .claim("exp", expiresAt.getEpochSecond()) - .claim("jti", jti) - .sign(); - } - - /** - * Génère un token de réinitialisation de mot de passe. - * - * @param user l'utilisateur pour lequel générer le token - * @return le token de réinitialisation - * @throws IllegalArgumentException si l'utilisateur est null - */ - public String generatePasswordResetToken(User user) { - if (user == null) { - throw new IllegalArgumentException("L'utilisateur ne peut pas être null"); - } - - logger.debug("Génération d'un token de réinitialisation pour l'utilisateur: {}", user.getEmail()); - - Instant now = Instant.now(); - Instant expiresAt = now.plus(Duration.ofHours(2)); // Token valide 2 heures - - String jti = UUID.randomUUID().toString(); - - return Jwt.issuer(issuer) - .audience(audience + "-password-reset") - .subject(user.getEmail()) - .claim("userId", user.getId()) - .claim("tokenType", "password-reset") - .claim("iat", now.getEpochSecond()) - .claim("exp", expiresAt.getEpochSecond()) - .claim("jti", jti) - .sign(); - } - - /** - * Valide un token JWT et retourne ses claims. - * - * @param token le token à valider - * @return les claims du token si valide - * @throws SecurityException si le token est invalide - */ - public JsonWebToken validateToken(String token) { - try { - // La validation est automatiquement effectuée par Quarkus JWT - // Cette méthode peut être étendue pour des validations supplémentaires - logger.debug("Validation du token JWT"); - return null; // Sera implémenté avec l'injection du JsonWebToken - } catch (Exception e) { - logger.error("Erreur lors de la validation du token: {}", e.getMessage()); - throw new SecurityException("Token JWT invalide", e); - } - } - - /** - * Extrait l'ID utilisateur d'un token JWT. - * - * @param jwt le token JWT - * @return l'ID de l'utilisateur - * @throws SecurityException si l'ID n'est pas présent - */ - public Long extractUserId(JsonWebToken jwt) { - if (jwt == null) { - throw new SecurityException("Token JWT requis"); - } - - Object userIdClaim = jwt.getClaim("userId"); - if (userIdClaim == null) { - throw new SecurityException("Claim userId manquant dans le token"); - } - - try { - return Long.valueOf(userIdClaim.toString()); - } catch (NumberFormatException e) { - throw new SecurityException("Format invalide pour userId dans le token", e); - } - } - - /** - * Extrait l'email d'un token JWT. - * - * @param jwt le token JWT - * @return l'email de l'utilisateur - * @throws SecurityException si l'email n'est pas présent - */ - public String extractEmail(JsonWebToken jwt) { - if (jwt == null) { - throw new SecurityException("Token JWT requis"); - } - - String email = jwt.getSubject(); - if (email == null || email.trim().isEmpty()) { - throw new SecurityException("Email manquant dans le token"); - } - - return email; - } - - /** - * Vérifie si un token est expiré. - * - * @param jwt le token JWT - * @return true si le token est expiré, false sinon - */ - public boolean isTokenExpired(JsonWebToken jwt) { - if (jwt == null) { - return true; - } - - Long exp = jwt.getExpirationTime(); - if (exp == null) { - return true; - } - - return Instant.ofEpochSecond(exp).isBefore(Instant.now()); - } - - /** - * Vérifie si un token est de type refresh. - * - * @param jwt le token JWT - * @return true si c'est un refresh token, false sinon - */ - public boolean isRefreshToken(JsonWebToken jwt) { - if (jwt == null) { - return false; - } - - Object tokenType = jwt.getClaim("tokenType"); - return "refresh".equals(tokenType); - } - - /** - * Vérifie si un token est de type password reset. - * - * @param jwt le token JWT - * @return true si c'est un token de réinitialisation, false sinon - */ - public boolean isPasswordResetToken(JsonWebToken jwt) { - if (jwt == null) { - return false; - } - - Object tokenType = jwt.getClaim("tokenType"); - return "password-reset".equals(tokenType); - } - - /** - * Crée un token avec des claims personnalisés. - * - * @param user l'utilisateur - * @param duration la durée de validité - * @param additionalClaims les claims supplémentaires - * @return le token JWT - */ - public String createCustomToken(User user, Duration duration, java.util.Map additionalClaims) { - if (user == null) { - throw new IllegalArgumentException("L'utilisateur ne peut pas être null"); - } - - logger.debug("Création d'un token personnalisé pour l'utilisateur: {}", user.getEmail()); - - Instant now = Instant.now(); - Instant expiresAt = now.plus(duration); - - JwtClaimsBuilder builder = Jwt.issuer(issuer) - .audience(audience) - .subject(user.getEmail()) - .claim("userId", user.getId()) - .claim("email", user.getEmail()) - .claim("iat", now.getEpochSecond()) - .claim("exp", expiresAt.getEpochSecond()) - .claim("jti", UUID.randomUUID().toString()); - - // Ajouter les claims supplémentaires - if (additionalClaims != null) { - additionalClaims.forEach(builder::claim); - } - - return builder.jws() - .keyId("gbcm-key") - .sign(); - } -} diff --git a/src/main/java/com/gbcm/server/impl/service/security/PasswordService.java b/src/main/java/com/gbcm/server/impl/service/security/PasswordService.java index 3e5ac5e..94d9795 100644 --- a/src/main/java/com/gbcm/server/impl/service/security/PasswordService.java +++ b/src/main/java/com/gbcm/server/impl/service/security/PasswordService.java @@ -107,7 +107,7 @@ public class PasswordService { /** * Valide la complexité d'un mot de passe selon les règles de sécurité GBCM. - * + * * @param password le mot de passe à valider * @return true si le mot de passe est valide, false sinon */ @@ -137,6 +137,16 @@ public class PasswordService { return isComplex; } + /** + * Alias pour isPasswordValid - utilisé par les tests. + * + * @param password le mot de passe à valider + * @return true si le mot de passe est valide, false sinon + */ + public boolean validatePassword(String password) { + return isPasswordValid(password); + } + /** * Génère un mot de passe aléatoire sécurisé. * diff --git a/src/main/java/com/gbcm/server/impl/service/security/SecurityService.java b/src/main/java/com/gbcm/server/impl/service/security/SecurityService.java index acc57f3..e678041 100644 --- a/src/main/java/com/gbcm/server/impl/service/security/SecurityService.java +++ b/src/main/java/com/gbcm/server/impl/service/security/SecurityService.java @@ -1,19 +1,19 @@ package com.gbcm.server.impl.service.security; +import java.util.Set; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.gbcm.server.api.enums.UserRole; import com.gbcm.server.api.exceptions.AuthenticationException; import com.gbcm.server.api.exceptions.AuthorizationException; import com.gbcm.server.impl.entity.User; + import io.quarkus.security.identity.SecurityIdentity; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import org.eclipse.microprofile.jwt.JsonWebToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.LocalDateTime; -import java.util.Optional; -import java.util.Set; /** * Service de sécurité central pour la plateforme GBCM. @@ -41,8 +41,7 @@ public class SecurityService { @Inject PasswordService passwordService; - @Inject - JwtService jwtService; + // JwtService supprimé - utilise OIDC Keycloak maintenant @Inject SecurityIdentity securityIdentity; @@ -190,33 +189,37 @@ public class SecurityService { } /** - * Obtient l'ID de l'utilisateur actuellement connecté. - * + * Obtient l'ID de l'utilisateur actuellement connecté via OIDC. + * * @return l'ID de l'utilisateur ou null si non connecté */ public Long getCurrentUserId() { try { - if (jwt != null) { - return jwtService.extractUserId(jwt); + if (securityIdentity != null && !securityIdentity.isAnonymous()) { + // Avec OIDC, nous devons chercher l'utilisateur par email/username + String email = securityIdentity.getPrincipal().getName(); + User user = User.findByEmail(email); + return user != null ? user.getId() : null; } } catch (Exception e) { - logger.debug("Impossible d'extraire l'ID utilisateur du token: {}", e.getMessage()); + logger.debug("Impossible d'extraire l'ID utilisateur OIDC: {}", e.getMessage()); } return null; } /** - * Obtient l'email de l'utilisateur actuellement connecté. - * + * Obtient l'email de l'utilisateur actuellement connecté via OIDC. + * * @return l'email de l'utilisateur ou null si non connecté */ public String getCurrentUserEmail() { try { - if (jwt != null) { - return jwtService.extractEmail(jwt); + if (securityIdentity != null && !securityIdentity.isAnonymous()) { + // Avec OIDC, le principal name est généralement l'email + return securityIdentity.getPrincipal().getName(); } } catch (Exception e) { - logger.debug("Impossible d'extraire l'email du token: {}", e.getMessage()); + logger.debug("Impossible d'extraire l'email OIDC: {}", e.getMessage()); } return null; } @@ -305,19 +308,17 @@ public class SecurityService { } /** - * Valide qu'un token JWT n'est pas expiré. - * - * @param jwt le token à valider - * @throws AuthenticationException si le token est expiré + * Valide qu'un token OIDC est valide. + * Avec OIDC, la validation est automatiquement effectuée par Quarkus. + * + * @param jwt le token à valider (peut être null avec OIDC) + * @throws AuthenticationException si l'utilisateur n'est pas authentifié */ public void validateTokenNotExpired(JsonWebToken jwt) throws AuthenticationException { - if (jwt == null) { - throw new AuthenticationException("Token requis", "AUTH_TOKEN_REQUIRED"); - } - - if (jwtService.isTokenExpired(jwt)) { - logger.warn("Tentative d'utilisation d'un token expiré"); - throw new AuthenticationException("Token expiré", "AUTH_TOKEN_EXPIRED"); + // Avec OIDC, nous vérifions plutôt l'identité de sécurité + if (securityIdentity == null || securityIdentity.isAnonymous()) { + logger.warn("Tentative d'accès sans authentification OIDC"); + throw new AuthenticationException("Authentification OIDC requise", "AUTH_OIDC_REQUIRED"); } } diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index e8e594a..2b643f7 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -11,10 +11,7 @@ #quarkus.hibernate-orm.database.generation=validate #quarkus.flyway.migrate-at-start=true -# Configuration JWT locale -gbcm.security.jwt.secret=local-development-secret-key-change-me -gbcm.security.jwt.access-token.duration=PT2H -gbcm.security.jwt.refresh-token.duration=P1D +# Configuration locale (JWT supprimé - utilise OIDC Keycloak) # Configuration email locale (pour tests) quarkus.mailer.mock=true diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 24a9529..639d921 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -29,19 +29,19 @@ quarkus.flyway.baseline-on-migrate=true # Security Configuration quarkus.security.auth.enabled=true -# JWT Configuration -mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem -mp.jwt.verify.issuer=https://gbcm.com -smallrye.jwt.sign.key.location=META-INF/resources/privateKey.pem -smallrye.jwt.new-token.issuer=https://gbcm.com -smallrye.jwt.new-token.audience=gbcm-users -smallrye.jwt.new-token.lifespan=3600 +# Keycloak OIDC Configuration +quarkus.oidc.auth-server-url=http://localhost:8180/realms/gbcm-llc +quarkus.oidc.client-id=gbcm-backend-api +quarkus.oidc.credentials.secret=gbcm-backend-secret +quarkus.oidc.tls.verification=none +quarkus.oidc.application-type=service +quarkus.oidc.bearer-only=true -# GBCM Security Configuration -gbcm.security.jwt.access-token.duration=PT1H -gbcm.security.jwt.refresh-token.duration=P7D -gbcm.security.jwt.reset-token.duration=PT2H -gbcm.security.jwt.secret=${JWT_SECRET:gbcm-super-secret-key-change-in-production} +# Mapping des rôles Keycloak vers GBCM +quarkus.oidc.roles.source=accesstoken +quarkus.oidc.token.principal-claim=preferred_username + +# GBCM Security Configuration (non-JWT) gbcm.security.password.bcrypt-cost=12 gbcm.security.password.min-length=8 gbcm.security.password.require-uppercase=true @@ -53,9 +53,9 @@ gbcm.security.account.lockout-duration=PT30M gbcm.security.password.max-age=P90D # OpenAPI Configuration -quarkus.smallrye-openapi.path=/swagger-ui +quarkus.smallrye-openapi.path=/q/openapi quarkus.swagger-ui.always-include=true -quarkus.swagger-ui.path=/swagger +quarkus.swagger-ui.path=/q/swagger-ui mp.openapi.extensions.smallrye.info.title=GBCM Server API mp.openapi.extensions.smallrye.info.version=1.0.0 mp.openapi.extensions.smallrye.info.description=API pour les services GBCM @@ -92,8 +92,11 @@ quarkus.log.category."org.hibernate.SQL".level=DEBUG # Development Profile %dev.quarkus.log.level=DEBUG %dev.quarkus.hibernate-orm.log.sql=true -%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:gbcm_server_dev;DB_CLOSE_DELAY=-1 -%dev.quarkus.datasource.db-kind=h2 +# Configuration PostgreSQL pour GBCM LLC (utilise la base skyfile directement) +%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/skyfile +%dev.quarkus.datasource.db-kind=postgresql +%dev.quarkus.datasource.username=skyfile +%dev.quarkus.datasource.password=skyfile %dev.quarkus.hibernate-orm.database.generation=drop-and-create %dev.quarkus.flyway.migrate-at-start=false %dev.gbcm.security.jwt.secret=dev-secret-key-not-for-production diff --git a/src/main/resources/db/migration/V3__Create_coaches_table.sql b/src/main/resources/db/migration/V3__Create_coaches_table.sql index 754f16c..2c7f28b 100644 --- a/src/main/resources/db/migration/V3__Create_coaches_table.sql +++ b/src/main/resources/db/migration/V3__Create_coaches_table.sql @@ -60,8 +60,8 @@ CREATE TABLE coaches ( -- Table de liaison pour les types de services offerts par le coach CREATE TABLE coach_service_types ( coach_id BIGINT NOT NULL, - service_type VARCHAR(30) NOT NULL CHECK (service_type IN ('strategic_workshop', 'one_on_one_coaching', 'on_demand_coaching', 'special_project')), - + service_type VARCHAR(30) NOT NULL CHECK (service_type IN ('STRATEGIC_WORKSHOP', 'ONE_ON_ONE_COACHING', 'ON_DEMAND_COACHING', 'SPECIAL_PROJECT')), + PRIMARY KEY (coach_id, service_type), CONSTRAINT fk_coach_service_types_coach_id FOREIGN KEY (coach_id) REFERENCES coaches(id) ON DELETE CASCADE ); diff --git a/src/main/resources/db/migration/V6__Add_status_column_to_users.sql b/src/main/resources/db/migration/V6__Add_status_column_to_users.sql new file mode 100644 index 0000000..3b9c72a --- /dev/null +++ b/src/main/resources/db/migration/V6__Add_status_column_to_users.sql @@ -0,0 +1,14 @@ +-- Migration V6: Ajout de la colonne status à la table users +-- Auteur: GBCM Development Team +-- Date: 2025-10-07 +-- Description: Ajout de la colonne status manquante dans la table users + +-- Ajout de la colonne status +ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE'; + +-- Ajout de la contrainte CHECK pour les valeurs valides +ALTER TABLE users ADD CONSTRAINT chk_users_status + CHECK (status IN ('ACTIVE', 'INACTIVE', 'SUSPENDED', 'PENDING')); + +-- Commentaire sur la nouvelle colonne +COMMENT ON COLUMN users.status IS 'Statut de l''utilisateur (ACTIVE, INACTIVE, SUSPENDED, PENDING)'; diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql index b57e3db..6066650 100644 --- a/src/main/resources/import.sql +++ b/src/main/resources/import.sql @@ -1,38 +1,64 @@ --- Données de test pour le développement GBCM +-- ===================================================== +-- DONNÉES DE TEST POUR LE DÉVELOPPEMENT GBCM +-- ===================================================== -- Ce fichier est chargé automatiquement en mode développement +-- Mot de passe pour tous les utilisateurs: "password123" (hash BCrypt) --- Insertion des utilisateurs de test --- Mot de passe pour tous: "password123" (hash BCrypt) -INSERT INTO users (first_name, last_name, email, password_hash, role, active, deleted, created_by) VALUES -('Admin', 'System', 'admin@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'ADMIN', true, false, 'system'), -('John', 'Manager', 'manager@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'MANAGER', true, false, 'system'), -('Sarah', 'Coach', 'sarah.coach@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'COACH', true, false, 'system'), -('Michael', 'Expert', 'michael.expert@gbcm.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'COACH', true, false, 'system'), -('Emily', 'Johnson', 'emily.johnson@techcorp.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'CLIENT', true, false, 'system'), -('David', 'Smith', 'david.smith@innovate.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'CLIENT', true, false, 'system'), -('Lisa', 'Brown', 'lisa.brown@startup.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'PROSPECT', true, false, 'system'), -('Robert', 'Wilson', 'robert.wilson@enterprise.com', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', 'PROSPECT', true, false, 'system'); +-- ===================================================== +-- 1. INSERTION DES UTILISATEURS DE BASE +-- ===================================================== +INSERT INTO users ( + first_name, last_name, email, password_hash, role, status, + active, failed_login_attempts, deleted, + created_at, updated_at, created_by +) VALUES +-- Administrateur système +('Admin', 'System', 'admin@gbcm.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'ADMIN', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'), --- Insertion des coaches -INSERT INTO coaches (user_id, specialization, bio, years_of_experience, certifications, languages, hourly_rate, status, available_for_booking, working_hours_start, working_hours_end, working_days, timezone, start_date, average_rating, total_ratings, total_sessions, total_revenue, deleted, created_by) VALUES -(3, 'Strategic Planning', 'Expert en planification stratégique avec plus de 10 ans d''expérience dans le conseil aux PME. Spécialisée dans la transformation digitale et l''optimisation des processus.', 10, 'Certified Management Consultant (CMC), PMP, Lean Six Sigma Black Belt', 'English, French, Spanish', 125.00, 'ACTIVE', true, '09:00:00', '17:00:00', 'MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY', 'America/New_York', '2020-01-15', 4.8, 45, 120, 15000.00, false, 'system'), -(4, 'Business Development', 'Coach expérimenté en développement commercial et leadership. Aide les entrepreneurs à développer leurs compétences managériales et à faire croître leur entreprise.', 8, 'Certified Business Coach (CBC), MBA, Dale Carnegie Leadership Training', 'English, French', 100.00, 'ACTIVE', true, '08:00:00', '18:00:00', 'MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY', 'America/New_York', '2021-03-01', 4.9, 38, 95, 9500.00, false, 'system'); +-- Manager +('John', 'Manager', 'manager@gbcm.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'MANAGER', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'), --- Insertion des types de services pour les coaches -INSERT INTO coach_service_types (coach_id, service_type) VALUES -(1, 'strategic_workshop'), -(1, 'one_on_one_coaching'), -(1, 'special_project'), -(2, 'one_on_one_coaching'), -(2, 'on_demand_coaching'), -(2, 'special_project'); +-- Coaches +('Sarah', 'Coach', 'sarah.coach@gbcm.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'COACH', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'), --- Insertion des clients -INSERT INTO clients (user_id, company_name, industry, company_size, annual_revenue, address_line1, city, state, postal_code, country, website, status, converted_at, primary_service_type, total_contract_value, relationship_start_date, acquisition_source, deleted, created_by) VALUES -(5, 'TechCorp Solutions', 'Technology', 50, 2500000.00, '123 Tech Street', 'Atlanta', 'GA', '30309', 'USA', 'https://techcorp.com', 'ACTIVE', '2023-06-15 10:30:00', 'strategic_workshop', 4000.00, '2023-06-15', 'Website', false, 'system'), -(6, 'Innovate Inc', 'Consulting', 25, 1200000.00, '456 Innovation Ave', 'Atlanta', 'GA', '30308', 'USA', 'https://innovate.com', 'ACTIVE', '2023-08-20 14:15:00', 'one_on_one_coaching', 2500.00, '2023-08-20', 'Referral', false, 'system'); +('Michael', 'Expert', 'michael.expert@gbcm.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'COACH', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'), --- Insertion des prospects -INSERT INTO clients (user_id, company_name, industry, company_size, annual_revenue, address_line1, city, state, postal_code, country, website, status, acquisition_source, deleted, created_by) VALUES -(7, 'StartupCo', 'E-commerce', 10, 500000.00, '789 Startup Blvd', 'Atlanta', 'GA', '30307', 'USA', 'https://startupco.com', 'PROSPECT', 'LinkedIn', false, 'system'), -(8, 'Enterprise Corp', 'Manufacturing', 200, 15000000.00, '321 Enterprise Way', 'Atlanta', 'GA', '30306', 'USA', 'https://enterprise.com', 'PROSPECT', 'Trade Show', false, 'system'); +-- Clients +('Emily', 'Johnson', 'emily.johnson@techcorp.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'CLIENT', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'), + +('David', 'Smith', 'david.smith@innovate.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'CLIENT', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'), + +-- Prospects +('Lisa', 'Brown', 'lisa.brown@startup.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'PROSPECT', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'), + +('Robert', 'Wilson', 'robert.wilson@enterprise.com', + '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9P8jW9TjnOvQF9G', + 'PROSPECT', 'ACTIVE', true, 0, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system'); + +-- ===================================================== +-- NOTE: DONNÉES COMPLEXES VIA API +-- ===================================================== +-- Les coaches, clients et autres entités avec relations +-- seront créés via les endpoints REST pour éviter les +-- problèmes de clés étrangères avec les IDs auto-générés. +-- +-- Utilisez les endpoints suivants après le démarrage: +-- POST /api/coaches - Créer des coaches +-- POST /api/clients - Créer des clients +-- POST /api/workshops - Créer des ateliers +-- POST /api/coaching-sessions - Créer des sessions +-- ===================================================== diff --git a/src/test/java/com/gbcm/server/impl/service/AuthServiceImplTest.java b/src/test/java/com/gbcm/server/impl/service/AuthServiceImplTest.java deleted file mode 100644 index 3a36300..0000000 --- a/src/test/java/com/gbcm/server/impl/service/AuthServiceImplTest.java +++ /dev/null @@ -1,319 +0,0 @@ -package com.gbcm.server.impl.service; - -import com.gbcm.server.api.dto.auth.LoginRequestDTO; -import com.gbcm.server.api.dto.auth.LoginResponseDTO; -import com.gbcm.server.api.dto.auth.TokenDTO; -import com.gbcm.server.api.dto.user.UserDTO; -import com.gbcm.server.api.enums.UserRole; -import com.gbcm.server.api.exceptions.AuthenticationException; -import com.gbcm.server.api.exceptions.GBCMException; -import com.gbcm.server.impl.entity.User; -import com.gbcm.server.impl.service.security.JwtService; -import com.gbcm.server.impl.service.security.PasswordService; -import com.gbcm.server.impl.service.security.SecurityService; - -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.mockito.InjectMock; -import jakarta.inject.Inject; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Tests unitaires pour AuthServiceImpl. - * Vérifie toutes les méthodes d'authentification avec mocks. - * - * @author GBCM Development Team - * @version 1.0 - * @since 1.0 - */ -@QuarkusTest -@DisplayName("Tests AuthServiceImpl") -class AuthServiceImplTest { - - @Inject - AuthServiceImpl authService; - - @InjectMock - JwtService jwtService; - - @InjectMock - PasswordService passwordService; - - @InjectMock - SecurityService securityService; - - private LoginRequestDTO validLoginRequest; - private User mockUser; - private TokenDTO mockToken; - - /** - * Configuration initiale avant chaque test. - * Prépare les objets de test et les mocks. - */ - @BeforeEach - void setUp() { - // Préparation des données de test - validLoginRequest = new LoginRequestDTO(); - validLoginRequest.setEmail("admin@gbcm.com"); - validLoginRequest.setPassword("admin123"); - - // Mock User - mockUser = new User(); - mockUser.setId(1L); - mockUser.setFirstName("Admin"); - mockUser.setLastName("GBCM"); - mockUser.setEmail("admin@gbcm.com"); - mockUser.setPasswordHash("$2a$10$hashedPassword"); - mockUser.setRole(UserRole.ADMIN); - mockUser.setActive(true); - - // Mock Token - mockToken = new TokenDTO(); - mockToken.setToken("mock_jwt_token_12345"); - mockToken.setExpiresAt(java.time.LocalDateTime.now().plusHours(1)); - - // Reset mocks - Mockito.reset(jwtService, passwordService, securityService); - } - - /** - * Test d'injection du service. - * Vérifie que le service est correctement injecté. - */ - @Test - @DisplayName("Injection du service AuthServiceImpl") - void testServiceInjection() { - assertThat(authService).isNotNull(); - assertThat(jwtService).isNotNull(); - assertThat(passwordService).isNotNull(); - assertThat(securityService).isNotNull(); - } - - /** - * Test de connexion avec utilisateur existant. - * Vérifie qu'une exception est levée car User.findByEmail() retourne null en simulation. - */ - @Test - @DisplayName("Connexion avec utilisateur non trouvé (simulation)") - void testLogin_UserNotFoundInSimulation() throws Exception { - // Given - En mode simulation, User.findByEmail() retourne toujours null - when(passwordService.verifyPassword(anyString(), anyString())).thenReturn(true); - when(jwtService.generateAccessToken(any(User.class))).thenReturn(mockToken); - - // When & Then - L'utilisateur ne sera pas trouvé en simulation - assertThatThrownBy(() -> authService.login(validLoginRequest)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Email ou mot de passe incorrect"); - } - - /** - * Test de connexion avec requête null. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Connexion avec requête null") - void testLogin_NullRequest() { - // When & Then - assertThatThrownBy(() -> authService.login(null)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Email et mot de passe requis"); - } - - /** - * Test de connexion avec email null. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Connexion avec email null") - void testLogin_NullEmail() { - // Given - validLoginRequest.setEmail(null); - - // When & Then - assertThatThrownBy(() -> authService.login(validLoginRequest)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Email et mot de passe requis"); - } - - /** - * Test de connexion avec mot de passe null. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Connexion avec mot de passe null") - void testLogin_NullPassword() { - // Given - validLoginRequest.setPassword(null); - - // When & Then - assertThatThrownBy(() -> authService.login(validLoginRequest)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Email et mot de passe requis"); - } - - /** - * Test de connexion avec utilisateur non trouvé. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Connexion avec utilisateur non trouvé") - void testLogin_UserNotFound() { - // Given - User.findByEmail retournera null (comportement par défaut) - validLoginRequest.setEmail("nonexistent@gbcm.com"); - - // When & Then - assertThatThrownBy(() -> authService.login(validLoginRequest)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Email ou mot de passe incorrect"); - } - - /** - * Test de connexion avec mot de passe incorrect. - * En simulation, User.findByEmail() retourne null donc ce test vérifie le comportement attendu. - */ - @Test - @DisplayName("Connexion avec mot de passe incorrect (simulation)") - void testLogin_WrongPassword() throws Exception { - // Given - En simulation, l'utilisateur ne sera pas trouvé - when(passwordService.verifyPassword(anyString(), anyString())).thenReturn(false); - - // When & Then - L'exception sera levée car utilisateur non trouvé - assertThatThrownBy(() -> authService.login(validLoginRequest)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Email ou mot de passe incorrect"); - - // Note: passwordService.verifyPassword ne sera pas appelé car User.findByEmail() retourne null - } - - /** - * Test de déconnexion. - * Vérifie que la méthode logout fonctionne sans erreur. - */ - @Test - @DisplayName("Déconnexion utilisateur") - void testLogout() { - // When & Then - Ne doit pas lever d'exception - assertThatCode(() -> authService.logout("valid_token")) - .doesNotThrowAnyException(); - } - - /** - * Test de rafraîchissement de token réussi. - * Vérifie qu'un nouveau token est généré. - */ - @Test - @DisplayName("Rafraîchissement de token réussi") - void testRefreshToken_Success() throws Exception { - // Given - String refreshToken = "valid_refresh_token"; - - // When - LoginResponseDTO result = authService.refreshToken(refreshToken); - - // Then - assertThat(result).isNotNull(); - assertThat(result.getToken()).startsWith("new_access_token_"); - assertThat(result.getRefreshToken()).isEqualTo(refreshToken); - } - - /** - * Test de rafraîchissement avec token null. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Rafraîchissement avec token null") - void testRefreshToken_NullToken() { - // When & Then - assertThatThrownBy(() -> authService.refreshToken(null)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Refresh token requis"); - } - - /** - * Test de rafraîchissement avec token vide. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Rafraîchissement avec token vide") - void testRefreshToken_EmptyToken() { - // When & Then - assertThatThrownBy(() -> authService.refreshToken(" ")) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Refresh token requis"); - } - - /** - * Test de validation de token réussie. - * Vérifie qu'un UserDTO est retourné. - */ - @Test - @DisplayName("Validation de token réussie") - void testValidateToken_Success() throws Exception { - // Given - String authToken = "Bearer valid_token"; - - // When - UserDTO result = authService.validateToken(authToken); - - // Then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(1L); - assertThat(result.getEmail()).isEqualTo("admin@gbcm.com"); - assertThat(result.getRole()).isEqualTo(UserRole.ADMIN); - assertThat(result.isActive()).isTrue(); - } - - /** - * Test de validation avec token null. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Validation avec token null") - void testValidateToken_NullToken() { - // When & Then - assertThatThrownBy(() -> authService.validateToken(null)) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Token requis"); - } - - /** - * Test de validation avec token vide. - * Vérifie qu'une AuthenticationException est levée. - */ - @Test - @DisplayName("Validation avec token vide") - void testValidateToken_EmptyToken() { - // When & Then - assertThatThrownBy(() -> authService.validateToken(" ")) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Token requis"); - } - - /** - * Test de performance pour la connexion. - * Vérifie que la tentative de connexion se fait rapidement même si elle échoue. - */ - @Test - @DisplayName("Performance de la connexion") - void testLogin_Performance() throws Exception { - // Given - when(passwordService.verifyPassword(anyString(), anyString())).thenReturn(true); - when(jwtService.generateAccessToken(any(User.class))).thenReturn(mockToken); - - // When & Then - Mesurer le temps même si l'exception est levée - long startTime = System.currentTimeMillis(); - try { - authService.login(validLoginRequest); - } catch (AuthenticationException e) { - // Exception attendue en simulation - } - long endTime = System.currentTimeMillis(); - assertThat(endTime - startTime).isLessThan(1000L); - } -} diff --git a/src/test/java/com/gbcm/server/impl/service/notification/EmailServiceSimpleTest.java b/src/test/java/com/gbcm/server/impl/service/notification/EmailServiceSimpleTest.java index 342a94c..96f9d8e 100644 --- a/src/test/java/com/gbcm/server/impl/service/notification/EmailServiceSimpleTest.java +++ b/src/test/java/com/gbcm/server/impl/service/notification/EmailServiceSimpleTest.java @@ -109,9 +109,10 @@ class EmailServiceSimpleTest { void testSendPasswordResetEmail_ValidUser() { // Given String resetToken = "reset-token-123"; + String resetUrl = "https://gbcm.com/reset-password?token=" + resetToken; // When & Then - Ne doit pas lever d'exception - assertThatCode(() -> emailService.sendPasswordResetEmail(testUser, resetToken)) + assertThatCode(() -> emailService.sendPasswordResetEmail(testUser, resetToken, resetUrl)) .doesNotThrowAnyException(); } @@ -123,11 +124,12 @@ class EmailServiceSimpleTest { void testSendPasswordResetEmail_NullUser() { // Given String resetToken = "reset-token-123"; + String resetUrl = "https://gbcm.com/reset-password?token=" + resetToken; // When & Then - assertThatThrownBy(() -> emailService.sendPasswordResetEmail(null, resetToken)) + assertThatThrownBy(() -> emailService.sendPasswordResetEmail(null, resetToken, resetUrl)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Utilisateur et token requis"); + .hasMessage("Utilisateur, email, token et URL requis"); } /** @@ -136,10 +138,13 @@ class EmailServiceSimpleTest { @Test @DisplayName("Envoi d'email de réinitialisation avec token null") void testSendPasswordResetEmail_NullToken() { + // Given + String resetUrl = "https://gbcm.com/reset-password"; + // When & Then - assertThatThrownBy(() -> emailService.sendPasswordResetEmail(testUser, null)) + assertThatThrownBy(() -> emailService.sendPasswordResetEmail(testUser, null, resetUrl)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Utilisateur et token requis"); + .hasMessage("Utilisateur, email, token et URL requis"); } /** @@ -151,11 +156,12 @@ class EmailServiceSimpleTest { // Given testUser.setEmail(null); String resetToken = "reset-token-123"; + String resetUrl = "https://gbcm.com/reset-password?token=" + resetToken; // When & Then - assertThatThrownBy(() -> emailService.sendPasswordResetEmail(testUser, resetToken)) + assertThatThrownBy(() -> emailService.sendPasswordResetEmail(testUser, resetToken, resetUrl)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Utilisateur et token requis"); + .hasMessage("Utilisateur, email, token et URL requis"); } /**