diff --git a/src/main/java/com/gbcm/server/impl/entity/User.java b/src/main/java/com/gbcm/server/impl/entity/User.java
index bff0563..a93c8b9 100644
--- a/src/main/java/com/gbcm/server/impl/entity/User.java
+++ b/src/main/java/com/gbcm/server/impl/entity/User.java
@@ -1,19 +1,30 @@
package com.gbcm.server.impl.entity;
+import java.time.LocalDateTime;
+import java.util.List;
+
import com.gbcm.server.api.enums.UserRole;
+
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;
-import jakarta.persistence.*;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.NamedQueries;
+import jakarta.persistence.NamedQuery;
+import jakarta.persistence.Table;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
-import java.time.LocalDateTime;
-import java.util.List;
-
/**
* Entité représentant un utilisateur de la plateforme GBCM.
* Utilisée pour l'authentification et l'autorisation avec Quarkus Security.
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
new file mode 100644
index 0000000..59b950e
--- /dev/null
+++ b/src/main/java/com/gbcm/server/impl/service/notification/EmailServiceSimple.java
@@ -0,0 +1,233 @@
+package com.gbcm.server.impl.service.notification;
+
+import com.gbcm.server.impl.entity.User;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Service d'envoi d'emails simplifié pour la plateforme GBCM.
+ * Version basique synchrone pour les tests et le développement initial.
+ *
+ * @author GBCM Development Team
+ * @version 1.0
+ * @since 1.0
+ */
+@ApplicationScoped
+public class EmailServiceSimple {
+
+ private static final Logger logger = LoggerFactory.getLogger(EmailServiceSimple.class);
+
+ @ConfigProperty(name = "gbcm.business.notification.email.enabled", defaultValue = "true")
+ boolean emailNotificationsEnabled;
+
+ @ConfigProperty(name = "quarkus.application.name", defaultValue = "GBCM")
+ String applicationName;
+
+ /**
+ * Simule l'envoi d'un email de bienvenue à un nouvel utilisateur.
+ *
+ * @param user le nouvel utilisateur
+ * @param temporaryPassword le mot de passe temporaire (optionnel)
+ */
+ public void sendWelcomeEmail(User user, String temporaryPassword) {
+ if (!emailNotificationsEnabled) {
+ logger.debug("Notifications email désactivées, email de bienvenue non envoyé");
+ return;
+ }
+
+ if (user == null || user.getEmail() == null) {
+ logger.error("Impossible d'envoyer l'email de bienvenue: utilisateur ou email null");
+ throw new IllegalArgumentException("Utilisateur et email requis");
+ }
+
+ logger.info("SIMULATION - Envoi de l'email de bienvenue à: {}", user.getEmail());
+ logger.debug("Contenu de l'email de bienvenue:");
+ logger.debug("Destinataire: {} {}", user.getFirstName(), user.getLastName());
+ logger.debug("Email: {}", user.getEmail());
+
+ if (temporaryPassword != null) {
+ logger.debug("Mot de passe temporaire inclus dans l'email");
+ }
+
+ logger.info("Email de bienvenue simulé avec succès pour: {}", user.getEmail());
+ }
+
+ /**
+ * Simule l'envoi d'un email de réinitialisation de mot de passe.
+ *
+ * @param user l'utilisateur demandant la réinitialisation
+ * @param resetToken le token de réinitialisation
+ * @param resetUrl l'URL de réinitialisation
+ */
+ public void sendPasswordResetEmail(User user, String resetToken, String resetUrl) {
+ if (!emailNotificationsEnabled) {
+ logger.debug("Notifications email désactivées, email de réinitialisation non envoyé");
+ return;
+ }
+
+ if (user == null || user.getEmail() == null || resetToken == null || resetUrl == null) {
+ logger.error("Impossible d'envoyer l'email de réinitialisation: paramètres manquants");
+ throw new IllegalArgumentException("Utilisateur, email, token et URL requis");
+ }
+
+ logger.info("SIMULATION - Envoi de l'email de réinitialisation de mot de passe à: {}", user.getEmail());
+ logger.debug("Contenu de l'email de réinitialisation:");
+ logger.debug("Destinataire: {} {}", user.getFirstName(), user.getLastName());
+ logger.debug("Email: {}", user.getEmail());
+ logger.debug("URL de réinitialisation: {}", resetUrl);
+ logger.debug("Token: {}...", resetToken.substring(0, Math.min(10, resetToken.length())));
+
+ logger.info("Email de réinitialisation simulé avec succès pour: {}", user.getEmail());
+ }
+
+ /**
+ * Simule l'envoi d'un email de notification de verrouillage de compte.
+ *
+ * @param user l'utilisateur dont le compte est verrouillé
+ * @param lockDurationMinutes la durée du verrouillage en minutes
+ */
+ public void sendAccountLockedEmail(User user, int lockDurationMinutes) {
+ if (!emailNotificationsEnabled) {
+ logger.debug("Notifications email désactivées, email de verrouillage non envoyé");
+ return;
+ }
+
+ if (user == null || user.getEmail() == null) {
+ logger.error("Impossible d'envoyer l'email de verrouillage: utilisateur ou email null");
+ throw new IllegalArgumentException("Utilisateur et email requis");
+ }
+
+ logger.info("SIMULATION - Envoi de l'email de verrouillage de compte à: {}", user.getEmail());
+ logger.debug("Contenu de l'email de verrouillage:");
+ logger.debug("Destinataire: {} {}", user.getFirstName(), user.getLastName());
+ logger.debug("Email: {}", user.getEmail());
+ logger.debug("Durée du verrouillage: {} minutes", lockDurationMinutes);
+
+ logger.info("Email de verrouillage simulé avec succès pour: {}", user.getEmail());
+ }
+
+ /**
+ * Simule l'envoi d'un email générique.
+ *
+ * @param to l'adresse email de destination
+ * @param subject le sujet de l'email
+ * @param htmlContent le contenu HTML
+ */
+ public void sendEmail(String to, String subject, String htmlContent) {
+ if (!emailNotificationsEnabled) {
+ logger.debug("Notifications email désactivées, email non envoyé");
+ return;
+ }
+
+ if (to == null || subject == null || htmlContent == null) {
+ logger.error("Impossible d'envoyer l'email: paramètres manquants");
+ throw new IllegalArgumentException("Destinataire, sujet et contenu requis");
+ }
+
+ logger.info("SIMULATION - Envoi d'email à: {}", to);
+ logger.debug("Sujet: {}", subject);
+ logger.debug("Contenu HTML: {} caractères", htmlContent.length());
+
+ logger.info("Email générique simulé avec succès pour: {}", to);
+ }
+
+ /**
+ * Génère le contenu HTML de l'email de bienvenue.
+ *
+ * @param user l'utilisateur
+ * @param temporaryPassword le mot de passe temporaire (optionnel)
+ * @return le contenu HTML
+ */
+ public String generateWelcomeEmailContent(User user, String temporaryPassword) {
+ StringBuilder html = new StringBuilder();
+ html.append("");
+ html.append("
Bienvenue");
+ html.append("");
+ html.append("
Bienvenue sur ").append(applicationName).append(" !
");
+ html.append("
Bonjour ").append(user.getFirstName()).append(" ").append(user.getLastName()).append(",
");
+ html.append("
Votre compte a été créé avec succès sur la plateforme GBCM.
");
+ html.append("
Email : ").append(user.getEmail()).append("
");
+
+ if (temporaryPassword != null) {
+ html.append("
Mot de passe temporaire : ").append(temporaryPassword).append("
");
+ html.append("
Important : Veuillez changer votre mot de passe lors de votre première connexion.
");
+ }
+
+ html.append("
Vous pouvez maintenant vous connecter et commencer à utiliser nos services de conseil en gestion d'entreprise.
");
+ html.append("
Si vous avez des questions, n'hésitez pas à nous contacter.
");
+ html.append("
Cordialement,
L'équipe GBCM
");
+ html.append("
");
+ html.append("
Cet email a été envoyé automatiquement le ");
+ html.append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy à HH:mm")));
+ html.append("
");
+ html.append("
");
+
+ return html.toString();
+ }
+
+ /**
+ * Génère le contenu HTML de l'email de réinitialisation de mot de passe.
+ *
+ * @param user l'utilisateur
+ * @param resetToken le token de réinitialisation
+ * @param resetUrl l'URL de réinitialisation
+ * @return le contenu HTML
+ */
+ public String generatePasswordResetEmailContent(User user, String resetToken, String resetUrl) {
+ StringBuilder html = new StringBuilder();
+ html.append("");
+ html.append("Réinitialisation de mot de passe");
+ html.append("");
+ html.append("
Réinitialisation de votre mot de passe
");
+ html.append("
Bonjour ").append(user.getFirstName()).append(" ").append(user.getLastName()).append(",
");
+ html.append("
Vous avez demandé la réinitialisation de votre mot de passe sur ").append(applicationName).append(".
");
+ html.append("
Cliquez sur le lien ci-dessous pour créer un nouveau mot de passe :
");
+ html.append("
Réinitialiser mon mot de passe
");
+ html.append("
Si le lien ne fonctionne pas, copiez et collez cette URL dans votre navigateur :
");
+ html.append("
").append(resetUrl).append("
");
+ html.append("
Important : Ce lien expire dans 2 heures pour des raisons de sécurité.
");
+ html.append("
Si vous n'avez pas demandé cette réinitialisation, ignorez cet email. Votre mot de passe actuel reste inchangé.
");
+ html.append("
Cordialement,
L'équipe GBCM
");
+ html.append("
");
+ html.append("
Cet email a été envoyé automatiquement le ");
+ html.append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy à HH:mm")));
+ html.append("
");
+ html.append("
");
+
+ return html.toString();
+ }
+
+ /**
+ * 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
+ */
+ public String generateAccountLockedEmailContent(User user, int lockDurationMinutes) {
+ StringBuilder html = new StringBuilder();
+ html.append("");
+ html.append("Compte verrouillé");
+ html.append("");
+ html.append("
Votre compte a été temporairement verrouillé
");
+ html.append("
Bonjour ").append(user.getFirstName()).append(" ").append(user.getLastName()).append(",
");
+ html.append("
Votre compte ").append(applicationName).append(" a été temporairement verrouillé suite à plusieurs tentatives de connexion échouées.
");
+ html.append("
Durée du verrouillage : ").append(lockDurationMinutes).append(" minutes
");
+ html.append("
Vous pourrez vous reconnecter après cette période.
");
+ html.append("
Mesure de sécurité : Si vous n'êtes pas à l'origine de ces tentatives de connexion, nous vous recommandons de changer votre mot de passe dès que possible.
");
+ html.append("
Si vous avez des questions concernant la sécurité de votre compte, contactez-nous immédiatement.
");
+ html.append("
Cordialement,
L'équipe GBCM
");
+ html.append("
");
+ html.append("
Cet email a été envoyé automatiquement le ");
+ html.append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy à HH:mm")));
+ html.append("
");
+ html.append("
");
+
+ return html.toString();
+ }
+}
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
new file mode 100644
index 0000000..d4f5ba4
--- /dev/null
+++ b/src/main/java/com/gbcm/server/impl/service/security/JwtService.java
@@ -0,0 +1,313 @@
+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
new file mode 100644
index 0000000..3e5ac5e
--- /dev/null
+++ b/src/main/java/com/gbcm/server/impl/service/security/PasswordService.java
@@ -0,0 +1,305 @@
+package com.gbcm.server.impl.service.security;
+
+import io.quarkus.elytron.security.common.BcryptUtil;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.SecureRandom;
+import java.util.regex.Pattern;
+
+/**
+ * Service de gestion des mots de passe pour la plateforme GBCM.
+ * Fournit les fonctionnalités de hachage, vérification et génération de mots de passe sécurisés.
+ *
+ * @author GBCM Development Team
+ * @version 1.0
+ * @since 1.0
+ */
+@ApplicationScoped
+public class PasswordService {
+
+ private static final Logger logger = LoggerFactory.getLogger(PasswordService.class);
+
+ /**
+ * Coût BCrypt par défaut (12 = très sécurisé mais plus lent).
+ */
+ private static final int DEFAULT_BCRYPT_COST = 12;
+
+ /**
+ * Longueur minimale d'un mot de passe.
+ */
+ private static final int MIN_PASSWORD_LENGTH = 8;
+
+ /**
+ * Longueur maximale d'un mot de passe.
+ */
+ private static final int MAX_PASSWORD_LENGTH = 128;
+
+ /**
+ * Pattern pour valider la complexité du mot de passe.
+ * Doit contenir au moins: 1 minuscule, 1 majuscule, 1 chiffre, 1 caractère spécial.
+ */
+ private static final Pattern PASSWORD_PATTERN = Pattern.compile(
+ "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"
+ );
+
+ /**
+ * Caractères utilisés pour la génération de mots de passe.
+ */
+ private static final String PASSWORD_CHARS =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@$!%*?&";
+
+ private final SecureRandom secureRandom = new SecureRandom();
+
+ /**
+ * Hache un mot de passe en utilisant BCrypt.
+ *
+ * @param plainPassword le mot de passe en clair
+ * @return le hash BCrypt du mot de passe
+ * @throws IllegalArgumentException si le mot de passe est null ou vide
+ */
+ public String hashPassword(String plainPassword) {
+ if (plainPassword == null || plainPassword.trim().isEmpty()) {
+ throw new IllegalArgumentException("Le mot de passe ne peut pas être null ou vide");
+ }
+
+ logger.debug("Hachage d'un mot de passe avec BCrypt (coût: {})", DEFAULT_BCRYPT_COST);
+
+ try {
+ String hashedPassword = BcryptUtil.bcryptHash(plainPassword, DEFAULT_BCRYPT_COST);
+ logger.debug("Mot de passe haché avec succès");
+ return hashedPassword;
+ } catch (Exception e) {
+ logger.error("Erreur lors du hachage du mot de passe: {}", e.getMessage());
+ throw new RuntimeException("Erreur lors du hachage du mot de passe", e);
+ }
+ }
+
+ /**
+ * Vérifie si un mot de passe en clair correspond au hash stocké.
+ *
+ * @param plainPassword le mot de passe en clair à vérifier
+ * @param hashedPassword le hash stocké
+ * @return true si le mot de passe correspond, false sinon
+ * @throws IllegalArgumentException si l'un des paramètres est null
+ */
+ public boolean verifyPassword(String plainPassword, String hashedPassword) {
+ if (plainPassword == null) {
+ throw new IllegalArgumentException("Le mot de passe en clair ne peut pas être null");
+ }
+
+ if (hashedPassword == null) {
+ throw new IllegalArgumentException("Le hash du mot de passe ne peut pas être null");
+ }
+
+ logger.debug("Vérification d'un mot de passe avec BCrypt");
+
+ try {
+ boolean matches = BcryptUtil.matches(plainPassword, hashedPassword);
+ logger.debug("Résultat de la vérification: {}", matches ? "succès" : "échec");
+ return matches;
+ } catch (Exception e) {
+ logger.error("Erreur lors de la vérification du mot de passe: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 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
+ */
+ public boolean isPasswordValid(String password) {
+ if (password == null) {
+ logger.debug("Validation échouée: mot de passe null");
+ return false;
+ }
+
+ // Vérifier la longueur
+ if (password.length() < MIN_PASSWORD_LENGTH) {
+ logger.debug("Validation échouée: mot de passe trop court (min: {})", MIN_PASSWORD_LENGTH);
+ return false;
+ }
+
+ if (password.length() > MAX_PASSWORD_LENGTH) {
+ logger.debug("Validation échouée: mot de passe trop long (max: {})", MAX_PASSWORD_LENGTH);
+ return false;
+ }
+
+ // Vérifier la complexité
+ boolean isComplex = PASSWORD_PATTERN.matcher(password).matches();
+ if (!isComplex) {
+ logger.debug("Validation échouée: mot de passe ne respecte pas les règles de complexité");
+ }
+
+ return isComplex;
+ }
+
+ /**
+ * Génère un mot de passe aléatoire sécurisé.
+ *
+ * @param length la longueur souhaitée du mot de passe
+ * @return un mot de passe généré aléatoirement
+ * @throws IllegalArgumentException si la longueur est invalide
+ */
+ public String generateRandomPassword(int length) {
+ if (length < MIN_PASSWORD_LENGTH) {
+ throw new IllegalArgumentException(
+ String.format("La longueur doit être au moins %d caractères", MIN_PASSWORD_LENGTH)
+ );
+ }
+
+ if (length > MAX_PASSWORD_LENGTH) {
+ throw new IllegalArgumentException(
+ String.format("La longueur ne peut pas dépasser %d caractères", MAX_PASSWORD_LENGTH)
+ );
+ }
+
+ logger.debug("Génération d'un mot de passe aléatoire de {} caractères", length);
+
+ StringBuilder password = new StringBuilder(length);
+
+ // S'assurer qu'on a au moins un caractère de chaque type requis
+ password.append(getRandomChar("abcdefghijklmnopqrstuvwxyz")); // minuscule
+ password.append(getRandomChar("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); // majuscule
+ password.append(getRandomChar("0123456789")); // chiffre
+ password.append(getRandomChar("@$!%*?&")); // caractère spécial
+
+ // Compléter avec des caractères aléatoires
+ for (int i = 4; i < length; i++) {
+ password.append(getRandomChar(PASSWORD_CHARS));
+ }
+
+ // Mélanger les caractères pour éviter un pattern prévisible
+ return shuffleString(password.toString());
+ }
+
+ /**
+ * Génère un mot de passe aléatoire avec la longueur par défaut (12 caractères).
+ *
+ * @return un mot de passe généré aléatoirement
+ */
+ public String generateRandomPassword() {
+ return generateRandomPassword(12);
+ }
+
+ /**
+ * Vérifie si un mot de passe doit être renouvelé.
+ *
+ * @param lastPasswordChange la date du dernier changement de mot de passe
+ * @param maxAgeInDays l'âge maximum autorisé en jours
+ * @return true si le mot de passe doit être renouvelé, false sinon
+ */
+ public boolean shouldRenewPassword(java.time.LocalDateTime lastPasswordChange, int maxAgeInDays) {
+ if (lastPasswordChange == null) {
+ logger.debug("Renouvellement requis: aucune date de dernier changement");
+ return true;
+ }
+
+ java.time.LocalDateTime expirationDate = lastPasswordChange.plusDays(maxAgeInDays);
+ boolean shouldRenew = java.time.LocalDateTime.now().isAfter(expirationDate);
+
+ logger.debug("Vérification du renouvellement: dernier changement={}, expiration={}, renouvellement requis={}",
+ lastPasswordChange, expirationDate, shouldRenew);
+
+ return shouldRenew;
+ }
+
+ /**
+ * Calcule la force d'un mot de passe sur une échelle de 0 à 100.
+ *
+ * @param password le mot de passe à évaluer
+ * @return un score de 0 (très faible) à 100 (très fort)
+ */
+ public int calculatePasswordStrength(String password) {
+ if (password == null || password.isEmpty()) {
+ return 0;
+ }
+
+ int score = 0;
+
+ // Longueur (max 25 points)
+ score += Math.min(password.length() * 2, 25);
+
+ // Variété de caractères (max 25 points chacun)
+ if (password.matches(".*[a-z].*")) score += 10; // minuscules
+ if (password.matches(".*[A-Z].*")) score += 10; // majuscules
+ if (password.matches(".*\\d.*")) score += 10; // chiffres
+ if (password.matches(".*[@$!%*?&].*")) score += 15; // caractères spéciaux
+
+ // Complexité supplémentaire (max 40 points)
+ if (password.length() >= 12) score += 10;
+ if (password.matches(".*[a-z].*[A-Z].*") || password.matches(".*[A-Z].*[a-z].*")) score += 10;
+ if (password.matches(".*\\d.*[@$!%*?&].*") || password.matches(".*[@$!%*?&].*\\d.*")) score += 10;
+ if (!hasRepeatingCharacters(password)) score += 10;
+
+ return Math.min(score, 100);
+ }
+
+ /**
+ * Obtient un message descriptif de la force du mot de passe.
+ *
+ * @param password le mot de passe à évaluer
+ * @return un message décrivant la force du mot de passe
+ */
+ public String getPasswordStrengthMessage(String password) {
+ int strength = calculatePasswordStrength(password);
+
+ if (strength < 30) {
+ return "Très faible - Augmentez la longueur et ajoutez différents types de caractères";
+ } else if (strength < 50) {
+ return "Faible - Ajoutez des majuscules, chiffres et caractères spéciaux";
+ } else if (strength < 70) {
+ return "Moyen - Augmentez la longueur ou la complexité";
+ } else if (strength < 90) {
+ return "Fort - Bon niveau de sécurité";
+ } else {
+ return "Très fort - Excellent niveau de sécurité";
+ }
+ }
+
+ /**
+ * Obtient un caractère aléatoire d'une chaîne donnée.
+ *
+ * @param chars la chaîne de caractères
+ * @return un caractère aléatoire
+ */
+ private char getRandomChar(String chars) {
+ return chars.charAt(secureRandom.nextInt(chars.length()));
+ }
+
+ /**
+ * Mélange les caractères d'une chaîne.
+ *
+ * @param input la chaîne à mélanger
+ * @return la chaîne mélangée
+ */
+ private String shuffleString(String input) {
+ char[] chars = input.toCharArray();
+ for (int i = chars.length - 1; i > 0; i--) {
+ int j = secureRandom.nextInt(i + 1);
+ char temp = chars[i];
+ chars[i] = chars[j];
+ chars[j] = temp;
+ }
+ return new String(chars);
+ }
+
+ /**
+ * Vérifie si un mot de passe contient des caractères répétitifs.
+ *
+ * @param password le mot de passe à vérifier
+ * @return true si des caractères se répètent, false sinon
+ */
+ private boolean hasRepeatingCharacters(String password) {
+ for (int i = 0; i < password.length() - 2; i++) {
+ if (password.charAt(i) == password.charAt(i + 1) &&
+ password.charAt(i + 1) == password.charAt(i + 2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
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
new file mode 100644
index 0000000..acc57f3
--- /dev/null
+++ b/src/main/java/com/gbcm/server/impl/service/security/SecurityService.java
@@ -0,0 +1,339 @@
+package com.gbcm.server.impl.service.security;
+
+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.
+ * Gère l'authentification, l'autorisation et les contrôles de sécurité.
+ *
+ * @author GBCM Development Team
+ * @version 1.0
+ * @since 1.0
+ */
+@ApplicationScoped
+public class SecurityService {
+
+ private static final Logger logger = LoggerFactory.getLogger(SecurityService.class);
+
+ /**
+ * Nombre maximum de tentatives de connexion échouées avant verrouillage.
+ */
+ private static final int MAX_FAILED_LOGIN_ATTEMPTS = 5;
+
+ /**
+ * Durée de verrouillage du compte en minutes après dépassement des tentatives.
+ */
+ private static final int ACCOUNT_LOCK_DURATION_MINUTES = 30;
+
+ @Inject
+ PasswordService passwordService;
+
+ @Inject
+ JwtService jwtService;
+
+ @Inject
+ SecurityIdentity securityIdentity;
+
+ @Inject
+ JsonWebToken jwt;
+
+ /**
+ * Authentifie un utilisateur avec email et mot de passe.
+ *
+ * @param user l'utilisateur à authentifier
+ * @param plainPassword le mot de passe en clair
+ * @param clientIp l'adresse IP du client
+ * @return true si l'authentification réussit, false sinon
+ * @throws AuthenticationException si l'authentification échoue
+ */
+ public boolean authenticateUser(User user, String plainPassword, String clientIp)
+ throws AuthenticationException {
+
+ if (user == null) {
+ logger.warn("Tentative d'authentification avec utilisateur null");
+ throw new AuthenticationException("Utilisateur invalide", "AUTH_INVALID_USER");
+ }
+
+ if (plainPassword == null || plainPassword.trim().isEmpty()) {
+ logger.warn("Tentative d'authentification avec mot de passe vide pour: {}", user.getEmail());
+ throw new AuthenticationException("Mot de passe requis", "AUTH_PASSWORD_REQUIRED");
+ }
+
+ logger.debug("Tentative d'authentification pour l'utilisateur: {}", user.getEmail());
+
+ // Vérifier si le compte est verrouillé
+ if (user.isLocked()) {
+ logger.warn("Tentative de connexion sur compte verrouillé: {}", user.getEmail());
+ throw new AuthenticationException("Compte temporairement verrouillé", "AUTH_ACCOUNT_LOCKED");
+ }
+
+ // Vérifier si le compte est actif
+ if (!user.isActive()) {
+ logger.warn("Tentative de connexion sur compte inactif: {}", user.getEmail());
+ throw new AuthenticationException("Compte désactivé", "AUTH_ACCOUNT_DISABLED");
+ }
+
+ // Vérifier le mot de passe
+ boolean passwordMatches = passwordService.verifyPassword(plainPassword, user.getPasswordHash());
+
+ if (passwordMatches) {
+ // Authentification réussie
+ user.updateLastLogin(clientIp);
+ logger.info("Authentification réussie pour l'utilisateur: {}", user.getEmail());
+ return true;
+ } else {
+ // Authentification échouée
+ handleFailedLogin(user);
+ logger.warn("Authentification échouée pour l'utilisateur: {} (tentative {})",
+ user.getEmail(), user.getFailedLoginAttempts());
+ throw new AuthenticationException("Email ou mot de passe incorrect", "AUTH_INVALID_CREDENTIALS");
+ }
+ }
+
+ /**
+ * Vérifie si l'utilisateur actuel a l'autorisation pour un rôle spécifique.
+ *
+ * @param requiredRole le rôle requis
+ * @throws AuthorizationException si l'utilisateur n'a pas l'autorisation
+ */
+ public void requireRole(UserRole requiredRole) throws AuthorizationException {
+ if (requiredRole == null) {
+ throw new IllegalArgumentException("Le rôle requis ne peut pas être null");
+ }
+
+ UserRole currentRole = getCurrentUserRole();
+ if (currentRole == null) {
+ logger.warn("Tentative d'accès sans authentification pour le rôle: {}", requiredRole);
+ throw new AuthorizationException("Authentification requise", "AUTH_AUTHENTICATION_REQUIRED");
+ }
+
+ if (!hasRole(currentRole, requiredRole)) {
+ logger.warn("Accès refusé: rôle {} requis, rôle actuel {}", requiredRole, currentRole);
+ throw new AuthorizationException("Accès refusé - Privilèges insuffisants", "AUTH_INSUFFICIENT_PRIVILEGES");
+ }
+
+ logger.debug("Autorisation accordée pour le rôle: {}", requiredRole);
+ }
+
+ /**
+ * Vérifie si l'utilisateur actuel a l'un des rôles spécifiés.
+ *
+ * @param requiredRoles les rôles autorisés
+ * @throws AuthorizationException si l'utilisateur n'a aucun des rôles requis
+ */
+ public void requireAnyRole(UserRole... requiredRoles) throws AuthorizationException {
+ if (requiredRoles == null || requiredRoles.length == 0) {
+ throw new IllegalArgumentException("Au moins un rôle requis doit être spécifié");
+ }
+
+ UserRole currentRole = getCurrentUserRole();
+ if (currentRole == null) {
+ logger.warn("Tentative d'accès sans authentification");
+ throw new AuthorizationException("Authentification requise", "AUTH_AUTHENTICATION_REQUIRED");
+ }
+
+ for (UserRole requiredRole : requiredRoles) {
+ if (hasRole(currentRole, requiredRole)) {
+ logger.debug("Autorisation accordée pour le rôle: {}", requiredRole);
+ return;
+ }
+ }
+
+ logger.warn("Accès refusé: aucun des rôles requis {} ne correspond au rôle actuel {}",
+ Set.of(requiredRoles), currentRole);
+ throw new AuthorizationException("Accès refusé - Privilèges insuffisants", "AUTH_INSUFFICIENT_PRIVILEGES");
+ }
+
+ /**
+ * Vérifie si l'utilisateur actuel peut accéder aux données d'un autre utilisateur.
+ *
+ * @param targetUserId l'ID de l'utilisateur cible
+ * @throws AuthorizationException si l'accès n'est pas autorisé
+ */
+ public void requireUserAccessOrAdmin(Long targetUserId) throws AuthorizationException {
+ if (targetUserId == null) {
+ throw new IllegalArgumentException("L'ID utilisateur cible ne peut pas être null");
+ }
+
+ Long currentUserId = getCurrentUserId();
+ UserRole currentRole = getCurrentUserRole();
+
+ // Les admins peuvent accéder à tous les utilisateurs
+ if (currentRole == UserRole.ADMIN || currentRole == UserRole.MANAGER) {
+ logger.debug("Accès autorisé par privilège administrateur");
+ return;
+ }
+
+ // Les utilisateurs peuvent accéder à leurs propres données
+ if (currentUserId != null && currentUserId.equals(targetUserId)) {
+ logger.debug("Accès autorisé aux propres données utilisateur");
+ return;
+ }
+
+ logger.warn("Accès refusé: utilisateur {} tente d'accéder aux données de l'utilisateur {}",
+ currentUserId, targetUserId);
+ throw new AuthorizationException("Accès refusé - Vous ne pouvez accéder qu'à vos propres données",
+ "AUTH_ACCESS_DENIED");
+ }
+
+ /**
+ * Obtient l'ID de l'utilisateur actuellement connecté.
+ *
+ * @return l'ID de l'utilisateur ou null si non connecté
+ */
+ public Long getCurrentUserId() {
+ try {
+ if (jwt != null) {
+ return jwtService.extractUserId(jwt);
+ }
+ } catch (Exception e) {
+ logger.debug("Impossible d'extraire l'ID utilisateur du token: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * Obtient l'email de l'utilisateur actuellement connecté.
+ *
+ * @return l'email de l'utilisateur ou null si non connecté
+ */
+ public String getCurrentUserEmail() {
+ try {
+ if (jwt != null) {
+ return jwtService.extractEmail(jwt);
+ }
+ } catch (Exception e) {
+ logger.debug("Impossible d'extraire l'email du token: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * Obtient le rôle de l'utilisateur actuellement connecté.
+ *
+ * @return le rôle de l'utilisateur ou null si non connecté
+ */
+ public UserRole getCurrentUserRole() {
+ try {
+ if (securityIdentity != null && securityIdentity.getRoles() != null) {
+ Set roles = securityIdentity.getRoles();
+ if (!roles.isEmpty()) {
+ String roleString = roles.iterator().next();
+ return UserRole.valueOf(roleString);
+ }
+ }
+ } catch (Exception e) {
+ logger.debug("Impossible d'extraire le rôle du contexte de sécurité: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * Vérifie si l'utilisateur actuel est authentifié.
+ *
+ * @return true si authentifié, false sinon
+ */
+ public boolean isAuthenticated() {
+ return securityIdentity != null && !securityIdentity.isAnonymous();
+ }
+
+ /**
+ * Vérifie si l'utilisateur actuel a un rôle administrateur.
+ *
+ * @return true si admin ou manager, false sinon
+ */
+ public boolean isAdmin() {
+ UserRole currentRole = getCurrentUserRole();
+ return currentRole == UserRole.ADMIN || currentRole == UserRole.MANAGER;
+ }
+
+ /**
+ * Gère les tentatives de connexion échouées.
+ *
+ * @param user l'utilisateur pour lequel la connexion a échoué
+ */
+ private void handleFailedLogin(User user) {
+ user.incrementFailedLoginAttempts();
+
+ if (user.getFailedLoginAttempts() >= MAX_FAILED_LOGIN_ATTEMPTS) {
+ user.lockAccount(ACCOUNT_LOCK_DURATION_MINUTES);
+ logger.warn("Compte verrouillé pour {} minutes après {} tentatives échouées: {}",
+ ACCOUNT_LOCK_DURATION_MINUTES, MAX_FAILED_LOGIN_ATTEMPTS, user.getEmail());
+ }
+ }
+
+ /**
+ * Vérifie si un rôle actuel a les privilèges d'un rôle requis.
+ *
+ * @param currentRole le rôle actuel
+ * @param requiredRole le rôle requis
+ * @return true si autorisé, false sinon
+ */
+ private boolean hasRole(UserRole currentRole, UserRole requiredRole) {
+ if (currentRole == requiredRole) {
+ return true;
+ }
+
+ // Hiérarchie des rôles: ADMIN > MANAGER > COACH > CLIENT > PROSPECT
+ switch (requiredRole) {
+ case PROSPECT:
+ return true; // Tous les rôles peuvent accéder aux fonctionnalités prospect
+ case CLIENT:
+ return currentRole != UserRole.PROSPECT;
+ case COACH:
+ return currentRole == UserRole.ADMIN || currentRole == UserRole.MANAGER || currentRole == UserRole.COACH;
+ case MANAGER:
+ return currentRole == UserRole.ADMIN || currentRole == UserRole.MANAGER;
+ case ADMIN:
+ return currentRole == UserRole.ADMIN;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Valide qu'un token JWT n'est pas expiré.
+ *
+ * @param jwt le token à valider
+ * @throws AuthenticationException si le token est expiré
+ */
+ 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");
+ }
+ }
+
+ /**
+ * Enregistre une activité de sécurité dans les logs.
+ *
+ * @param action l'action effectuée
+ * @param details les détails de l'action
+ */
+ public void logSecurityEvent(String action, String details) {
+ String userInfo = getCurrentUserEmail();
+ if (userInfo == null) {
+ userInfo = "utilisateur anonyme";
+ }
+
+ logger.info("Événement de sécurité - Action: {}, Utilisateur: {}, Détails: {}",
+ action, userInfo, details);
+ }
+}