Initial commit: GBCM Server Quarkus implementation with JPA entities and services

This commit is contained in:
dahoud
2025-10-06 18:48:01 +00:00
commit e4d125e14c
6 changed files with 1037 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
package com.gbcm.server.entities;
import com.gbcm.server.api.enums.UserRole;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
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.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* Entité utilisateur pour l'authentification et la gestion des profils
*/
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_user_email", columnList = "email", unique = true),
@Index(name = "idx_user_role", columnList = "role"),
@Index(name = "idx_user_active", columnList = "active")
})
@UserDefinition
public class User extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(name = "first_name", nullable = false, length = 100)
@NotBlank(message = "Le prénom est obligatoire")
public String firstName;
@Column(name = "last_name", nullable = false, length = 100)
@NotBlank(message = "Le nom est obligatoire")
public String lastName;
@Column(name = "email", nullable = false, unique = true, length = 255)
@NotBlank(message = "L'email est obligatoire")
@Email(message = "Format d'email invalide")
@Username
public String email;
@Column(name = "password_hash", nullable = false)
@NotBlank(message = "Le mot de passe est obligatoire")
@Password
public String passwordHash;
@Column(name = "phone", length = 20)
public String phone;
@Column(name = "role", nullable = false, length = 20)
@Enumerated(EnumType.STRING)
@NotNull(message = "Le rôle est obligatoire")
@Roles
public UserRole role;
@Column(name = "active", nullable = false)
public boolean active = true;
@Column(name = "email_verified", nullable = false)
public boolean emailVerified = false;
@Column(name = "created_at", nullable = false)
public LocalDateTime createdAt;
@Column(name = "updated_at")
public LocalDateTime updatedAt;
@Column(name = "last_login_at")
public LocalDateTime lastLoginAt;
@Column(name = "password_reset_token")
public String passwordResetToken;
@Column(name = "password_reset_expires_at")
public LocalDateTime passwordResetExpiresAt;
@Column(name = "email_verification_token")
public String emailVerificationToken;
// Constructeurs
public User() {
this.createdAt = LocalDateTime.now();
}
public User(String firstName, String lastName, String email, String passwordHash, UserRole role) {
this();
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.passwordHash = passwordHash;
this.role = role;
}
// Méthodes utilitaires
public String getFullName() {
return firstName + " " + lastName;
}
public String getInitials() {
String firstInitial = firstName != null && !firstName.isEmpty() ?
firstName.substring(0, 1).toUpperCase() : "";
String lastInitial = lastName != null && !lastName.isEmpty() ?
lastName.substring(0, 1).toUpperCase() : "";
return firstInitial + lastInitial;
}
public boolean isPasswordResetTokenValid() {
return passwordResetToken != null &&
passwordResetExpiresAt != null &&
passwordResetExpiresAt.isAfter(LocalDateTime.now());
}
public void updateLastLogin() {
this.lastLoginAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Callbacks JPA
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
// Méthodes de recherche Panache
public static User findByEmail(String email) {
return find("email", email).firstResult();
}
public static User findByEmailAndActive(String email, boolean active) {
return find("email = ?1 and active = ?2", email, active).firstResult();
}
public static User findByPasswordResetToken(String token) {
return find("passwordResetToken", token).firstResult();
}
public static User findByEmailVerificationToken(String token) {
return find("emailVerificationToken", token).firstResult();
}
public static long countByRole(UserRole role) {
return count("role", role);
}
public static long countActiveUsers() {
return count("active", true);
}
// equals et hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) && Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(id, email);
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", role=" + role +
", active=" + active +
", createdAt=" + createdAt +
'}';
}
}

View File

