Task 1.2 - Entités JPA fondamentales

- Création de BaseEntity avec audit trail et soft delete
- Création de l'entité User avec Quarkus Security JPA
- Création de l'entité Client avec informations d'entreprise
- Création de l'entité Coach avec informations professionnelles
- Relations JPA one-to-one entre User-Client et User-Coach
- Migrations Flyway V1, V2, V3 pour les tables
- Données de test dans import.sql
- Compilation réussie du module d'implémentation
This commit is contained in:
dahoud
2025-10-06 20:11:18 +00:00
parent e4d125e14c
commit 9d8ce834e8
10 changed files with 2127 additions and 429 deletions

View File

@@ -1,180 +0,0 @@
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,271 @@
package com.gbcm.server.impl.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* Entité de base pour toutes les entités GBCM.
* Fournit l'audit trail (createdAt, updatedAt, createdBy, updatedBy) et le soft delete.
*
* @author GBCM Development Team
* @version 1.0
* @since 1.0
*/
@MappedSuperclass
public abstract class BaseEntity extends PanacheEntityBase {
/**
* Date et heure de création de l'enregistrement.
* Automatiquement définie lors de la persistance.
*/
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* Date et heure de dernière mise à jour de l'enregistrement.
* Automatiquement mise à jour lors des modifications.
*/
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
/**
* Identifiant de l'utilisateur qui a créé l'enregistrement.
* Doit être défini manuellement lors de la création.
*/
@Column(name = "created_by", length = 100)
private String createdBy;
/**
* Identifiant de l'utilisateur qui a effectué la dernière mise à jour.
* Doit être défini manuellement lors des modifications.
*/
@Column(name = "updated_by", length = 100)
private String updatedBy;
/**
* Indicateur de suppression logique (soft delete).
* true = supprimé, false = actif.
*/
@Column(name = "deleted", nullable = false)
private boolean deleted = false;
/**
* Date et heure de suppression logique.
* Définie lors du soft delete.
*/
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
/**
* Identifiant de l'utilisateur qui a effectué la suppression logique.
*/
@Column(name = "deleted_by", length = 100)
private String deletedBy;
/**
* Constructeur par défaut.
*/
protected BaseEntity() {}
/**
* Méthode appelée avant la persistance pour définir les champs d'audit.
*/
@PrePersist
protected void onCreate() {
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
if (updatedAt == null) {
updatedAt = LocalDateTime.now();
}
}
/**
* Méthode appelée avant la mise à jour pour mettre à jour les champs d'audit.
*/
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
/**
* Effectue une suppression logique de l'entité.
*
* @param deletedBy l'identifiant de l'utilisateur effectuant la suppression
*/
public void softDelete(String deletedBy) {
this.deleted = true;
this.deletedAt = LocalDateTime.now();
this.deletedBy = deletedBy;
}
/**
* Restaure une entité supprimée logiquement.
*/
public void restore() {
this.deleted = false;
this.deletedAt = null;
this.deletedBy = null;
}
/**
* Vérifie si l'entité est supprimée logiquement.
*
* @return true si l'entité est supprimée, false sinon
*/
public boolean isDeleted() {
return deleted;
}
/**
* Vérifie si l'entité est active (non supprimée).
*
* @return true si l'entité est active, false sinon
*/
public boolean isActive() {
return !deleted;
}
// Getters et Setters
/**
* Retourne la date de création.
*
* @return la date de création
*/
public LocalDateTime getCreatedAt() {
return createdAt;
}
/**
* Définit la date de création.
*
* @param createdAt la date de création
*/
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
/**
* Retourne la date de dernière mise à jour.
*
* @return la date de dernière mise à jour
*/
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
/**
* Définit la date de dernière mise à jour.
*
* @param updatedAt la date de dernière mise à jour
*/
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
/**
* Retourne l'identifiant de l'utilisateur créateur.
*
* @return l'identifiant du créateur
*/
public String getCreatedBy() {
return createdBy;
}
/**
* Définit l'identifiant de l'utilisateur créateur.
*
* @param createdBy l'identifiant du créateur
*/
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
/**
* Retourne l'identifiant de l'utilisateur ayant effectué la dernière mise à jour.
*
* @return l'identifiant du modificateur
*/
public String getUpdatedBy() {
return updatedBy;
}
/**
* Définit l'identifiant de l'utilisateur ayant effectué la dernière mise à jour.
*
* @param updatedBy l'identifiant du modificateur
*/
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}
/**
* Définit le statut de suppression logique.
*
* @param deleted true pour marquer comme supprimé, false sinon
*/
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
/**
* Retourne la date de suppression logique.
*
* @return la date de suppression
*/
public LocalDateTime getDeletedAt() {
return deletedAt;
}
/**
* Définit la date de suppression logique.
*
* @param deletedAt la date de suppression
*/
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
/**
* Retourne l'identifiant de l'utilisateur ayant effectué la suppression.
*
* @return l'identifiant du suppresseur
*/
public String getDeletedBy() {
return deletedBy;
}
/**
* Définit l'identifiant de l'utilisateur ayant effectué la suppression.
*
* @param deletedBy l'identifiant du suppresseur
*/
public void setDeletedBy(String deletedBy) {
this.deletedBy = deletedBy;
}
/**
* Représentation textuelle de l'entité de base.
*
* @return une chaîne représentant l'entité
*/
@Override
public String toString() {
return "BaseEntity{" +
"createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", createdBy='" + createdBy + '\'' +
", updatedBy='" + updatedBy + '\'' +
", deleted=" + deleted +
", deletedAt=" + deletedAt +
", deletedBy='" + deletedBy + '\'' +
'}';
}
}