@@ -0,0 +1,249 @@
package com.gbcm.server.services.impl;
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.entities.User;
import com.gbcm.server.services.security.JwtService;
import com.gbcm.server.utils.EmailUtils;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* Implémentation du service d'authentification
*/
@ApplicationScoped
public class AuthServiceImpl implements AuthService {
@Inject
JwtService jwtService;
@Inject
EmailUtils emailUtils;
@Override
@Transactional
public LoginResponseDTO login(@Valid LoginRequestDTO loginRequest)
throws AuthenticationException, GBCMException {
Log.infof("Tentative de connexion pour: %s", loginRequest.getEmail());
try {
// Recherche de l'utilisateur
User user = User.findByEmailAndActive(loginRequest.getEmail(), true);
if (user == null) {
Log.warnf("Utilisateur non trouvé ou inactif: %s", loginRequest.getEmail());
throw new AuthenticationException("Identifiants invalides");
}
// Vérification du mot de passe
if (!BcryptUtil.matches(loginRequest.getPassword(), user.passwordHash)) {
Log.warnf("Mot de passe incorrect pour: %s", loginRequest.getEmail());
throw new AuthenticationException("Identifiants invalides");
}
// Mise à jour de la dernière connexion
user.updateLastLogin();
user.persist();
// Génération du token JWT
String token = jwtService.generateToken(user);
LocalDateTime expiresAt = LocalDateTime.now().plusHours(1);
// Création de la réponse
UserDTO userDTO = mapToUserDTO(user);
LoginResponseDTO response = LoginResponseDTO.success(token, expiresAt, userDTO);
if (loginRequest.isRememberMe()) {
String refreshToken = jwtService.generateRefreshToken(user);
response.setRefreshToken(refreshToken);
}
Log.infof("Connexion réussie pour: %s", loginRequest.getEmail());
return response;
} catch (AuthenticationException e) {
throw e;
} catch (Exception e) {
Log.errorf(e, "Erreur lors de la connexion pour: %s", loginRequest.getEmail());
throw new GBCMException("Erreur système lors de la connexion");
}
}
@Override
@Transactional
public void logout(String authToken) throws AuthenticationException {
try {
// Extraction du token (suppression du préfixe "Bearer ")
String token = extractToken(authToken);
// Validation et extraction des informations utilisateur
UserDTO userDTO = jwtService.validateToken(token);
// Ajout du token à la blacklist
jwtService.blacklistToken(token);
Log.infof("Déconnexion réussie pour l'utilisateur: %s", userDTO.getEmail());
} catch (Exception e) {
Log.errorf(e, "Erreur lors de la déconnexion");
throw new AuthenticationException("Erreur lors de la déconnexion");
}
}
@Override
public LoginResponseDTO refreshToken(String refreshToken) throws AuthenticationException {
try {
// Validation du refresh token
UserDTO userDTO = jwtService.validateRefreshToken(refreshToken);
// Recherche de l'utilisateur
User user = User.findByEmail(userDTO.getEmail());
if (user == null || !user.active) {
throw new AuthenticationException("Utilisateur non trouvé ou inactif");
}
// Génération d'un nouveau token
String newToken = jwtService.generateToken(user);
LocalDateTime expiresAt = LocalDateTime.now().plusHours(1);
return LoginResponseDTO.success(newToken, expiresAt, userDTO);
} catch (Exception e) {
Log.errorf(e, "Erreur lors du rafraîchissement du token");
throw new AuthenticationException("Token de rafraîchissement invalide");
}
}
@Override
public UserDTO validateToken(String authToken) throws AuthenticationException {
try {
String token = extractToken(authToken);
return jwtService.validateToken(token);
} catch (Exception e) {
Log.errorf(e, "Erreur lors de la validation du token");
throw new AuthenticationException("Token invalide");
}
}
@Override
@Transactional
public void forgotPassword(String email) throws GBCMException {
try {
User user = User.findByEmailAndActive(email, true);
if (user == null) {
// Pour des raisons de sécurité, on ne révèle pas si l'email existe
Log.warnf("Demande de réinitialisation pour email inexistant: %s", email);
return;
}
// Génération du token de réinitialisation
String resetToken = UUID.randomUUID().toString();
user.passwordResetToken = resetToken;
user.passwordResetExpiresAt = LocalDateTime.now().plusHours(24);
user.persist();
// Envoi de l'email
emailUtils.sendPasswordResetEmail(user.email, user.getFullName(), resetToken);
Log.infof("Email de réinitialisation envoyé à: %s", email);
} catch (Exception e) {
Log.errorf(e, "Erreur lors de l'envoi de l'email de réinitialisation");
throw new GBCMException("Erreur lors de l'envoi de l'email");
}
}
@Override
@Transactional
public void resetPassword(String resetToken, String newPassword) throws GBCMException {
try {
User user = User.findByPasswordResetToken(resetToken);
if (user == null || !user.isPasswordResetTokenValid()) {
throw new GBCMException("Token de réinitialisation invalide ou expiré");
}
// Mise à jour du mot de passe
user.passwordHash = BcryptUtil.bcryptHash(newPassword);
user.passwordResetToken = null;
user.passwordResetExpiresAt = null;
user.updatedAt = LocalDateTime.now();
user.persist();
Log.infof("Mot de passe réinitialisé pour: %s", user.email);
} catch (GBCMException e) {
throw e;
} catch (Exception e) {
Log.errorf(e, "Erreur lors de la réinitialisation du mot de passe");
throw new GBCMException("Erreur lors de la réinitialisation");
}
}
@Override
@Transactional
public void changePassword(String authToken, String oldPassword, String newPassword)
throws AuthenticationException, GBCMException {
try {
// Validation du token et récupération de l'utilisateur
UserDTO userDTO = validateToken(authToken);
User user = User.findByEmail(userDTO.getEmail());
if (user == null) {
throw new AuthenticationException("Utilisateur non trouvé");
}
// Vérification de l'ancien mot de passe
if (!BcryptUtil.matches(oldPassword, user.passwordHash)) {
throw new GBCMException("Ancien mot de passe incorrect");
}
// Mise à jour du mot de passe
user.passwordHash = BcryptUtil.bcryptHash(newPassword);
user.updatedAt = LocalDateTime.now();
user.persist();
Log.infof("Mot de passe changé pour: %s", user.email);
} catch (AuthenticationException | GBCMException e) {
throw e;
} catch (Exception e) {
Log.errorf(e, "Erreur lors du changement de mot de passe");
throw new GBCMException("Erreur lors du changement de mot de passe");
}
}
// Méthodes utilitaires privées
private String extractToken(String authToken) throws AuthenticationException {
if (authToken == null || !authToken.startsWith("Bearer ")) {
throw new AuthenticationException("Format de token invalide");
}
return authToken.substring(7);
}
private UserDTO mapToUserDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.id);
dto.setFirstName(user.firstName);
dto.setLastName(user.lastName);
dto.setEmail(user.email);
dto.setPhone(user.phone);
dto.setRole(user.role);
dto.setActive(user.active);
dto.setCreatedAt(user.createdAt);
dto.setUpdatedAt(user.updatedAt);
dto.setLastLoginAt(user.lastLoginAt);
return dto;
}
}