View File

@@ -0,0 +1,531 @@
package com.gbcm.server.impl.entity;
import com.gbcm.server.api.enums.ServiceType;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* Entité représentant un client de la plateforme GBCM.
* Un client est associé à un utilisateur et peut avoir plusieurs services.
*
* @author GBCM Development Team
* @version 1.0
* @since 1.0
*/
@Entity
@Table(name = "clients", indexes = {
@Index(name = "idx_clients_user_id", columnList = "user_id", unique = true),
@Index(name = "idx_clients_company", columnList = "company_name"),
@Index(name = "idx_clients_status", columnList = "status"),
@Index(name = "idx_clients_deleted", columnList = "deleted")
})
@NamedQueries({
@NamedQuery(name = "Client.findByUserId",
query = "SELECT c FROM Client c WHERE c.user.id = :userId AND c.deleted = false"),
@NamedQuery(name = "Client.findByStatus",
query = "SELECT c FROM Client c WHERE c.status = :status AND c.deleted = false"),
@NamedQuery(name = "Client.findByCompanyName",
query = "SELECT c FROM Client c WHERE LOWER(c.companyName) LIKE LOWER(:companyName) AND c.deleted = false"),
@NamedQuery(name = "Client.findActiveClients",
query = "SELECT c FROM Client c WHERE c.status = 'ACTIVE' AND c.deleted = false")
})
public class Client extends BaseEntity {
/**
* Identifiant unique du client.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* Utilisateur associé à ce client.
* Relation one-to-one obligatoire.
*/
@OneToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false, unique = true,
foreignKey = @ForeignKey(name = "fk_clients_user_id"))
@NotNull(message = "L'utilisateur associé est obligatoire")
private User user;
/**
* Nom de l'entreprise du client.
*/
@Column(name = "company_name", nullable = false, length = 200)
@NotBlank(message = "Le nom de l'entreprise est obligatoire")
@Size(max = 200, message = "Le nom de l'entreprise ne peut pas dépasser 200 caractères")
private String companyName;
/**
* Secteur d'activité de l'entreprise.
*/
@Column(name = "industry", length = 100)
@Size(max = 100, message = "Le secteur d'activité ne peut pas dépasser 100 caractères")
private String industry;
/**
* Taille de l'entreprise (nombre d'employés).
*/
@Column(name = "company_size")
private Integer companySize;
/**
* Chiffre d'affaires annuel de l'entreprise.
*/
@Column(name = "annual_revenue", precision = 15, scale = 2)
private BigDecimal annualRevenue;
/**
* Adresse de l'entreprise - ligne 1.
*/
@Column(name = "address_line1", length = 255)
@Size(max = 255, message = "L'adresse ligne 1 ne peut pas dépasser 255 caractères")
private String addressLine1;
/**
* Adresse de l'entreprise - ligne 2.
*/
@Column(name = "address_line2", length = 255)
@Size(max = 255, message = "L'adresse ligne 2 ne peut pas dépasser 255 caractères")
private String addressLine2;
/**
* Ville de l'entreprise.
*/
@Column(name = "city", length = 100)
@Size(max = 100, message = "La ville ne peut pas dépasser 100 caractères")
private String city;
/**
* État/Province de l'entreprise.
*/
@Column(name = "state", length = 100)
@Size(max = 100, message = "L'état ne peut pas dépasser 100 caractères")
private String state;
/**
* Code postal de l'entreprise.
*/
@Column(name = "postal_code", length = 20)
@Size(max = 20, message = "Le code postal ne peut pas dépasser 20 caractères")
private String postalCode;
/**
* Pays de l'entreprise.
*/
@Column(name = "country", length = 100)
@Size(max = 100, message = "Le pays ne peut pas dépasser 100 caractères")
private String country;
/**
* Site web de l'entreprise.
*/
@Column(name = "website", length = 255)
@Size(max = 255, message = "Le site web ne peut pas dépasser 255 caractères")
private String website;
/**
* Statut du client.
*/
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
@NotNull(message = "Le statut est obligatoire")
private ClientStatus status = ClientStatus.PROSPECT;
/**
* Date de conversion de prospect à client.
*/
@Column(name = "converted_at")
private LocalDateTime convertedAt;
/**
* Type de service principal du client.
*/
@Enumerated(EnumType.STRING)
@Column(name = "primary_service_type", length = 30)
private ServiceType primaryServiceType;
/**
* Valeur totale des contrats du client.
*/
@Column(name = "total_contract_value", precision = 15, scale = 2)
private BigDecimal totalContractValue = BigDecimal.ZERO;
/**
* Date de début de la relation client.
*/
@Column(name = "relationship_start_date")
private LocalDate relationshipStartDate;
/**
* Notes internes sur le client.
*/
@Column(name = "notes", columnDefinition = "TEXT")
private String notes;
/**
* Source d'acquisition du client.
*/
@Column(name = "acquisition_source", length = 100)
@Size(max = 100, message = "La source d'acquisition ne peut pas dépasser 100 caractères")
private String acquisitionSource;
/**
* Constructeur par défaut.
*/
public Client() {
super();
}
/**
* Constructeur avec les champs obligatoires.
*
* @param user l'utilisateur associé
* @param companyName le nom de l'entreprise
*/
public Client(User user, String companyName) {
this();
this.user = user;
this.companyName = companyName;
}
/**
* Convertit un prospect en client.
*/
public void convertToClient() {
if (this.status == ClientStatus.PROSPECT) {
this.status = ClientStatus.ACTIVE;
this.convertedAt = LocalDateTime.now();
this.relationshipStartDate = LocalDate.now();
}
}
/**
* Désactive le client.
*/
public void deactivate() {
this.status = ClientStatus.INACTIVE;
}
/**
* Réactive le client.
*/
public void reactivate() {
this.status = ClientStatus.ACTIVE;
}
/**
* Ajoute une valeur au contrat total.
*
* @param amount le montant à ajouter
*/
public void addContractValue(BigDecimal amount) {
if (amount != null && amount.compareTo(BigDecimal.ZERO) > 0) {
this.totalContractValue = this.totalContractValue.add(amount);
}
}
/**
* Retourne l'adresse complète formatée.
*
* @return l'adresse complète
*/
public String getFullAddress() {
StringBuilder address = new StringBuilder();
if (addressLine1 != null && !addressLine1.trim().isEmpty()) {
address.append(addressLine1);
}
if (addressLine2 != null && !addressLine2.trim().isEmpty()) {
if (address.length() > 0) address.append(", ");
address.append(addressLine2);
}
if (city != null && !city.trim().isEmpty()) {
if (address.length() > 0) address.append(", ");
address.append(city);
}
if (state != null && !state.trim().isEmpty()) {
if (address.length() > 0) address.append(", ");
address.append(state);
}
if (postalCode != null && !postalCode.trim().isEmpty()) {
if (address.length() > 0) address.append(" ");
address.append(postalCode);
}
if (country != null && !country.trim().isEmpty()) {
if (address.length() > 0) address.append(", ");
address.append(country);
}
return address.toString();
}
/**
* Vérifie si le client est actif.
*
* @return true si le client est actif, false sinon
*/
public boolean isActiveClient() {
return status == ClientStatus.ACTIVE;
}
/**
* Vérifie si le client est un prospect.
*
* @return true si c'est un prospect, false sinon
*/
public boolean isProspect() {
return status == ClientStatus.PROSPECT;
}
/**
* Méthode de recherche par ID utilisateur.
*
* @param userId l'ID de l'utilisateur
* @return le client trouvé ou null
*/
public static Client findByUserId(Long userId) {
return find("#Client.findByUserId", userId).firstResult();
}
/**
* Méthode de recherche par statut.
*
* @param status le statut à rechercher
* @return la liste des clients avec ce statut
*/
public static List<Client> findByStatus(ClientStatus status) {
return find("#Client.findByStatus", status).list();
}
/**
* Méthode de recherche par nom d'entreprise.
*
* @param companyName le nom d'entreprise à rechercher
* @return la liste des clients correspondants
*/
public static List<Client> findByCompanyName(String companyName) {
String searchPattern = "%" + companyName + "%";
return find("#Client.findByCompanyName", searchPattern).list();
}
/**
* Méthode de recherche des clients actifs.
*
* @return la liste des clients actifs
*/
public static List<Client> findActiveClients() {
return find("#Client.findActiveClients").list();
}
// Getters et Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public String getIndustry() {
return industry;
}
public void setIndustry(String industry) {
this.industry = industry;
}
public Integer getCompanySize() {
return companySize;
}
public void setCompanySize(Integer companySize) {
this.companySize = companySize;
}
public BigDecimal getAnnualRevenue() {
return annualRevenue;
}
public void setAnnualRevenue(BigDecimal annualRevenue) {
this.annualRevenue = annualRevenue;
}
public String getAddressLine1() {
return addressLine1;
}
public void setAddressLine1(String addressLine1) {
this.addressLine1 = addressLine1;
}
public String getAddressLine2() {
return addressLine2;
}
public void setAddressLine2(String addressLine2) {
this.addressLine2 = addressLine2;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
public ClientStatus getStatus() {
return status;
}
public void setStatus(ClientStatus status) {
this.status = status;
}
public LocalDateTime getConvertedAt() {
return convertedAt;
}
public void setConvertedAt(LocalDateTime convertedAt) {
this.convertedAt = convertedAt;
}
public ServiceType getPrimaryServiceType() {
return primaryServiceType;
}
public void setPrimaryServiceType(ServiceType primaryServiceType) {
this.primaryServiceType = primaryServiceType;
}
public BigDecimal getTotalContractValue() {
return totalContractValue;
}
public void setTotalContractValue(BigDecimal totalContractValue) {
this.totalContractValue = totalContractValue;
}
public LocalDate getRelationshipStartDate() {
return relationshipStartDate;
}
public void setRelationshipStartDate(LocalDate relationshipStartDate) {
this.relationshipStartDate = relationshipStartDate;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public String getAcquisitionSource() {
return acquisitionSource;
}
public void setAcquisitionSource(String acquisitionSource) {
this.acquisitionSource = acquisitionSource;
}
@Override
public String toString() {
return "Client{" +
"id=" + id +
", companyName='" + companyName + '\'' +
", industry='" + industry + '\'' +
", status=" + status +
", totalContractValue=" + totalContractValue +
", relationshipStartDate=" + relationshipStartDate +
'}';
}
/**
* Énumération des statuts de client.
*/
public enum ClientStatus {
/**
* Prospect - pas encore client.
*/
PROSPECT,
/**
* Client actif.
*/
ACTIVE,
/**
* Client inactif.
*/
INACTIVE,
/**
* Ancien client.
*/
FORMER
}
}

View File

@@ -0,0 +1,550 @@
package com.gbcm.server.impl.entity;
import com.gbcm.server.api.enums.ServiceType;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Set;
/**
* Entité représentant un coach de la plateforme GBCM.
* Un coach est associé à un utilisateur et peut offrir différents services.
*
* @author GBCM Development Team
* @version 1.0
* @since 1.0
*/
@Entity
@Table(name = "coaches", indexes = {
@Index(name = "idx_coaches_user_id", columnList = "user_id", unique = true),
@Index(name = "idx_coaches_status", columnList = "status"),
@Index(name = "idx_coaches_specialization", columnList = "specialization"),
@Index(name = "idx_coaches_deleted", columnList = "deleted")
})
@NamedQueries({
@NamedQuery(name = "Coach.findByUserId",
query = "SELECT c FROM Coach c WHERE c.user.id = :userId AND c.deleted = false"),
@NamedQuery(name = "Coach.findByStatus",
query = "SELECT c FROM Coach c WHERE c.status = :status AND c.deleted = false"),
@NamedQuery(name = "Coach.findBySpecialization",
query = "SELECT c FROM Coach c WHERE c.specialization = :specialization AND c.deleted = false"),
@NamedQuery(name = "Coach.findAvailableCoaches",
query = "SELECT c FROM Coach c WHERE c.status = 'ACTIVE' AND c.availableForBooking = true AND c.deleted = false")
})
public class Coach extends BaseEntity {
/**
* Identifiant unique du coach.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* Utilisateur associé à ce coach.
* Relation one-to-one obligatoire.
*/
@OneToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false, unique = true,
foreignKey = @ForeignKey(name = "fk_coaches_user_id"))
@NotNull(message = "L'utilisateur associé est obligatoire")
private User user;
/**
* Spécialisation principale du coach.
*/
@Column(name = "specialization", nullable = false, length = 100)
@NotBlank(message = "La spécialisation est obligatoire")
@Size(max = 100, message = "La spécialisation ne peut pas dépasser 100 caractères")
private String specialization;
/**
* Biographie professionnelle du coach.
*/
@Column(name = "bio", columnDefinition = "TEXT")
private String bio;
/**
* Années d'expérience du coach.
*/
@Column(name = "years_of_experience")
private Integer yearsOfExperience;
/**
* Certifications du coach.
*/
@Column(name = "certifications", columnDefinition = "TEXT")
private String certifications;
/**
* Langues parlées par le coach.
*/
@Column(name = "languages", length = 255)
@Size(max = 255, message = "Les langues ne peuvent pas dépasser 255 caractères")
private String languages;
/**
* Tarif horaire du coach pour les sessions individuelles.
*/
@Column(name = "hourly_rate", precision = 10, scale = 2)
private BigDecimal hourlyRate;
/**
* Statut du coach.
*/
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
@NotNull(message = "Le statut est obligatoire")
private CoachStatus status = CoachStatus.ACTIVE;
/**
* Disponibilité pour les réservations.
*/
@Column(name = "available_for_booking", nullable = false)
private boolean availableForBooking = true;
/**
* Types de services offerts par le coach.
*/
@ElementCollection(targetClass = ServiceType.class)
@CollectionTable(name = "coach_service_types",
joinColumns = @JoinColumn(name = "coach_id"),
foreignKey = @ForeignKey(name = "fk_coach_service_types_coach_id"))
@Column(name = "service_type", length = 30)
@Enumerated(EnumType.STRING)
private Set<ServiceType> serviceTypes;
/**
* Heures de travail - début.
*/
@Column(name = "working_hours_start")
private LocalTime workingHoursStart;
/**
* Heures de travail - fin.
*/
@Column(name = "working_hours_end")
private LocalTime workingHoursEnd;
/**
* Jours de travail (format: MONDAY,TUESDAY,WEDNESDAY...).
*/
@Column(name = "working_days", length = 100)
@Size(max = 100, message = "Les jours de travail ne peuvent pas dépasser 100 caractères")
private String workingDays;
/**
* Fuseau horaire du coach.
*/
@Column(name = "timezone", length = 50)
@Size(max = 50, message = "Le fuseau horaire ne peut pas dépasser 50 caractères")
private String timezone;
/**
* Date de début d'activité comme coach.
*/
@Column(name = "start_date")
private LocalDate startDate;
/**
* Date de fin d'activité comme coach.
*/
@Column(name = "end_date")
private LocalDate endDate;
/**
* Note moyenne du coach (calculée à partir des évaluations).
*/
@Column(name = "average_rating", precision = 3, scale = 2)
private BigDecimal averageRating;
/**
* Nombre total d'évaluations reçues.
*/
@Column(name = "total_ratings", nullable = false)
private int totalRatings = 0;
/**
* Nombre total de sessions effectuées.
*/
@Column(name = "total_sessions", nullable = false)
private int totalSessions = 0;
/**
* Revenus totaux générés.
*/
@Column(name = "total_revenue", precision = 15, scale = 2)
private BigDecimal totalRevenue = BigDecimal.ZERO;
/**
* Notes internes sur le coach.
*/
@Column(name = "notes", columnDefinition = "TEXT")
private String notes;
/**
* Constructeur par défaut.
*/
public Coach() {
super();
}
/**
* Constructeur avec les champs obligatoires.
*
* @param user l'utilisateur associé
* @param specialization la spécialisation du coach
*/
public Coach(User user, String specialization) {
this();
this.user = user;
this.specialization = specialization;
this.startDate = LocalDate.now();
}
/**
* Active le coach.
*/
public void activate() {
this.status = CoachStatus.ACTIVE;
this.availableForBooking = true;
}
/**
* Désactive le coach.
*/
public void deactivate() {
this.status = CoachStatus.INACTIVE;
this.availableForBooking = false;
}
/**
* Met le coach en congé.
*/
public void setOnLeave() {
this.status = CoachStatus.ON_LEAVE;
this.availableForBooking = false;
}
/**
* Termine l'activité du coach.
*/
public void terminate() {
this.status = CoachStatus.TERMINATED;
this.availableForBooking = false;
this.endDate = LocalDate.now();
}
/**
* Met à jour la note moyenne après une nouvelle évaluation.
*
* @param newRating la nouvelle note reçue
*/
public void updateRating(BigDecimal newRating) {
if (newRating != null && newRating.compareTo(BigDecimal.ZERO) > 0) {
if (averageRating == null) {
averageRating = newRating;
totalRatings = 1;
} else {
BigDecimal totalScore = averageRating.multiply(new BigDecimal(totalRatings));
totalScore = totalScore.add(newRating);
totalRatings++;
averageRating = totalScore.divide(new BigDecimal(totalRatings), 2, java.math.RoundingMode.HALF_UP);
}
}
}
/**
* Incrémente le nombre de sessions effectuées.
*/
public void incrementSessionCount() {
this.totalSessions++;
}
/**
* Ajoute des revenus au total.
*
* @param amount le montant à ajouter
*/
public void addRevenue(BigDecimal amount) {
if (amount != null && amount.compareTo(BigDecimal.ZERO) > 0) {
this.totalRevenue = this.totalRevenue.add(amount);
}
}
/**
* Vérifie si le coach est disponible pour les réservations.
*
* @return true si disponible, false sinon
*/
public boolean isAvailableForBooking() {
return availableForBooking && status == CoachStatus.ACTIVE;
}
/**
* Vérifie si le coach offre un type de service spécifique.
*
* @param serviceType le type de service à vérifier
* @return true si le coach offre ce service, false sinon
*/
public boolean offersService(ServiceType serviceType) {
return serviceTypes != null && serviceTypes.contains(serviceType);
}
/**
* Méthode de recherche par ID utilisateur.
*
* @param userId l'ID de l'utilisateur
* @return le coach trouvé ou null
*/
public static Coach findByUserId(Long userId) {
return find("#Coach.findByUserId", userId).firstResult();
}
/**
* Méthode de recherche par statut.
*
* @param status le statut à rechercher
* @return la liste des coaches avec ce statut
*/
public static List<Coach> findByStatus(CoachStatus status) {
return find("#Coach.findByStatus", status).list();
}
/**
* Méthode de recherche par spécialisation.
*
* @param specialization la spécialisation à rechercher
* @return la liste des coaches avec cette spécialisation
*/
public static List<Coach> findBySpecialization(String specialization) {
return find("#Coach.findBySpecialization", specialization).list();
}
/**
* Méthode de recherche des coaches disponibles.
*
* @return la liste des coaches disponibles pour réservation
*/
public static List<Coach> findAvailableCoaches() {
return find("#Coach.findAvailableCoaches").list();
}
// Getters et Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getSpecialization() {
return specialization;
}
public void setSpecialization(String specialization) {
this.specialization = specialization;
}
public String getBio() {
return bio;
}
public void setBio(String bio) {
this.bio = bio;
}
public Integer getYearsOfExperience() {
return yearsOfExperience;
}
public void setYearsOfExperience(Integer yearsOfExperience) {
this.yearsOfExperience = yearsOfExperience;
}
public String getCertifications() {
return certifications;
}
public void setCertifications(String certifications) {
this.certifications = certifications;
}
public String getLanguages() {
return languages;
}
public void setLanguages(String languages) {
this.languages = languages;
}
public BigDecimal getHourlyRate() {
return hourlyRate;
}
public void setHourlyRate(BigDecimal hourlyRate) {
this.hourlyRate = hourlyRate;
}
public CoachStatus getStatus() {
return status;
}
public void setStatus(CoachStatus status) {
this.status = status;
}
public void setAvailableForBooking(boolean availableForBooking) {
this.availableForBooking = availableForBooking;
}
public Set<ServiceType> getServiceTypes() {
return serviceTypes;
}
public void setServiceTypes(Set<ServiceType> serviceTypes) {
this.serviceTypes = serviceTypes;
}
public LocalTime getWorkingHoursStart() {
return workingHoursStart;
}
public void setWorkingHoursStart(LocalTime workingHoursStart) {
this.workingHoursStart = workingHoursStart;
}
public LocalTime getWorkingHoursEnd() {
return workingHoursEnd;
}
public void setWorkingHoursEnd(LocalTime workingHoursEnd) {
this.workingHoursEnd = workingHoursEnd;
}
public String getWorkingDays() {
return workingDays;
}
public void setWorkingDays(String workingDays) {
this.workingDays = workingDays;
}
public String getTimezone() {
return timezone;
}
public void setTimezone(String timezone) {
this.timezone = timezone;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
public BigDecimal getAverageRating() {
return averageRating;
}
public void setAverageRating(BigDecimal averageRating) {
this.averageRating = averageRating;
}
public int getTotalRatings() {
return totalRatings;
}
public void setTotalRatings(int totalRatings) {
this.totalRatings = totalRatings;
}
public int getTotalSessions() {
return totalSessions;
}
public void setTotalSessions(int totalSessions) {
this.totalSessions = totalSessions;
}
public BigDecimal getTotalRevenue() {
return totalRevenue;
}
public void setTotalRevenue(BigDecimal totalRevenue) {
this.totalRevenue = totalRevenue;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
@Override
public String toString() {
return "Coach{" +
"id=" + id +
", specialization='" + specialization + '\'' +
", status=" + status +
", availableForBooking=" + availableForBooking +
", averageRating=" + averageRating +
", totalSessions=" + totalSessions +
", totalRevenue=" + totalRevenue +
'}';
}
/**
* Énumération des statuts de coach.
*/
public enum CoachStatus {
/**
* Coach actif et disponible.
*/
ACTIVE,
/**
* Coach inactif temporairement.
*/
INACTIVE,
/**
* Coach en congé.
*/
ON_LEAVE,
/**
* Coach dont le contrat est terminé.
*/
TERMINATED
}
}

View File

@@ -0,0 +1,432 @@
package com.gbcm.server.impl.entity;
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.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.
*
* @author GBCM Development Team
* @version 1.0
* @since 1.0
*/
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_users_email", columnList = "email", unique = true),
@Index(name = "idx_users_role", columnList = "role"),
@Index(name = "idx_users_active", columnList = "active"),
@Index(name = "idx_users_deleted", columnList = "deleted")
})
@UserDefinition
@NamedQueries({
@NamedQuery(name = "User.findByEmail",
query = "SELECT u FROM User u WHERE u.email = :email AND u.deleted = false"),
@NamedQuery(name = "User.findActiveUsers",
query = "SELECT u FROM User u WHERE u.active = true AND u.deleted = false"),
@NamedQuery(name = "User.findByRole",
query = "SELECT u FROM User u WHERE u.role = :role AND u.deleted = false"),
@NamedQuery(name = "User.searchByNameOrEmail",
query = "SELECT u FROM User u WHERE (LOWER(u.firstName) LIKE LOWER(:search) OR LOWER(u.lastName) LIKE LOWER(:search) OR LOWER(u.email) LIKE LOWER(:search)) AND u.deleted = false")
})
public class User extends BaseEntity {
/**
* Identifiant unique de l'utilisateur.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* Prénom de l'utilisateur.
*/
@Column(name = "first_name", nullable = false, length = 50)
@NotBlank(message = "Le prénom est obligatoire")
@Size(max = 50, message = "Le prénom ne peut pas dépasser 50 caractères")
private String firstName;
/**
* Nom de famille de l'utilisateur.
*/
@Column(name = "last_name", nullable = false, length = 50)
@NotBlank(message = "Le nom est obligatoire")
@Size(max = 50, message = "Le nom ne peut pas dépasser 50 caractères")
private String lastName;
/**
* Adresse email unique de l'utilisateur.
* Utilisée comme nom d'utilisateur pour l'authentification.
*/
@Column(name = "email", nullable = false, unique = true, length = 255)
@NotBlank(message = "L'email est obligatoire")
@Email(message = "Format d'email invalide")
@Size(max = 255, message = "L'email ne peut pas dépasser 255 caractères")
@Username
private String email;
/**
* Mot de passe haché de l'utilisateur.
* Utilisé pour l'authentification avec Quarkus Security.
*/
@Column(name = "password_hash", nullable = false, length = 255)
@NotBlank(message = "Le mot de passe est obligatoire")
@Size(max = 255, message = "Le hash du mot de passe ne peut pas dépasser 255 caractères")
@Password
private String passwordHash;
/**
* Numéro de téléphone de l'utilisateur.
*/
@Column(name = "phone", length = 20)
@Size(max = 20, message = "Le téléphone ne peut pas dépasser 20 caractères")
private String phone;
/**
* Rôle de l'utilisateur dans le système.
* Détermine les permissions et l'accès aux fonctionnalités.
*/
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false, length = 20)
@NotNull(message = "Le rôle est obligatoire")
@Roles
private UserRole role;
/**
* Statut d'activation du compte utilisateur.
* true = compte actif, false = compte désactivé.
*/
@Column(name = "active", nullable = false)
private boolean active = true;
/**
* Date de dernière connexion de l'utilisateur.
*/
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
/**
* Adresse IP de la dernière connexion.
*/
@Column(name = "last_login_ip", length = 45)
@Size(max = 45, message = "L'adresse IP ne peut pas dépasser 45 caractères")
private String lastLoginIp;
/**
* Nombre de tentatives de connexion échouées consécutives.
*/
@Column(name = "failed_login_attempts", nullable = false)
private int failedLoginAttempts = 0;
/**
* Date de verrouillage du compte (après trop de tentatives échouées).
*/
@Column(name = "locked_until")
private LocalDateTime lockedUntil;
/**
* Token de réinitialisation de mot de passe.
*/
@Column(name = "password_reset_token", length = 255)
@Size(max = 255, message = "Le token de réinitialisation ne peut pas dépasser 255 caractères")
private String passwordResetToken;
/**
* Date d'expiration du token de réinitialisation.
*/
@Column(name = "password_reset_expires_at")
private LocalDateTime passwordResetExpiresAt;
/**
* Constructeur par défaut.
*/
public User() {
super();
}
/**
* Constructeur avec les champs obligatoires.
*
* @param firstName le prénom
* @param lastName le nom de famille
* @param email l'adresse email
* @param passwordHash le mot de passe haché
* @param role le rôle de l'utilisateur
*/
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;
}
/**
* Retourne le nom complet de l'utilisateur.
*
* @return le nom complet (prénom + nom)
*/
public String getFullName() {
return firstName + " " + lastName;
}
/**
* Vérifie si le compte est verrouillé.
*
* @return true si le compte est verrouillé, false sinon
*/
public boolean isLocked() {
return lockedUntil != null && LocalDateTime.now().isBefore(lockedUntil);
}
/**
* Verrouille le compte pour une durée spécifiée.
*
* @param lockDurationMinutes durée de verrouillage en minutes
*/
public void lockAccount(int lockDurationMinutes) {
this.lockedUntil = LocalDateTime.now().plusMinutes(lockDurationMinutes);
}
/**
* Déverrouille le compte et remet à zéro les tentatives échouées.
*/
public void unlockAccount() {
this.lockedUntil = null;
this.failedLoginAttempts = 0;
}
/**
* Incrémente le nombre de tentatives de connexion échouées.
*/
public void incrementFailedLoginAttempts() {
this.failedLoginAttempts++;
}
/**
* Remet à zéro les tentatives de connexion échouées.
*/
public void resetFailedLoginAttempts() {
this.failedLoginAttempts = 0;
}
/**
* Met à jour les informations de dernière connexion.
*
* @param ipAddress l'adresse IP de connexion
*/
public void updateLastLogin(String ipAddress) {
this.lastLoginAt = LocalDateTime.now();
this.lastLoginIp = ipAddress;
resetFailedLoginAttempts();
}
/**
* Génère un token de réinitialisation de mot de passe.
*
* @param token le token généré
* @param expirationHours durée de validité en heures
*/
public void setPasswordResetToken(String token, int expirationHours) {
this.passwordResetToken = token;
this.passwordResetExpiresAt = LocalDateTime.now().plusHours(expirationHours);
}
/**
* Efface le token de réinitialisation de mot de passe.
*/
public void clearPasswordResetToken() {
this.passwordResetToken = null;
this.passwordResetExpiresAt = null;
}
/**
* Vérifie si le token de réinitialisation est valide.
*
* @param token le token à vérifier
* @return true si le token est valide, false sinon
*/
public boolean isPasswordResetTokenValid(String token) {
return passwordResetToken != null &&
passwordResetToken.equals(token) &&
passwordResetExpiresAt != null &&
LocalDateTime.now().isBefore(passwordResetExpiresAt);
}
/**
* Méthode de recherche par email.
*
* @param email l'adresse email à rechercher
* @return l'utilisateur trouvé ou null
*/
public static User findByEmail(String email) {
return find("#User.findByEmail", email).firstResult();
}
/**
* Méthode de recherche des utilisateurs actifs.
*
* @return la liste des utilisateurs actifs
*/
public static List<User> findActiveUsers() {
return find("#User.findActiveUsers").list();
}
/**
* Méthode de recherche par rôle.
*
* @param role le rôle à rechercher
* @return la liste des utilisateurs avec ce rôle
*/
public static List<User> findByRole(UserRole role) {
return find("#User.findByRole", role).list();
}
/**
* Méthode de recherche par nom ou email.
*
* @param search le terme de recherche
* @return la liste des utilisateurs correspondants
*/
public static List<User> searchByNameOrEmail(String search) {
String searchPattern = "%" + search + "%";
return find("#User.searchByNameOrEmail", searchPattern).list();
}
// Getters et Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public UserRole getRole() {
return role;
}
public void setRole(UserRole role) {
this.role = role;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public LocalDateTime getLastLoginAt() {
return lastLoginAt;
}
public void setLastLoginAt(LocalDateTime lastLoginAt) {
this.lastLoginAt = lastLoginAt;
}
public String getLastLoginIp() {
return lastLoginIp;
}
public void setLastLoginIp(String lastLoginIp) {
this.lastLoginIp = lastLoginIp;
}
public int getFailedLoginAttempts() {
return failedLoginAttempts;
}
public void setFailedLoginAttempts(int failedLoginAttempts) {
this.failedLoginAttempts = failedLoginAttempts;
}
public LocalDateTime getLockedUntil() {
return lockedUntil;
}
public void setLockedUntil(LocalDateTime lockedUntil) {
this.lockedUntil = lockedUntil;
}
public String getPasswordResetToken() {
return passwordResetToken;
}
public LocalDateTime getPasswordResetExpiresAt() {
return passwordResetExpiresAt;
}
public void setPasswordResetExpiresAt(LocalDateTime passwordResetExpiresAt) {
this.passwordResetExpiresAt = passwordResetExpiresAt;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", role=" + role +
", active=" + active +
", lastLoginAt=" + lastLoginAt +
", failedLoginAttempts=" + failedLoginAttempts +
", locked=" + isLocked() +
'}';
}
}

View File

@@ -1,249 +0,0 @@
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;
}
}