View File

@@ -0,0 +1,103 @@
# GBCM Server Configuration
quarkus.application.name=gbcm-server
quarkus.application.version=1.0.0-SNAPSHOT
# Server Configuration
quarkus.http.port=8081
quarkus.http.host=0.0.0.0
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:8080,https://gbcm.com
quarkus.http.cors.methods=GET,PUT,POST,DELETE,OPTIONS
quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with
# Database Configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=gbcm_server
quarkus.datasource.password=gbcm_server_password
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/gbcm_server
# Hibernate Configuration
quarkus.hibernate-orm.database.generation=validate
quarkus.hibernate-orm.log.sql=false
quarkus.hibernate-orm.sql-load-script=import.sql
# Flyway Configuration
quarkus.flyway.migrate-at-start=true
quarkus.flyway.locations=db/migration
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
# OpenAPI Configuration
quarkus.smallrye-openapi.path=/swagger-ui
quarkus.swagger-ui.always-include=true
quarkus.swagger-ui.path=/swagger
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
mp.openapi.extensions.smallrye.info.contact.email=support@gbcm.com
mp.openapi.extensions.smallrye.info.contact.name=GBCM Support
mp.openapi.extensions.smallrye.info.license.name=Proprietary
mp.openapi.extensions.smallrye.info.license.url=https://gbcm.com/license
# Health Check Configuration
quarkus.smallrye-health.root-path=/health
# Metrics Configuration
quarkus.micrometer.enabled=true
quarkus.micrometer.export.prometheus.enabled=true
# Email Configuration
quarkus.mailer.from=noreply@gbcm.com
quarkus.mailer.host=smtp.gmail.com
quarkus.mailer.port=587
quarkus.mailer.start-tls=REQUIRED
quarkus.mailer.username=${SMTP_USERNAME:}
quarkus.mailer.password=${SMTP_PASSWORD:}
# Cache Configuration
quarkus.cache.caffeine.default.initial-capacity=100
quarkus.cache.caffeine.default.maximum-size=1000
quarkus.cache.caffeine.default.expire-after-write=PT30M
# Logging Configuration
quarkus.log.level=INFO
quarkus.log.category."com.gbcm".level=DEBUG
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
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.flyway.migrate-at-start=false
# Test Profile
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:gbcm_server_test;DB_CLOSE_DELAY=-1
%test.quarkus.datasource.db-kind=h2
%test.quarkus.hibernate-orm.database.generation=drop-and-create
%test.quarkus.flyway.migrate-at-start=false
# Production Profile
%prod.quarkus.log.level=WARN
%prod.quarkus.hibernate-orm.log.sql=false
%prod.quarkus.datasource.jdbc.url=${DATABASE_URL}
%prod.quarkus.datasource.username=${DB_USERNAME}
%prod.quarkus.datasource.password=${DB_PASSWORD}
# Business Configuration
gbcm.business.workshop.max-participants=20
gbcm.business.coaching.session-duration=60
gbcm.business.billing.currency=USD
gbcm.business.notification.email.enabled=true
gbcm.business.notification.sms.enabled=false