feat(mobile): Implement Keycloak WebView authentication with HTTP callback

- Replace flutter_appauth with custom WebView implementation to resolve deep link issues
- Add KeycloakWebViewAuthService with integrated WebView for seamless authentication
- Configure Android manifest for HTTP cleartext traffic support
- Add network security config for development environment (192.168.1.11)
- Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback)
- Remove obsolete keycloak_auth_service.dart and temporary scripts
- Clean up dependencies and regenerate injection configuration
- Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F)

BREAKING CHANGE: Authentication flow now uses WebView instead of external browser
- Users will see Keycloak login page within the app instead of browser redirect
- Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues
- Maintains full OIDC compliance with PKCE flow and secure token storage

Technical improvements:
- WebView with custom navigation delegate for callback handling
- Automatic token extraction and user info parsing from JWT
- Proper error handling and user feedback
- Consistent authentication state management across app lifecycle
This commit is contained in:
DahoudG
2025-09-15 01:44:16 +00:00
parent 73459b3092
commit f89f6167cc
290 changed files with 34563 additions and 3528 deletions

View File

@@ -0,0 +1,134 @@
package de.lions.unionflow.server.auth;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
/**
* Resource temporaire pour gérer les callbacks d'authentification OAuth2/OIDC
* depuis l'application mobile.
*/
@Path("/auth")
public class AuthCallbackResource {
/**
* Endpoint de callback pour l'authentification OAuth2/OIDC.
* Redirige vers l'application mobile avec les paramètres reçus.
*/
@GET
@Path("/callback")
public Response handleCallback(
@QueryParam("code") String code,
@QueryParam("state") String state,
@QueryParam("session_state") String sessionState,
@QueryParam("error") String error,
@QueryParam("error_description") String errorDescription) {
try {
// Log des paramètres reçus pour debug
System.out.println("=== CALLBACK DEBUG ===");
System.out.println("Code: " + code);
System.out.println("State: " + state);
System.out.println("Session State: " + sessionState);
System.out.println("Error: " + error);
System.out.println("Error Description: " + errorDescription);
// URL de redirection simple vers l'application mobile
String redirectUrl = "dev.lions.unionflow-mobile://callback";
// Si nous avons un code d'autorisation, c'est un succès
if (code != null && !code.isEmpty()) {
redirectUrl += "?code=" + code;
if (state != null && !state.isEmpty()) {
redirectUrl += "&state=" + state;
}
} else if (error != null) {
redirectUrl += "?error=" + error;
if (errorDescription != null) {
redirectUrl += "&error_description=" + errorDescription;
}
}
// Page HTML simple qui redirige automatiquement vers l'app mobile
String html = """
<!DOCTYPE html>
<html>
<head>
<title>Redirection vers UnionFlow</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
max-width: 400px;
margin: 0 auto;
background: rgba(255,255,255,0.1);
padding: 30px;
border-radius: 10px;
}
.spinner {
border: 4px solid rgba(255,255,255,0.3);
border-top: 4px solid white;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
a { color: #ffeb3b; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<h2>🔐 Authentification réussie</h2>
<div class="spinner"></div>
<p>Redirection vers l'application UnionFlow...</p>
<p><small>Si la redirection ne fonctionne pas automatiquement,
<a href="%s">cliquez ici</a></small></p>
</div>
<script>
// Tentative de redirection automatique
setTimeout(function() {
window.location.href = '%s';
}, 2000);
// Fallback: ouvrir l'app mobile si possible
setTimeout(function() {
try {
window.open('%s', '_self');
} catch(e) {
console.log('Redirection manuelle nécessaire');
}
}, 3000);
</script>
</body>
</html>
""".formatted(redirectUrl, redirectUrl, redirectUrl);
return Response.ok(html).type("text/html").build();
} catch (Exception e) {
// En cas d'erreur, retourner une page d'erreur simple
String errorHtml = """
<!DOCTYPE html>
<html>
<head><title>Erreur d'authentification</title></head>
<body style="font-family: Arial; text-align: center; padding: 50px;">
<h2>❌ Erreur d'authentification</h2>
<p>Une erreur s'est produite lors de la redirection.</p>
<p>Veuillez fermer cette page et réessayer.</p>
</body>
</html>
""";
return Response.status(500).entity(errorHtml).type("text/html").build();
}
}
}

View File

@@ -0,0 +1,302 @@
package dev.lions.unionflow.server.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Entité Événement pour la gestion des événements de l'union
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@Entity
@Table(name = "evenements", indexes = {
@Index(name = "idx_evenement_date_debut", columnList = "date_debut"),
@Index(name = "idx_evenement_statut", columnList = "statut"),
@Index(name = "idx_evenement_type", columnList = "type_evenement"),
@Index(name = "idx_evenement_organisation", columnList = "organisation_id")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
public class Evenement extends PanacheEntity {
@NotBlank
@Size(min = 3, max = 200)
@Column(name = "titre", nullable = false, length = 200)
private String titre;
@Size(max = 2000)
@Column(name = "description", length = 2000)
private String description;
@NotNull
@Column(name = "date_debut", nullable = false)
private LocalDateTime dateDebut;
@Column(name = "date_fin")
private LocalDateTime dateFin;
@Size(max = 500)
@Column(name = "lieu", length = 500)
private String lieu;
@Size(max = 1000)
@Column(name = "adresse", length = 1000)
private String adresse;
@Enumerated(EnumType.STRING)
@Column(name = "type_evenement", length = 50)
private TypeEvenement typeEvenement;
@Enumerated(EnumType.STRING)
@Builder.Default
@Column(name = "statut", nullable = false, length = 30)
private StatutEvenement statut = StatutEvenement.PLANIFIE;
@Min(0)
@Column(name = "capacite_max")
private Integer capaciteMax;
@DecimalMin("0.00")
@Digits(integer = 8, fraction = 2)
@Column(name = "prix", precision = 10, scale = 2)
private BigDecimal prix;
@Builder.Default
@Column(name = "inscription_requise", nullable = false)
private Boolean inscriptionRequise = false;
@Column(name = "date_limite_inscription")
private LocalDateTime dateLimiteInscription;
@Size(max = 1000)
@Column(name = "instructions_particulieres", length = 1000)
private String instructionsParticulieres;
@Size(max = 500)
@Column(name = "contact_organisateur", length = 500)
private String contactOrganisateur;
@Size(max = 2000)
@Column(name = "materiel_requis", length = 2000)
private String materielRequis;
@Builder.Default
@Column(name = "visible_public", nullable = false)
private Boolean visiblePublic = true;
@Builder.Default
@Column(name = "actif", nullable = false)
private Boolean actif = true;
// Relations
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation_id")
private Organisation organisation;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisateur_id")
private Membre organisateur;
@OneToMany(mappedBy = "evenement", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@Builder.Default
private List<InscriptionEvenement> inscriptions = new ArrayList<>();
// Métadonnées
@CreationTimestamp
@Column(name = "date_creation", nullable = false, updatable = false)
private LocalDateTime dateCreation;
@UpdateTimestamp
@Column(name = "date_modification")
private LocalDateTime dateModification;
@Column(name = "cree_par", length = 100)
private String creePar;
@Column(name = "modifie_par", length = 100)
private String modifiePar;
/**
* Types d'événements
*/
public enum TypeEvenement {
ASSEMBLEE_GENERALE("Assemblée Générale"),
REUNION("Réunion"),
FORMATION("Formation"),
CONFERENCE("Conférence"),
ATELIER("Atelier"),
SEMINAIRE("Séminaire"),
EVENEMENT_SOCIAL("Événement Social"),
MANIFESTATION("Manifestation"),
CELEBRATION("Célébration"),
AUTRE("Autre");
private final String libelle;
TypeEvenement(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
}
/**
* Statuts d'événement
*/
public enum StatutEvenement {
PLANIFIE("Planifié"),
CONFIRME("Confirmé"),
EN_COURS("En cours"),
TERMINE("Terminé"),
ANNULE("Annulé"),
REPORTE("Reporté");
private final String libelle;
StatutEvenement(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
}
// Méthodes métier
/**
* Vérifie si l'événement est ouvert aux inscriptions
*/
public boolean isOuvertAuxInscriptions() {
if (!inscriptionRequise || !actif) {
return false;
}
LocalDateTime maintenant = LocalDateTime.now();
// Vérifier si la date limite d'inscription n'est pas dépassée
if (dateLimiteInscription != null && maintenant.isAfter(dateLimiteInscription)) {
return false;
}
// Vérifier si l'événement n'a pas déjà commencé
if (maintenant.isAfter(dateDebut)) {
return false;
}
// Vérifier la capacité
if (capaciteMax != null && getNombreInscrits() >= capaciteMax) {
return false;
}
return statut == StatutEvenement.PLANIFIE || statut == StatutEvenement.CONFIRME;
}
/**
* Obtient le nombre d'inscrits à l'événement
*/
public int getNombreInscrits() {
return inscriptions != null ? (int) inscriptions.stream()
.filter(inscription -> inscription.getStatut() == InscriptionEvenement.StatutInscription.CONFIRMEE)
.count() : 0;
}
/**
* Vérifie si l'événement est complet
*/
public boolean isComplet() {
return capaciteMax != null && getNombreInscrits() >= capaciteMax;
}
/**
* Vérifie si l'événement est en cours
*/
public boolean isEnCours() {
LocalDateTime maintenant = LocalDateTime.now();
return maintenant.isAfter(dateDebut) &&
(dateFin == null || maintenant.isBefore(dateFin));
}
/**
* Vérifie si l'événement est terminé
*/
public boolean isTermine() {
if (statut == StatutEvenement.TERMINE) {
return true;
}
LocalDateTime maintenant = LocalDateTime.now();
return dateFin != null && maintenant.isAfter(dateFin);
}
/**
* Calcule la durée de l'événement en heures
*/
public Long getDureeEnHeures() {
if (dateFin == null) {
return null;
}
return java.time.Duration.between(dateDebut, dateFin).toHours();
}
/**
* Obtient le nombre de places restantes
*/
public Integer getPlacesRestantes() {
if (capaciteMax == null) {
return null; // Capacité illimitée
}
return Math.max(0, capaciteMax - getNombreInscrits());
}
/**
* Vérifie si un membre est inscrit à l'événement
*/
public boolean isMemberInscrit(Long membreId) {
return inscriptions != null && inscriptions.stream()
.anyMatch(inscription ->
inscription.getMembre().id.equals(membreId) &&
inscription.getStatut() == InscriptionEvenement.StatutInscription.CONFIRMEE);
}
/**
* Obtient le taux de remplissage en pourcentage
*/
public Double getTauxRemplissage() {
if (capaciteMax == null || capaciteMax == 0) {
return null;
}
return (double) getNombreInscrits() / capaciteMax * 100;
}
@PrePersist
public void prePersist() {
if (dateCreation == null) {
dateCreation = LocalDateTime.now();
}
}
@PreUpdate
public void preUpdate() {
dateModification = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,165 @@
package dev.lions.unionflow.server.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import java.time.LocalDateTime;
/**
* Entité InscriptionEvenement représentant l'inscription d'un membre à un événement
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@Entity
@Table(name = "inscriptions_evenement", indexes = {
@Index(name = "idx_inscription_membre", columnList = "membre_id"),
@Index(name = "idx_inscription_evenement", columnList = "evenement_id"),
@Index(name = "idx_inscription_date", columnList = "date_inscription")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
public class InscriptionEvenement extends PanacheEntity {
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre_id", nullable = false)
private Membre membre;
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "evenement_id", nullable = false)
private Evenement evenement;
@Builder.Default
@Column(name = "date_inscription", nullable = false)
private LocalDateTime dateInscription = LocalDateTime.now();
@Enumerated(EnumType.STRING)
@Column(name = "statut", length = 20)
@Builder.Default
private StatutInscription statut = StatutInscription.CONFIRMEE;
@Column(name = "commentaire", length = 500)
private String commentaire;
@Builder.Default
@Column(name = "date_creation", nullable = false)
private LocalDateTime dateCreation = LocalDateTime.now();
@Column(name = "date_modification")
private LocalDateTime dateModification;
/**
* Énumération des statuts d'inscription
*/
public enum StatutInscription {
CONFIRMEE("Confirmée"),
EN_ATTENTE("En attente"),
ANNULEE("Annulée"),
REFUSEE("Refusée");
private final String libelle;
StatutInscription(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
}
// Méthodes utilitaires
/**
* Vérifie si l'inscription est confirmée
*
* @return true si l'inscription est confirmée
*/
public boolean isConfirmee() {
return StatutInscription.CONFIRMEE.equals(this.statut);
}
/**
* Vérifie si l'inscription est en attente
*
* @return true si l'inscription est en attente
*/
public boolean isEnAttente() {
return StatutInscription.EN_ATTENTE.equals(this.statut);
}
/**
* Vérifie si l'inscription est annulée
*
* @return true si l'inscription est annulée
*/
public boolean isAnnulee() {
return StatutInscription.ANNULEE.equals(this.statut);
}
/**
* Confirme l'inscription
*/
public void confirmer() {
this.statut = StatutInscription.CONFIRMEE;
this.dateModification = LocalDateTime.now();
}
/**
* Annule l'inscription
*
* @param commentaire le commentaire d'annulation
*/
public void annuler(String commentaire) {
this.statut = StatutInscription.ANNULEE;
this.commentaire = commentaire;
this.dateModification = LocalDateTime.now();
}
/**
* Met l'inscription en attente
*
* @param commentaire le commentaire de mise en attente
*/
public void mettreEnAttente(String commentaire) {
this.statut = StatutInscription.EN_ATTENTE;
this.commentaire = commentaire;
this.dateModification = LocalDateTime.now();
}
/**
* Refuse l'inscription
*
* @param commentaire le commentaire de refus
*/
public void refuser(String commentaire) {
this.statut = StatutInscription.REFUSEE;
this.commentaire = commentaire;
this.dateModification = LocalDateTime.now();
}
// Callbacks JPA
@PreUpdate
public void preUpdate() {
this.dateModification = LocalDateTime.now();
}
@Override
public String toString() {
return String.format("InscriptionEvenement{id=%d, membre=%s, evenement=%s, statut=%s, dateInscription=%s}",
id,
membre != null ? membre.getEmail() : "null",
evenement != null ? evenement.getTitre() : "null",
statut,
dateInscription);
}
}

View File

@@ -47,6 +47,9 @@ public class Membre extends PanacheEntity {
@Column(name = "email", unique = true, nullable = false, length = 255)
private String email;
@Column(name = "mot_de_passe", length = 255)
private String motDePasse;
@Column(name = "telephone", length = 20)
private String telephone;
@@ -58,6 +61,9 @@ public class Membre extends PanacheEntity {
@Column(name = "date_adhesion", nullable = false)
private LocalDate dateAdhesion;
@Column(name = "roles", length = 500)
private String roles;
@Builder.Default
@Column(name = "actif", nullable = false)
private Boolean actif = true;
@@ -69,6 +75,11 @@ public class Membre extends PanacheEntity {
@Column(name = "date_modification")
private LocalDateTime dateModification;
// Relations
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation_id")
private Organisation organisation;
/**
* Méthode métier pour obtenir le nom complet
*/

View File

@@ -0,0 +1,353 @@
package dev.lions.unionflow.server.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Entité Organisation avec Lombok
* Représente une organisation (Lions Club, Association, Coopérative, etc.)
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@Entity
@Table(name = "organisations", indexes = {
@Index(name = "idx_organisation_nom", columnList = "nom"),
@Index(name = "idx_organisation_email", columnList = "email", unique = true),
@Index(name = "idx_organisation_statut", columnList = "statut"),
@Index(name = "idx_organisation_type", columnList = "type_organisation"),
@Index(name = "idx_organisation_ville", columnList = "ville"),
@Index(name = "idx_organisation_pays", columnList = "pays"),
@Index(name = "idx_organisation_parente", columnList = "organisation_parente_id"),
@Index(name = "idx_organisation_numero_enregistrement", columnList = "numero_enregistrement", unique = true)
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = false)
public class Organisation extends PanacheEntity {
@NotBlank
@Column(name = "nom", nullable = false, length = 200)
private String nom;
@Column(name = "nom_court", length = 50)
private String nomCourt;
@NotBlank
@Column(name = "type_organisation", nullable = false, length = 50)
private String typeOrganisation;
@NotBlank
@Column(name = "statut", nullable = false, length = 50)
private String statut;
@Column(name = "description", length = 2000)
private String description;
@Column(name = "date_fondation")
private LocalDate dateFondation;
@Column(name = "numero_enregistrement", unique = true, length = 100)
private String numeroEnregistrement;
// Informations de contact
@Email
@NotBlank
@Column(name = "email", unique = true, nullable = false, length = 255)
private String email;
@Column(name = "telephone", length = 20)
private String telephone;
@Column(name = "telephone_secondaire", length = 20)
private String telephoneSecondaire;
@Email
@Column(name = "email_secondaire", length = 255)
private String emailSecondaire;
// Adresse
@Column(name = "adresse", length = 500)
private String adresse;
@Column(name = "ville", length = 100)
private String ville;
@Column(name = "code_postal", length = 20)
private String codePostal;
@Column(name = "region", length = 100)
private String region;
@Column(name = "pays", length = 100)
private String pays;
// Coordonnées géographiques
@DecimalMin(value = "-90.0", message = "La latitude doit être comprise entre -90 et 90")
@DecimalMax(value = "90.0", message = "La latitude doit être comprise entre -90 et 90")
@Digits(integer = 3, fraction = 6)
@Column(name = "latitude", precision = 9, scale = 6)
private BigDecimal latitude;
@DecimalMin(value = "-180.0", message = "La longitude doit être comprise entre -180 et 180")
@DecimalMax(value = "180.0", message = "La longitude doit être comprise entre -180 et 180")
@Digits(integer = 3, fraction = 6)
@Column(name = "longitude", precision = 9, scale = 6)
private BigDecimal longitude;
// Web et réseaux sociaux
@Column(name = "site_web", length = 500)
private String siteWeb;
@Column(name = "logo", length = 500)
private String logo;
@Column(name = "reseaux_sociaux", length = 1000)
private String reseauxSociaux;
// Hiérarchie
@Column(name = "organisation_parente_id")
private UUID organisationParenteId;
@Builder.Default
@Column(name = "niveau_hierarchique", nullable = false)
private Integer niveauHierarchique = 0;
// Statistiques
@Builder.Default
@Column(name = "nombre_membres", nullable = false)
private Integer nombreMembres = 0;
@Builder.Default
@Column(name = "nombre_administrateurs", nullable = false)
private Integer nombreAdministrateurs = 0;
// Finances
@DecimalMin(value = "0.0", message = "Le budget annuel doit être positif")
@Digits(integer = 12, fraction = 2)
@Column(name = "budget_annuel", precision = 14, scale = 2)
private BigDecimal budgetAnnuel;
@Builder.Default
@Column(name = "devise", length = 3)
private String devise = "XOF";
@Builder.Default
@Column(name = "cotisation_obligatoire", nullable = false)
private Boolean cotisationObligatoire = false;
@DecimalMin(value = "0.0", message = "Le montant de cotisation doit être positif")
@Digits(integer = 10, fraction = 2)
@Column(name = "montant_cotisation_annuelle", precision = 12, scale = 2)
private BigDecimal montantCotisationAnnuelle;
// Informations complémentaires
@Column(name = "objectifs", length = 2000)
private String objectifs;
@Column(name = "activites_principales", length = 2000)
private String activitesPrincipales;
@Column(name = "certifications", length = 500)
private String certifications;
@Column(name = "partenaires", length = 1000)
private String partenaires;
@Column(name = "notes", length = 1000)
private String notes;
// Paramètres
@Builder.Default
@Column(name = "organisation_publique", nullable = false)
private Boolean organisationPublique = true;
@Builder.Default
@Column(name = "accepte_nouveaux_membres", nullable = false)
private Boolean accepteNouveauxMembres = true;
// Métadonnées
@Builder.Default
@Column(name = "actif", nullable = false)
private Boolean actif = true;
@Builder.Default
@Column(name = "date_creation", nullable = false)
private LocalDateTime dateCreation = LocalDateTime.now();
@Column(name = "date_modification")
private LocalDateTime dateModification;
@Column(name = "cree_par", length = 100)
private String creePar;
@Column(name = "modifie_par", length = 100)
private String modifiePar;
@Builder.Default
@Column(name = "version", nullable = false)
private Long version = 0L;
// Relations
@OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<Membre> membres = new ArrayList<>();
/**
* Méthode métier pour obtenir le nom complet avec sigle
*/
public String getNomComplet() {
if (nomCourt != null && !nomCourt.isEmpty()) {
return nom + " (" + nomCourt + ")";
}
return nom;
}
/**
* Méthode métier pour calculer l'ancienneté en années
*/
public int getAncienneteAnnees() {
if (dateFondation == null) {
return 0;
}
return Period.between(dateFondation, LocalDate.now()).getYears();
}
/**
* Méthode métier pour vérifier si l'organisation est récente (moins de 2 ans)
*/
public boolean isRecente() {
return getAncienneteAnnees() < 2;
}
/**
* Méthode métier pour vérifier si l'organisation est active
*/
public boolean isActive() {
return "ACTIVE".equals(statut) && actif;
}
/**
* Méthode métier pour ajouter un membre
*/
public void ajouterMembre() {
if (nombreMembres == null) {
nombreMembres = 0;
}
nombreMembres++;
}
/**
* Méthode métier pour retirer un membre
*/
public void retirerMembre() {
if (nombreMembres != null && nombreMembres > 0) {
nombreMembres--;
}
}
/**
* Méthode métier pour activer l'organisation
*/
public void activer(String utilisateur) {
this.statut = "ACTIVE";
this.actif = true;
marquerCommeModifie(utilisateur);
}
/**
* Méthode métier pour suspendre l'organisation
*/
public void suspendre(String utilisateur) {
this.statut = "SUSPENDUE";
this.accepteNouveauxMembres = false;
marquerCommeModifie(utilisateur);
}
/**
* Méthode métier pour dissoudre l'organisation
*/
public void dissoudre(String utilisateur) {
this.statut = "DISSOUTE";
this.actif = false;
this.accepteNouveauxMembres = false;
marquerCommeModifie(utilisateur);
}
/**
* Marque l'entité comme modifiée
*/
public void marquerCommeModifie(String utilisateur) {
this.dateModification = LocalDateTime.now();
this.modifiePar = utilisateur;
this.version++;
}
/**
* Callback JPA avant la persistance
*/
@PrePersist
protected void onCreate() {
if (dateCreation == null) {
dateCreation = LocalDateTime.now();
}
if (statut == null) {
statut = "ACTIVE";
}
if (typeOrganisation == null) {
typeOrganisation = "ASSOCIATION";
}
if (devise == null) {
devise = "XOF";
}
if (niveauHierarchique == null) {
niveauHierarchique = 0;
}
if (nombreMembres == null) {
nombreMembres = 0;
}
if (nombreAdministrateurs == null) {
nombreAdministrateurs = 0;
}
if (organisationPublique == null) {
organisationPublique = true;
}
if (accepteNouveauxMembres == null) {
accepteNouveauxMembres = true;
}
if (cotisationObligatoire == null) {
cotisationObligatoire = false;
}
if (actif == null) {
actif = true;
}
if (version == null) {
version = 0L;
}
}
/**
* Callback JPA avant la mise à jour
*/
@PreUpdate
protected void onUpdate() {
dateModification = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,489 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Repository pour l'entité Événement
*
* Fournit les méthodes d'accès aux données pour la gestion des événements
* avec des fonctionnalités de recherche avancées et de filtrage.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@ApplicationScoped
public class EvenementRepository implements PanacheRepository<Evenement> {
/**
* Trouve un événement par son titre (recherche exacte)
*
* @param titre le titre de l'événement
* @return l'événement trouvé ou Optional.empty()
*/
public Optional<Evenement> findByTitre(String titre) {
return find("titre", titre).firstResultOptional();
}
/**
* Trouve tous les événements actifs
*
* @return la liste des événements actifs
*/
public List<Evenement> findAllActifs() {
return find("actif", true).list();
}
/**
* Trouve tous les événements actifs avec pagination et tri
*
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements actifs
*/
public List<Evenement> findAllActifs(Page page, Sort sort) {
return find("actif", sort, true).page(page).list();
}
/**
* Compte le nombre d'événements actifs
*
* @return le nombre d'événements actifs
*/
public long countActifs() {
return count("actif", true);
}
/**
* Trouve les événements par statut
*
* @param statut le statut recherché
* @return la liste des événements avec ce statut
*/
public List<Evenement> findByStatut(StatutEvenement statut) {
return find("statut", statut).list();
}
/**
* Trouve les événements par statut avec pagination et tri
*
* @param statut le statut recherché
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements avec ce statut
*/
public List<Evenement> findByStatut(StatutEvenement statut, Page page, Sort sort) {
return find("statut", sort, statut).page(page).list();
}
/**
* Trouve les événements par type
*
* @param type le type d'événement recherché
* @return la liste des événements de ce type
*/
public List<Evenement> findByType(TypeEvenement type) {
return find("typeEvenement", type).list();
}
/**
* Trouve les événements par type avec pagination et tri
*
* @param type le type d'événement recherché
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements de ce type
*/
public List<Evenement> findByType(TypeEvenement type, Page page, Sort sort) {
return find("typeEvenement", sort, type).page(page).list();
}
/**
* Trouve les événements par organisation
*
* @param organisationId l'ID de l'organisation
* @return la liste des événements de cette organisation
*/
public List<Evenement> findByOrganisation(Long organisationId) {
return find("organisation.id", organisationId).list();
}
/**
* Trouve les événements par organisation avec pagination et tri
*
* @param organisationId l'ID de l'organisation
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements de cette organisation
*/
public List<Evenement> findByOrganisation(Long organisationId, Page page, Sort sort) {
return find("organisation.id", sort, organisationId).page(page).list();
}
/**
* Trouve les événements par organisateur
*
* @param organisateurId l'ID de l'organisateur
* @return la liste des événements organisés par ce membre
*/
public List<Evenement> findByOrganisateur(Long organisateurId) {
return find("organisateur.id", organisateurId).list();
}
/**
* Trouve les événements dans une période donnée
*
* @param dateDebut la date de début de la période
* @param dateFin la date de fin de la période
* @return la liste des événements dans cette période
*/
public List<Evenement> findByPeriode(LocalDateTime dateDebut, LocalDateTime dateFin) {
return find("dateDebut >= ?1 and dateDebut <= ?2", dateDebut, dateFin).list();
}
/**
* Trouve les événements dans une période donnée avec pagination et tri
*
* @param dateDebut la date de début de la période
* @param dateFin la date de fin de la période
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements dans cette période
*/
public List<Evenement> findByPeriode(LocalDateTime dateDebut, LocalDateTime dateFin,
Page page, Sort sort) {
return find("dateDebut >= ?1 and dateDebut <= ?2", sort, dateDebut, dateFin)
.page(page).list();
}
/**
* Trouve les événements à venir (date de début future)
*
* @return la liste des événements à venir
*/
public List<Evenement> findEvenementsAVenir() {
return find("dateDebut > ?1 and actif = true", LocalDateTime.now()).list();
}
/**
* Trouve les événements à venir avec pagination et tri
*
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements à venir
*/
public List<Evenement> findEvenementsAVenir(Page page, Sort sort) {
return find("dateDebut > ?1 and actif = true", sort, LocalDateTime.now())
.page(page).list();
}
/**
* Trouve les événements en cours
*
* @return la liste des événements en cours
*/
public List<Evenement> findEvenementsEnCours() {
LocalDateTime maintenant = LocalDateTime.now();
return find("dateDebut <= ?1 and (dateFin is null or dateFin >= ?1) and actif = true",
maintenant).list();
}
/**
* Trouve les événements passés
*
* @return la liste des événements passés
*/
public List<Evenement> findEvenementsPasses() {
return find("(dateFin < ?1 or (dateFin is null and dateDebut < ?1)) and actif = true",
LocalDateTime.now()).list();
}
/**
* Trouve les événements passés avec pagination et tri
*
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements passés
*/
public List<Evenement> findEvenementsPasses(Page page, Sort sort) {
return find("(dateFin < ?1 or (dateFin is null and dateDebut < ?1)) and actif = true",
sort, LocalDateTime.now()).page(page).list();
}
/**
* Trouve les événements visibles au public
*
* @return la liste des événements publics
*/
public List<Evenement> findEvenementsPublics() {
return find("visiblePublic = true and actif = true").list();
}
/**
* Trouve les événements visibles au public avec pagination et tri
*
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements publics
*/
public List<Evenement> findEvenementsPublics(Page page, Sort sort) {
return find("visiblePublic = true and actif = true", sort).page(page).list();
}
/**
* Trouve les événements ouverts aux inscriptions
*
* @return la liste des événements ouverts aux inscriptions
*/
public List<Evenement> findEvenementsOuvertsInscription() {
LocalDateTime maintenant = LocalDateTime.now();
return find("inscriptionRequise = true and actif = true and dateDebut > ?1 and " +
"(dateLimiteInscription is null or dateLimiteInscription > ?1) and " +
"(statut = 'PLANIFIE' or statut = 'CONFIRME')", maintenant).list();
}
/**
* Recherche d'événements par titre ou description (recherche partielle)
*
* @param recherche le terme de recherche
* @return la liste des événements correspondants
*/
public List<Evenement> findByTitreOrDescription(String recherche) {
return find("lower(titre) like ?1 or lower(description) like ?1",
"%" + recherche.toLowerCase() + "%").list();
}
/**
* Recherche d'événements par titre ou description avec pagination et tri
*
* @param recherche le terme de recherche
* @param page la page demandée
* @param sort le tri à appliquer
* @return la liste paginée des événements correspondants
*/
public List<Evenement> findByTitreOrDescription(String recherche, Page page, Sort sort) {
return find("lower(titre) like ?1 or lower(description) like ?1",
sort, "%" + recherche.toLowerCase() + "%").page(page).list();
}
/**
* Compte les événements créés depuis une date donnée
*
* @param depuis la date de référence
* @return le nombre d'événements créés depuis cette date
*/
public long countNouveauxEvenements(LocalDateTime depuis) {
return count("dateCreation >= ?1", depuis);
}
/**
* Trouve les événements nécessitant une inscription avec places disponibles
*
* @return la liste des événements avec places disponibles
*/
public List<Evenement> findEvenementsAvecPlacesDisponibles() {
LocalDateTime maintenant = LocalDateTime.now();
return find("inscriptionRequise = true and actif = true and dateDebut > ?1 and " +
"(dateLimiteInscription is null or dateLimiteInscription > ?1) and " +
"(capaciteMax is null or " +
"(select count(i) from InscriptionEvenement i where i.evenement = this and i.statut = 'CONFIRMEE') < capaciteMax)",
maintenant).list();
}
/**
* Recherche avancée d'événements avec filtres multiples
*
* @param recherche terme de recherche (titre, description)
* @param statut statut de l'événement (optionnel)
* @param type type d'événement (optionnel)
* @param organisationId ID de l'organisation (optionnel)
* @param organisateurId ID de l'organisateur (optionnel)
* @param dateDebutMin date de début minimum (optionnel)
* @param dateDebutMax date de début maximum (optionnel)
* @param visiblePublic visibilité publique (optionnel)
* @param inscriptionRequise inscription requise (optionnel)
* @param actif statut actif (optionnel)
* @param page pagination
* @param sort tri
* @return la liste paginée des événements correspondants aux critères
*/
public List<Evenement> rechercheAvancee(String recherche,
StatutEvenement statut,
TypeEvenement type,
Long organisationId,
Long organisateurId,
LocalDateTime dateDebutMin,
LocalDateTime dateDebutMax,
Boolean visiblePublic,
Boolean inscriptionRequise,
Boolean actif,
Page page,
Sort sort) {
StringBuilder query = new StringBuilder("1=1");
Map<String, Object> params = new java.util.HashMap<>();
if (recherche != null && !recherche.trim().isEmpty()) {
query.append(" and (lower(titre) like :recherche or lower(description) like :recherche or lower(lieu) like :recherche)");
params.put("recherche", "%" + recherche.toLowerCase() + "%");
}
if (statut != null) {
query.append(" and statut = :statut");
params.put("statut", statut);
}
if (type != null) {
query.append(" and typeEvenement = :type");
params.put("type", type);
}
if (organisationId != null) {
query.append(" and organisation.id = :organisationId");
params.put("organisationId", organisationId);
}
if (organisateurId != null) {
query.append(" and organisateur.id = :organisateurId");
params.put("organisateurId", organisateurId);
}
if (dateDebutMin != null) {
query.append(" and dateDebut >= :dateDebutMin");
params.put("dateDebutMin", dateDebutMin);
}
if (dateDebutMax != null) {
query.append(" and dateDebut <= :dateDebutMax");
params.put("dateDebutMax", dateDebutMax);
}
if (visiblePublic != null) {
query.append(" and visiblePublic = :visiblePublic");
params.put("visiblePublic", visiblePublic);
}
if (inscriptionRequise != null) {
query.append(" and inscriptionRequise = :inscriptionRequise");
params.put("inscriptionRequise", inscriptionRequise);
}
if (actif != null) {
query.append(" and actif = :actif");
params.put("actif", actif);
}
return find(query.toString(), sort, params).page(page).list();
}
/**
* Compte les résultats de la recherche avancée
*
* @param recherche terme de recherche (titre, description)
* @param statut statut de l'événement (optionnel)
* @param type type d'événement (optionnel)
* @param organisationId ID de l'organisation (optionnel)
* @param organisateurId ID de l'organisateur (optionnel)
* @param dateDebutMin date de début minimum (optionnel)
* @param dateDebutMax date de début maximum (optionnel)
* @param visiblePublic visibilité publique (optionnel)
* @param inscriptionRequise inscription requise (optionnel)
* @param actif statut actif (optionnel)
* @return le nombre d'événements correspondants aux critères
*/
public long countRechercheAvancee(String recherche,
StatutEvenement statut,
TypeEvenement type,
Long organisationId,
Long organisateurId,
LocalDateTime dateDebutMin,
LocalDateTime dateDebutMax,
Boolean visiblePublic,
Boolean inscriptionRequise,
Boolean actif) {
StringBuilder query = new StringBuilder("1=1");
Map<String, Object> params = new java.util.HashMap<>();
if (recherche != null && !recherche.trim().isEmpty()) {
query.append(" and (lower(titre) like :recherche or lower(description) like :recherche or lower(lieu) like :recherche)");
params.put("recherche", "%" + recherche.toLowerCase() + "%");
}
if (statut != null) {
query.append(" and statut = :statut");
params.put("statut", statut);
}
if (type != null) {
query.append(" and typeEvenement = :type");
params.put("type", type);
}
if (organisationId != null) {
query.append(" and organisation.id = :organisationId");
params.put("organisationId", organisationId);
}
if (organisateurId != null) {
query.append(" and organisateur.id = :organisateurId");
params.put("organisateurId", organisateurId);
}
if (dateDebutMin != null) {
query.append(" and dateDebut >= :dateDebutMin");
params.put("dateDebutMin", dateDebutMin);
}
if (dateDebutMax != null) {
query.append(" and dateDebut <= :dateDebutMax");
params.put("dateDebutMax", dateDebutMax);
}
if (visiblePublic != null) {
query.append(" and visiblePublic = :visiblePublic");
params.put("visiblePublic", visiblePublic);
}
if (inscriptionRequise != null) {
query.append(" and inscriptionRequise = :inscriptionRequise");
params.put("inscriptionRequise", inscriptionRequise);
}
if (actif != null) {
query.append(" and actif = :actif");
params.put("actif", actif);
}
return count(query.toString(), params);
}
/**
* Obtient les statistiques des événements
*
* @return une map contenant les statistiques
*/
public Map<String, Long> getStatistiques() {
Map<String, Long> stats = new java.util.HashMap<>();
stats.put("total", count());
stats.put("actifs", count("actif", true));
stats.put("inactifs", count("actif", false));
stats.put("aVenir", count("dateDebut > ?1 and actif = true", LocalDateTime.now()));
stats.put("enCours", count("dateDebut <= ?1 and (dateFin is null or dateFin >= ?1) and actif = true", LocalDateTime.now()));
stats.put("passes", count("(dateFin < ?1 or (dateFin is null and dateDebut < ?1)) and actif = true", LocalDateTime.now()));
stats.put("publics", count("visiblePublic = true and actif = true"));
stats.put("avecInscription", count("inscriptionRequise = true and actif = true"));
return stats;
}
}

View File

@@ -0,0 +1,286 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Organisation;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour l'entité Organisation
* Utilise Panache pour simplifier les opérations JPA
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@ApplicationScoped
public class OrganisationRepository implements PanacheRepository<Organisation> {
/**
* Trouve une organisation par son email
*
* @param email l'email de l'organisation
* @return Optional contenant l'organisation si trouvée
*/
public Optional<Organisation> findByEmail(String email) {
return find("email = ?1", email).firstResultOptional();
}
/**
* Trouve une organisation par son nom
*
* @param nom le nom de l'organisation
* @return Optional contenant l'organisation si trouvée
*/
public Optional<Organisation> findByNom(String nom) {
return find("nom = ?1", nom).firstResultOptional();
}
/**
* Trouve une organisation par son numéro d'enregistrement
*
* @param numeroEnregistrement le numéro d'enregistrement officiel
* @return Optional contenant l'organisation si trouvée
*/
public Optional<Organisation> findByNumeroEnregistrement(String numeroEnregistrement) {
return find("numeroEnregistrement = ?1", numeroEnregistrement).firstResultOptional();
}
/**
* Trouve toutes les organisations actives
*
* @return liste des organisations actives
*/
public List<Organisation> findAllActives() {
return find("statut = 'ACTIVE' and actif = true").list();
}
/**
* Trouve toutes les organisations actives avec pagination
*
* @param page pagination
* @param sort tri
* @return liste paginée des organisations actives
*/
public List<Organisation> findAllActives(Page page, Sort sort) {
return find("statut = 'ACTIVE' and actif = true", sort).page(page).list();
}
/**
* Compte le nombre d'organisations actives
*
* @return nombre d'organisations actives
*/
public long countActives() {
return count("statut = 'ACTIVE' and actif = true");
}
/**
* Trouve les organisations par statut
*
* @param statut le statut recherché
* @param page pagination
* @param sort tri
* @return liste paginée des organisations avec le statut spécifié
*/
public List<Organisation> findByStatut(String statut, Page page, Sort sort) {
return find("statut = ?1", sort, statut).page(page).list();
}
/**
* Trouve les organisations par type
*
* @param typeOrganisation le type d'organisation
* @param page pagination
* @param sort tri
* @return liste paginée des organisations du type spécifié
*/
public List<Organisation> findByType(String typeOrganisation, Page page, Sort sort) {
return find("typeOrganisation = ?1", sort, typeOrganisation).page(page).list();
}
/**
* Trouve les organisations par ville
*
* @param ville la ville
* @param page pagination
* @param sort tri
* @return liste paginée des organisations de la ville spécifiée
*/
public List<Organisation> findByVille(String ville, Page page, Sort sort) {
return find("ville = ?1", sort, ville).page(page).list();
}
/**
* Trouve les organisations par pays
*
* @param pays le pays
* @param page pagination
* @param sort tri
* @return liste paginée des organisations du pays spécifié
*/
public List<Organisation> findByPays(String pays, Page page, Sort sort) {
return find("pays = ?1", sort, pays).page(page).list();
}
/**
* Trouve les organisations par région
*
* @param region la région
* @param page pagination
* @param sort tri
* @return liste paginée des organisations de la région spécifiée
*/
public List<Organisation> findByRegion(String region, Page page, Sort sort) {
return find("region = ?1", sort, region).page(page).list();
}
/**
* Trouve les organisations filles d'une organisation parente
*
* @param organisationParenteId l'ID de l'organisation parente
* @param page pagination
* @param sort tri
* @return liste paginée des organisations filles
*/
public List<Organisation> findByOrganisationParente(UUID organisationParenteId, Page page, Sort sort) {
return find("organisationParenteId = ?1", sort, organisationParenteId).page(page).list();
}
/**
* Trouve les organisations racines (sans parent)
*
* @param page pagination
* @param sort tri
* @return liste paginée des organisations racines
*/
public List<Organisation> findOrganisationsRacines(Page page, Sort sort) {
return find("organisationParenteId is null", sort).page(page).list();
}
/**
* Recherche d'organisations par nom ou nom court
*
* @param recherche terme de recherche
* @param page pagination
* @param sort tri
* @return liste paginée des organisations correspondantes
*/
public List<Organisation> findByNomOrNomCourt(String recherche, Page page, Sort sort) {
String pattern = "%" + recherche.toLowerCase() + "%";
return find("lower(nom) like ?1 or lower(nomCourt) like ?1", sort, pattern).page(page).list();
}
/**
* Recherche avancée d'organisations
*
* @param nom nom (optionnel)
* @param typeOrganisation type (optionnel)
* @param statut statut (optionnel)
* @param ville ville (optionnel)
* @param region région (optionnel)
* @param pays pays (optionnel)
* @param page pagination
* @return liste filtrée des organisations
*/
public List<Organisation> rechercheAvancee(String nom, String typeOrganisation, String statut,
String ville, String region, String pays, Page page) {
StringBuilder query = new StringBuilder("1=1");
Map<String, Object> parameters = new HashMap<>();
if (nom != null && !nom.isEmpty()) {
query.append(" and (lower(nom) like :nom or lower(nomCourt) like :nom)");
parameters.put("nom", "%" + nom.toLowerCase() + "%");
}
if (typeOrganisation != null && !typeOrganisation.isEmpty()) {
query.append(" and typeOrganisation = :typeOrganisation");
parameters.put("typeOrganisation", typeOrganisation);
}
if (statut != null && !statut.isEmpty()) {
query.append(" and statut = :statut");
parameters.put("statut", statut);
}
if (ville != null && !ville.isEmpty()) {
query.append(" and lower(ville) like :ville");
parameters.put("ville", "%" + ville.toLowerCase() + "%");
}
if (region != null && !region.isEmpty()) {
query.append(" and lower(region) like :region");
parameters.put("region", "%" + region.toLowerCase() + "%");
}
if (pays != null && !pays.isEmpty()) {
query.append(" and lower(pays) like :pays");
parameters.put("pays", "%" + pays.toLowerCase() + "%");
}
return find(query.toString(), Sort.by("nom").ascending(), parameters)
.page(page).list();
}
/**
* Compte les nouvelles organisations depuis une date donnée
*
* @param depuis date de référence
* @return nombre de nouvelles organisations
*/
public long countNouvellesOrganisations(LocalDate depuis) {
return count("dateCreation >= ?1", depuis.atStartOfDay());
}
/**
* Trouve les organisations publiques (visibles dans l'annuaire)
*
* @param page pagination
* @param sort tri
* @return liste paginée des organisations publiques
*/
public List<Organisation> findOrganisationsPubliques(Page page, Sort sort) {
return find("organisationPublique = true and statut = 'ACTIVE' and actif = true", sort)
.page(page).list();
}
/**
* Trouve les organisations acceptant de nouveaux membres
*
* @param page pagination
* @param sort tri
* @return liste paginée des organisations acceptant de nouveaux membres
*/
public List<Organisation> findOrganisationsOuvertes(Page page, Sort sort) {
return find("accepteNouveauxMembres = true and statut = 'ACTIVE' and actif = true", sort)
.page(page).list();
}
/**
* Compte les organisations par statut
*
* @param statut le statut
* @return nombre d'organisations avec ce statut
*/
public long countByStatut(String statut) {
return count("statut = ?1", statut);
}
/**
* Compte les organisations par type
*
* @param typeOrganisation le type d'organisation
* @return nombre d'organisations de ce type
*/
public long countByType(String typeOrganisation) {
return count("typeOrganisation = ?1", typeOrganisation);
}
}

View File

@@ -0,0 +1,399 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
import dev.lions.unionflow.server.service.EvenementService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Resource REST pour la gestion des événements
*
* Fournit les endpoints API pour les opérations CRUD sur les événements,
* optimisé pour l'intégration avec l'application mobile UnionFlow.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@Path("/api/evenements")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Événements", description = "Gestion des événements de l'union")
public class EvenementResource {
private static final Logger LOG = Logger.getLogger(EvenementResource.class);
@Inject
EvenementService evenementService;
/**
* Liste tous les événements actifs avec pagination
*/
@GET
@Operation(summary = "Lister tous les événements actifs",
description = "Récupère la liste paginée des événements actifs")
@APIResponse(responseCode = "200", description = "Liste des événements actifs")
@APIResponse(responseCode = "401", description = "Non authentifié")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"})
public Response listerEvenements(
@Parameter(description = "Numéro de page (0-based)", example = "0")
@QueryParam("page") @DefaultValue("0") @Min(0) int page,
@Parameter(description = "Taille de la page", example = "20")
@QueryParam("size") @DefaultValue("20") @Min(1) int size,
@Parameter(description = "Champ de tri", example = "dateDebut")
@QueryParam("sort") @DefaultValue("dateDebut") String sortField,
@Parameter(description = "Direction du tri (asc/desc)", example = "asc")
@QueryParam("direction") @DefaultValue("asc") String sortDirection) {
try {
LOG.infof("GET /api/evenements - page: %d, size: %d", page, size);
Sort sort = sortDirection.equalsIgnoreCase("desc")
? Sort.by(sortField).descending()
: Sort.by(sortField).ascending();
List<Evenement> evenements = evenementService.listerEvenementsActifs(
Page.of(page, size), sort);
return Response.ok(evenements).build();
} catch (Exception e) {
LOG.errorf("Erreur lors de la récupération des événements: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération des événements"))
.build();
}
}
/**
* Récupère un événement par son ID
*/
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un événement par ID")
@APIResponse(responseCode = "200", description = "Événement trouvé")
@APIResponse(responseCode = "404", description = "Événement non trouvé")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"})
public Response obtenirEvenement(
@Parameter(description = "ID de l'événement", required = true)
@PathParam("id") Long id) {
try {
LOG.infof("GET /api/evenements/%d", id);
Optional<Evenement> evenement = evenementService.trouverParId(id);
if (evenement.isPresent()) {
return Response.ok(evenement.get()).build();
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Événement non trouvé"))
.build();
}
} catch (Exception e) {
LOG.errorf("Erreur lors de la récupération de l'événement %d: %s", id, e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération de l'événement"))
.build();
}
}
/**
* Crée un nouvel événement
*/
@POST
@Operation(summary = "Créer un nouvel événement")
@APIResponse(responseCode = "201", description = "Événement créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT"})
public Response creerEvenement(
@Parameter(description = "Données de l'événement à créer", required = true)
@Valid Evenement evenement) {
try {
LOG.infof("POST /api/evenements - Création événement: %s", evenement.getTitre());
Evenement evenementCree = evenementService.creerEvenement(evenement);
return Response.status(Response.Status.CREATED)
.entity(evenementCree)
.build();
} catch (IllegalArgumentException e) {
LOG.warnf("Données invalides: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (SecurityException e) {
LOG.warnf("Permissions insuffisantes: %s", e.getMessage());
return Response.status(Response.Status.FORBIDDEN)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf("Erreur lors de la création: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la création de l'événement"))
.build();
}
}
/**
* Met à jour un événement existant
*/
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour un événement")
@APIResponse(responseCode = "200", description = "Événement mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Événement non trouvé")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT"})
public Response mettreAJourEvenement(
@PathParam("id") Long id,
@Valid Evenement evenement) {
try {
LOG.infof("PUT /api/evenements/%d", id);
Evenement evenementMisAJour = evenementService.mettreAJourEvenement(id, evenement);
return Response.ok(evenementMisAJour).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf("Erreur lors de la mise à jour: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la mise à jour"))
.build();
}
}
/**
* Supprime un événement
*/
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer un événement")
@APIResponse(responseCode = "204", description = "Événement supprimé avec succès")
@RolesAllowed({"ADMIN", "PRESIDENT", "ORGANISATEUR_EVENEMENT"})
public Response supprimerEvenement(@PathParam("id") Long id) {
try {
LOG.infof("DELETE /api/evenements/%d", id);
evenementService.supprimerEvenement(id);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalStateException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf("Erreur lors de la suppression: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la suppression"))
.build();
}
}
/**
* Endpoints spécialisés pour l'application mobile
*/
/**
* Liste les événements à venir
*/
@GET
@Path("/a-venir")
@Operation(summary = "Événements à venir")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"})
public Response evenementsAVenir(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
try {
List<Evenement> evenements = evenementService.listerEvenementsAVenir(
Page.of(page, size), Sort.by("dateDebut").ascending());
return Response.ok(evenements).build();
} catch (Exception e) {
LOG.errorf("Erreur événements à venir: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération"))
.build();
}
}
/**
* Liste les événements publics
*/
@GET
@Path("/publics")
@Operation(summary = "Événements publics")
public Response evenementsPublics(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
try {
List<Evenement> evenements = evenementService.listerEvenementsPublics(
Page.of(page, size), Sort.by("dateDebut").ascending());
return Response.ok(evenements).build();
} catch (Exception e) {
LOG.errorf("Erreur événements publics: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération"))
.build();
}
}
/**
* Recherche d'événements
*/
@GET
@Path("/recherche")
@Operation(summary = "Rechercher des événements")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"})
public Response rechercherEvenements(
@QueryParam("q") String recherche,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
try {
if (recherche == null || recherche.trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Le terme de recherche est obligatoire"))
.build();
}
List<Evenement> evenements = evenementService.rechercherEvenements(
recherche, Page.of(page, size), Sort.by("dateDebut").ascending());
return Response.ok(evenements).build();
} catch (Exception e) {
LOG.errorf("Erreur recherche: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la recherche"))
.build();
}
}
/**
* Événements par type
*/
@GET
@Path("/type/{type}")
@Operation(summary = "Événements par type")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"})
public Response evenementsParType(
@PathParam("type") TypeEvenement type,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
try {
List<Evenement> evenements = evenementService.listerParType(
type, Page.of(page, size), Sort.by("dateDebut").ascending());
return Response.ok(evenements).build();
} catch (Exception e) {
LOG.errorf("Erreur événements par type: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la récupération"))
.build();
}
}
/**
* Change le statut d'un événement
*/
@PATCH
@Path("/{id}/statut")
@Operation(summary = "Changer le statut d'un événement")
@RolesAllowed({"ADMIN", "PRESIDENT", "ORGANISATEUR_EVENEMENT"})
public Response changerStatut(
@PathParam("id") Long id,
@QueryParam("statut") StatutEvenement nouveauStatut) {
try {
if (nouveauStatut == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Le nouveau statut est obligatoire"))
.build();
}
Evenement evenement = evenementService.changerStatut(id, nouveauStatut);
return Response.ok(evenement).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (SecurityException e) {
return Response.status(Response.Status.FORBIDDEN)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf("Erreur changement statut: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du changement de statut"))
.build();
}
}
/**
* Statistiques des événements
*/
@GET
@Path("/statistiques")
@Operation(summary = "Statistiques des événements")
@RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT"})
public Response obtenirStatistiques() {
try {
Map<String, Object> statistiques = evenementService.obtenirStatistiques();
return Response.ok(statistiques).build();
} catch (Exception e) {
LOG.errorf("Erreur statistiques: %s", e.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul des statistiques"))
.build();
}
}
}

View File

@@ -0,0 +1,381 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.service.OrganisationService;
import dev.lions.unionflow.server.service.KeycloakService;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Resource REST pour la gestion des organisations
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@Path("/api/organisations")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Organisations", description = "Gestion des organisations")
@Authenticated
public class OrganisationResource {
private static final Logger LOG = Logger.getLogger(OrganisationResource.class);
@Inject
OrganisationService organisationService;
@Inject
KeycloakService keycloakService;
/**
* Crée une nouvelle organisation
*/
@POST
@Operation(summary = "Créer une nouvelle organisation", description = "Crée une nouvelle organisation dans le système")
@APIResponses({
@APIResponse(responseCode = "201", description = "Organisation créée avec succès",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = OrganisationDTO.class))),
@APIResponse(responseCode = "400", description = "Données invalides"),
@APIResponse(responseCode = "409", description = "Organisation déjà existante"),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response creerOrganisation(@Valid OrganisationDTO organisationDTO) {
LOG.infof("Création d'une nouvelle organisation: %s", organisationDTO.getNom());
try {
Organisation organisation = organisationService.convertFromDTO(organisationDTO);
Organisation organisationCreee = organisationService.creerOrganisation(organisation);
OrganisationDTO dto = organisationService.convertToDTO(organisationCreee);
return Response.created(URI.create("/api/organisations/" + organisationCreee.id))
.entity(dto)
.build();
} catch (IllegalArgumentException e) {
LOG.warnf("Erreur lors de la création de l'organisation: %s", e.getMessage());
return Response.status(Response.Status.CONFLICT)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur inattendue lors de la création de l'organisation");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
/**
* Récupère toutes les organisations actives
*/
@GET
@Operation(summary = "Lister les organisations", description = "Récupère la liste des organisations actives avec pagination")
@APIResponses({
@APIResponse(responseCode = "200", description = "Liste des organisations récupérée avec succès",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(type = SchemaType.ARRAY, implementation = OrganisationDTO.class))),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response listerOrganisations(
@Parameter(description = "Numéro de page (commence à 0)", example = "0")
@QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Taille de la page", example = "20")
@QueryParam("size") @DefaultValue("20") int size,
@Parameter(description = "Terme de recherche (nom ou nom court)")
@QueryParam("recherche") String recherche) {
LOG.infof("Récupération des organisations - page: %d, size: %d, recherche: %s", page, size, recherche);
try {
List<Organisation> organisations;
if (recherche != null && !recherche.trim().isEmpty()) {
organisations = organisationService.rechercherOrganisations(recherche.trim(), page, size);
} else {
organisations = organisationService.listerOrganisationsActives(page, size);
}
List<OrganisationDTO> dtos = organisations.stream()
.map(organisationService::convertToDTO)
.collect(Collectors.toList());
return Response.ok(dtos).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération des organisations");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
/**
* Récupère une organisation par son ID
*/
@GET
@Path("/{id}")
@Operation(summary = "Récupérer une organisation", description = "Récupère une organisation par son ID")
@APIResponses({
@APIResponse(responseCode = "200", description = "Organisation trouvée",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = OrganisationDTO.class))),
@APIResponse(responseCode = "404", description = "Organisation non trouvée"),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response obtenirOrganisation(
@Parameter(description = "ID de l'organisation", required = true)
@PathParam("id") Long id) {
LOG.infof("Récupération de l'organisation ID: %d", id);
return organisationService.trouverParId(id)
.map(organisation -> {
OrganisationDTO dto = organisationService.convertToDTO(organisation);
return Response.ok(dto).build();
})
.orElse(Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Organisation non trouvée"))
.build());
}
/**
* Met à jour une organisation
*/
@PUT
@Path("/{id}")
@Operation(summary = "Mettre à jour une organisation", description = "Met à jour les informations d'une organisation")
@APIResponses({
@APIResponse(responseCode = "200", description = "Organisation mise à jour avec succès",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = OrganisationDTO.class))),
@APIResponse(responseCode = "400", description = "Données invalides"),
@APIResponse(responseCode = "404", description = "Organisation non trouvée"),
@APIResponse(responseCode = "409", description = "Conflit de données"),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response mettreAJourOrganisation(
@Parameter(description = "ID de l'organisation", required = true)
@PathParam("id") Long id,
@Valid OrganisationDTO organisationDTO) {
LOG.infof("Mise à jour de l'organisation ID: %d", id);
try {
Organisation organisationMiseAJour = organisationService.convertFromDTO(organisationDTO);
Organisation organisation = organisationService.mettreAJourOrganisation(id, organisationMiseAJour, "system");
OrganisationDTO dto = organisationService.convertToDTO(organisation);
return Response.ok(dto).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalArgumentException e) {
LOG.warnf("Erreur lors de la mise à jour de l'organisation: %s", e.getMessage());
return Response.status(Response.Status.CONFLICT)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur inattendue lors de la mise à jour de l'organisation");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
/**
* Supprime une organisation
*/
@DELETE
@Path("/{id}")
@Operation(summary = "Supprimer une organisation", description = "Supprime une organisation (soft delete)")
@APIResponses({
@APIResponse(responseCode = "204", description = "Organisation supprimée avec succès"),
@APIResponse(responseCode = "404", description = "Organisation non trouvée"),
@APIResponse(responseCode = "409", description = "Impossible de supprimer l'organisation"),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response supprimerOrganisation(
@Parameter(description = "ID de l'organisation", required = true)
@PathParam("id") Long id) {
LOG.infof("Suppression de l'organisation ID: %d", id);
try {
organisationService.supprimerOrganisation(id, "system");
return Response.noContent().build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (IllegalStateException e) {
LOG.warnf("Erreur lors de la suppression de l'organisation: %s", e.getMessage());
return Response.status(Response.Status.CONFLICT)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur inattendue lors de la suppression de l'organisation");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
/**
* Recherche avancée d'organisations
*/
@GET
@Path("/recherche")
@Operation(summary = "Recherche avancée", description = "Recherche d'organisations avec critères multiples")
@APIResponses({
@APIResponse(responseCode = "200", description = "Résultats de recherche",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(type = SchemaType.ARRAY, implementation = OrganisationDTO.class))),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response rechercheAvancee(
@Parameter(description = "Nom de l'organisation") @QueryParam("nom") String nom,
@Parameter(description = "Type d'organisation") @QueryParam("type") String typeOrganisation,
@Parameter(description = "Statut") @QueryParam("statut") String statut,
@Parameter(description = "Ville") @QueryParam("ville") String ville,
@Parameter(description = "Région") @QueryParam("region") String region,
@Parameter(description = "Pays") @QueryParam("pays") String pays,
@Parameter(description = "Numéro de page") @QueryParam("page") @DefaultValue("0") int page,
@Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") int size) {
LOG.infof("Recherche avancée d'organisations avec critères multiples");
try {
List<Organisation> organisations = organisationService.rechercheAvancee(
nom, typeOrganisation, statut, ville, region, pays, page, size);
List<OrganisationDTO> dtos = organisations.stream()
.map(organisationService::convertToDTO)
.collect(Collectors.toList());
return Response.ok(dtos).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche avancée");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
/**
* Active une organisation
*/
@POST
@Path("/{id}/activer")
@Operation(summary = "Activer une organisation", description = "Active une organisation suspendue")
@APIResponses({
@APIResponse(responseCode = "200", description = "Organisation activée avec succès"),
@APIResponse(responseCode = "404", description = "Organisation non trouvée"),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response activerOrganisation(
@Parameter(description = "ID de l'organisation", required = true)
@PathParam("id") Long id) {
LOG.infof("Activation de l'organisation ID: %d", id);
try {
Organisation organisation = organisationService.activerOrganisation(id, "system");
OrganisationDTO dto = organisationService.convertToDTO(organisation);
return Response.ok(dto).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'activation de l'organisation");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
/**
* Suspend une organisation
*/
@POST
@Path("/{id}/suspendre")
@Operation(summary = "Suspendre une organisation", description = "Suspend une organisation active")
@APIResponses({
@APIResponse(responseCode = "200", description = "Organisation suspendue avec succès"),
@APIResponse(responseCode = "404", description = "Organisation non trouvée"),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response suspendreOrganisation(
@Parameter(description = "ID de l'organisation", required = true)
@PathParam("id") Long id) {
LOG.infof("Suspension de l'organisation ID: %d", id);
try {
Organisation organisation = organisationService.suspendreOrganisation(id, "system");
OrganisationDTO dto = organisationService.convertToDTO(organisation);
return Response.ok(dto).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la suspension de l'organisation");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
/**
* Obtient les statistiques des organisations
*/
@GET
@Path("/statistiques")
@Operation(summary = "Statistiques des organisations", description = "Récupère les statistiques globales des organisations")
@APIResponses({
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès"),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response obtenirStatistiques() {
LOG.info("Récupération des statistiques des organisations");
try {
Map<String, Object> statistiques = organisationService.obtenirStatistiques();
return Response.ok(statistiques).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération des statistiques");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
}
}
}

View File

@@ -0,0 +1,222 @@
package dev.lions.unionflow.server.security;
import dev.lions.unionflow.server.service.KeycloakService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;
import java.util.Set;
/**
* Configuration et utilitaires de sécurité avec Keycloak
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@ApplicationScoped
public class SecurityConfig {
private static final Logger LOG = Logger.getLogger(SecurityConfig.class);
@Inject
KeycloakService keycloakService;
/**
* Rôles disponibles dans l'application
*/
public static class Roles {
public static final String ADMIN = "ADMIN";
public static final String GESTIONNAIRE_MEMBRE = "GESTIONNAIRE_MEMBRE";
public static final String TRESORIER = "TRESORIER";
public static final String SECRETAIRE = "SECRETAIRE";
public static final String MEMBRE = "MEMBRE";
public static final String PRESIDENT = "PRESIDENT";
public static final String VICE_PRESIDENT = "VICE_PRESIDENT";
public static final String ORGANISATEUR_EVENEMENT = "ORGANISATEUR_EVENEMENT";
public static final String GESTIONNAIRE_SOLIDARITE = "GESTIONNAIRE_SOLIDARITE";
public static final String AUDITEUR = "AUDITEUR";
}
/**
* Permissions disponibles dans l'application
*/
public static class Permissions {
// Permissions membres
public static final String CREATE_MEMBRE = "CREATE_MEMBRE";
public static final String READ_MEMBRE = "READ_MEMBRE";
public static final String UPDATE_MEMBRE = "UPDATE_MEMBRE";
public static final String DELETE_MEMBRE = "DELETE_MEMBRE";
// Permissions organisations
public static final String CREATE_ORGANISATION = "CREATE_ORGANISATION";
public static final String READ_ORGANISATION = "READ_ORGANISATION";
public static final String UPDATE_ORGANISATION = "UPDATE_ORGANISATION";
public static final String DELETE_ORGANISATION = "DELETE_ORGANISATION";
// Permissions événements
public static final String CREATE_EVENEMENT = "CREATE_EVENEMENT";
public static final String READ_EVENEMENT = "READ_EVENEMENT";
public static final String UPDATE_EVENEMENT = "UPDATE_EVENEMENT";
public static final String DELETE_EVENEMENT = "DELETE_EVENEMENT";
// Permissions finances
public static final String CREATE_COTISATION = "CREATE_COTISATION";
public static final String READ_COTISATION = "READ_COTISATION";
public static final String UPDATE_COTISATION = "UPDATE_COTISATION";
public static final String DELETE_COTISATION = "DELETE_COTISATION";
// Permissions solidarité
public static final String CREATE_SOLIDARITE = "CREATE_SOLIDARITE";
public static final String READ_SOLIDARITE = "READ_SOLIDARITE";
public static final String UPDATE_SOLIDARITE = "UPDATE_SOLIDARITE";
public static final String DELETE_SOLIDARITE = "DELETE_SOLIDARITE";
// Permissions administration
public static final String ADMIN_USERS = "ADMIN_USERS";
public static final String ADMIN_SYSTEM = "ADMIN_SYSTEM";
public static final String VIEW_REPORTS = "VIEW_REPORTS";
public static final String EXPORT_DATA = "EXPORT_DATA";
}
/**
* Vérifie si l'utilisateur actuel a un rôle spécifique
*
* @param role le rôle à vérifier
* @return true si l'utilisateur a le rôle
*/
public boolean hasRole(String role) {
return keycloakService.hasRole(role);
}
/**
* Vérifie si l'utilisateur actuel a au moins un des rôles spécifiés
*
* @param roles les rôles à vérifier
* @return true si l'utilisateur a au moins un des rôles
*/
public boolean hasAnyRole(String... roles) {
return keycloakService.hasAnyRole(roles);
}
/**
* Vérifie si l'utilisateur actuel a tous les rôles spécifiés
*
* @param roles les rôles à vérifier
* @return true si l'utilisateur a tous les rôles
*/
public boolean hasAllRoles(String... roles) {
return keycloakService.hasAllRoles(roles);
}
/**
* Obtient l'ID de l'utilisateur actuel
*
* @return l'ID de l'utilisateur ou null si non authentifié
*/
public String getCurrentUserId() {
return keycloakService.getCurrentUserId();
}
/**
* Obtient l'email de l'utilisateur actuel
*
* @return l'email de l'utilisateur ou null si non authentifié
*/
public String getCurrentUserEmail() {
return keycloakService.getCurrentUserEmail();
}
/**
* Obtient tous les rôles de l'utilisateur actuel
*
* @return les rôles de l'utilisateur
*/
public Set<String> getCurrentUserRoles() {
return keycloakService.getCurrentUserRoles();
}
/**
* Vérifie si l'utilisateur actuel est authentifié
*
* @return true si l'utilisateur est authentifié
*/
public boolean isAuthenticated() {
return keycloakService.isAuthenticated();
}
/**
* Vérifie si l'utilisateur actuel est un administrateur
*
* @return true si l'utilisateur est administrateur
*/
public boolean isAdmin() {
return hasRole(Roles.ADMIN);
}
/**
* Vérifie si l'utilisateur actuel peut gérer les membres
*
* @return true si l'utilisateur peut gérer les membres
*/
public boolean canManageMembers() {
return hasAnyRole(Roles.ADMIN, Roles.GESTIONNAIRE_MEMBRE, Roles.PRESIDENT, Roles.SECRETAIRE);
}
/**
* Vérifie si l'utilisateur actuel peut gérer les finances
*
* @return true si l'utilisateur peut gérer les finances
*/
public boolean canManageFinances() {
return hasAnyRole(Roles.ADMIN, Roles.TRESORIER, Roles.PRESIDENT);
}
/**
* Vérifie si l'utilisateur actuel peut gérer les événements
*
* @return true si l'utilisateur peut gérer les événements
*/
public boolean canManageEvents() {
return hasAnyRole(Roles.ADMIN, Roles.ORGANISATEUR_EVENEMENT, Roles.PRESIDENT, Roles.SECRETAIRE);
}
/**
* Vérifie si l'utilisateur actuel peut gérer les organisations
*
* @return true si l'utilisateur peut gérer les organisations
*/
public boolean canManageOrganizations() {
return hasAnyRole(Roles.ADMIN, Roles.PRESIDENT);
}
/**
* Vérifie si l'utilisateur actuel peut accéder aux données d'un membre spécifique
*
* @param membreId l'ID du membre
* @return true si l'utilisateur peut accéder aux données
*/
public boolean canAccessMemberData(String membreId) {
// Un utilisateur peut toujours accéder à ses propres données
if (membreId.equals(getCurrentUserId())) {
return true;
}
// Les gestionnaires peuvent accéder aux données de tous les membres
return canManageMembers();
}
/**
* Log les informations de sécurité pour debug
*/
public void logSecurityInfo() {
if (LOG.isDebugEnabled()) {
if (isAuthenticated()) {
LOG.debugf("Utilisateur authentifié: %s, Rôles: %s",
getCurrentUserEmail(), getCurrentUserRoles());
} else {
LOG.debug("Utilisateur non authentifié");
}
}
}
}

View File

@@ -0,0 +1,331 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Service métier pour la gestion des événements
* Version simplifiée pour tester les imports et Lombok
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@ApplicationScoped
public class EvenementService {
private static final Logger LOG = Logger.getLogger(EvenementService.class);
@Inject
EvenementRepository evenementRepository;
@Inject
MembreRepository membreRepository;
@Inject
OrganisationRepository organisationRepository;
@Inject
KeycloakService keycloakService;
/**
* Crée un nouvel événement
*
* @param evenement l'événement à créer
* @return l'événement créé
* @throws IllegalArgumentException si les données sont invalides
*/
@Transactional
public Evenement creerEvenement(Evenement evenement) {
LOG.infof("Création événement: %s", evenement.getTitre());
// Validation des données
validerEvenement(evenement);
// Vérifier l'unicité du titre dans l'organisation
if (evenement.getOrganisation() != null) {
Optional<Evenement> existant = evenementRepository.findByTitre(evenement.getTitre());
if (existant.isPresent() &&
existant.get().getOrganisation().id.equals(evenement.getOrganisation().id)) {
throw new IllegalArgumentException("Un événement avec ce titre existe déjà dans cette organisation");
}
}
// Métadonnées de création
evenement.setCreePar(keycloakService.getCurrentUserEmail());
evenement.setDateCreation(LocalDateTime.now());
// Valeurs par défaut
if (evenement.getStatut() == null) {
evenement.setStatut(StatutEvenement.PLANIFIE);
}
if (evenement.getActif() == null) {
evenement.setActif(true);
}
if (evenement.getVisiblePublic() == null) {
evenement.setVisiblePublic(true);
}
if (evenement.getInscriptionRequise() == null) {
evenement.setInscriptionRequise(true);
}
evenement.persist();
LOG.infof("Événement créé avec succès: ID=%d, Titre=%s", evenement.id, evenement.getTitre());
return evenement;
}
/**
* Met à jour un événement existant
*
* @param id l'ID de l'événement
* @param evenementMisAJour les nouvelles données
* @return l'événement mis à jour
* @throws IllegalArgumentException si l'événement n'existe pas
*/
@Transactional
public Evenement mettreAJourEvenement(Long id, Evenement evenementMisAJour) {
LOG.infof("Mise à jour événement ID: %d", id);
Evenement evenementExistant = evenementRepository.findByIdOptional(id)
.orElseThrow(() -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id));
// Vérifier les permissions
if (!peutModifierEvenement(evenementExistant)) {
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
}
// Validation des nouvelles données
validerEvenement(evenementMisAJour);
// Mise à jour des champs
evenementExistant.setTitre(evenementMisAJour.getTitre());
evenementExistant.setDescription(evenementMisAJour.getDescription());
evenementExistant.setDateDebut(evenementMisAJour.getDateDebut());
evenementExistant.setDateFin(evenementMisAJour.getDateFin());
evenementExistant.setLieu(evenementMisAJour.getLieu());
evenementExistant.setAdresse(evenementMisAJour.getAdresse());
evenementExistant.setTypeEvenement(evenementMisAJour.getTypeEvenement());
evenementExistant.setCapaciteMax(evenementMisAJour.getCapaciteMax());
evenementExistant.setPrix(evenementMisAJour.getPrix());
evenementExistant.setInscriptionRequise(evenementMisAJour.getInscriptionRequise());
evenementExistant.setDateLimiteInscription(evenementMisAJour.getDateLimiteInscription());
evenementExistant.setInstructionsParticulieres(evenementMisAJour.getInstructionsParticulieres());
evenementExistant.setContactOrganisateur(evenementMisAJour.getContactOrganisateur());
evenementExistant.setMaterielRequis(evenementMisAJour.getMaterielRequis());
evenementExistant.setVisiblePublic(evenementMisAJour.getVisiblePublic());
// Métadonnées de modification
evenementExistant.setModifiePar(keycloakService.getCurrentUserEmail());
evenementExistant.setDateModification(LocalDateTime.now());
evenementExistant.persist();
LOG.infof("Événement mis à jour avec succès: ID=%d", id);
return evenementExistant;
}
/**
* Trouve un événement par ID
*/
public Optional<Evenement> trouverParId(Long id) {
return evenementRepository.findByIdOptional(id);
}
/**
* Liste tous les événements actifs avec pagination
*/
public List<Evenement> listerEvenementsActifs(Page page, Sort sort) {
return evenementRepository.findAllActifs(page, sort);
}
/**
* Liste les événements à venir
*/
public List<Evenement> listerEvenementsAVenir(Page page, Sort sort) {
return evenementRepository.findEvenementsAVenir(page, sort);
}
/**
* Liste les événements publics
*/
public List<Evenement> listerEvenementsPublics(Page page, Sort sort) {
return evenementRepository.findEvenementsPublics(page, sort);
}
/**
* Recherche d'événements par terme
*/
public List<Evenement> rechercherEvenements(String terme, Page page, Sort sort) {
return evenementRepository.rechercheAvancee(terme, null, null, null, null,
null, null, null, null, null, page, sort);
}
/**
* Liste les événements par type
*/
public List<Evenement> listerParType(TypeEvenement type, Page page, Sort sort) {
return evenementRepository.findByType(type, page, sort);
}
/**
* Supprime logiquement un événement
*
* @param id l'ID de l'événement à supprimer
* @throws IllegalArgumentException si l'événement n'existe pas
*/
@Transactional
public void supprimerEvenement(Long id) {
LOG.infof("Suppression événement ID: %d", id);
Evenement evenement = evenementRepository.findByIdOptional(id)
.orElseThrow(() -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id));
// Vérifier les permissions
if (!peutModifierEvenement(evenement)) {
throw new SecurityException("Vous n'avez pas les permissions pour supprimer cet événement");
}
// Vérifier s'il y a des inscriptions
if (evenement.getNombreInscrits() > 0) {
throw new IllegalStateException("Impossible de supprimer un événement avec des inscriptions");
}
// Suppression logique
evenement.setActif(false);
evenement.setModifiePar(keycloakService.getCurrentUserEmail());
evenement.setDateModification(LocalDateTime.now());
evenement.persist();
LOG.infof("Événement supprimé avec succès: ID=%d", id);
}
/**
* Change le statut d'un événement
*
* @param id l'ID de l'événement
* @param nouveauStatut le nouveau statut
* @return l'événement mis à jour
*/
@Transactional
public Evenement changerStatut(Long id, StatutEvenement nouveauStatut) {
LOG.infof("Changement statut événement ID: %d vers %s", id, nouveauStatut);
Evenement evenement = evenementRepository.findByIdOptional(id)
.orElseThrow(() -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id));
// Vérifier les permissions
if (!peutModifierEvenement(evenement)) {
throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement");
}
// Valider le changement de statut
validerChangementStatut(evenement.getStatut(), nouveauStatut);
evenement.setStatut(nouveauStatut);
evenement.setModifiePar(keycloakService.getCurrentUserEmail());
evenement.setDateModification(LocalDateTime.now());
evenement.persist();
LOG.infof("Statut événement changé avec succès: ID=%d, Nouveau statut=%s", id, nouveauStatut);
return evenement;
}
/**
* Obtient les statistiques des événements
*
* @return les statistiques sous forme de Map
*/
public Map<String, Object> obtenirStatistiques() {
Map<String, Long> statsBase = evenementRepository.getStatistiques();
long total = statsBase.getOrDefault("total", 0L);
long actifs = statsBase.getOrDefault("actifs", 0L);
long aVenir = statsBase.getOrDefault("aVenir", 0L);
long enCours = statsBase.getOrDefault("enCours", 0L);
Map<String, Object> result = new java.util.HashMap<>();
result.put("total", total);
result.put("actifs", actifs);
result.put("aVenir", aVenir);
result.put("enCours", enCours);
result.put("passes", statsBase.getOrDefault("passes", 0L));
result.put("publics", statsBase.getOrDefault("publics", 0L));
result.put("avecInscription", statsBase.getOrDefault("avecInscription", 0L));
result.put("tauxActivite", total > 0 ? (actifs * 100.0 / total) : 0.0);
result.put("tauxEvenementsAVenir", total > 0 ? (aVenir * 100.0 / total) : 0.0);
result.put("tauxEvenementsEnCours", total > 0 ? (enCours * 100.0 / total) : 0.0);
result.put("timestamp", LocalDateTime.now());
return result;
}
// Méthodes privées de validation et permissions
/**
* Valide les données d'un événement
*/
private void validerEvenement(Evenement evenement) {
if (evenement.getTitre() == null || evenement.getTitre().trim().isEmpty()) {
throw new IllegalArgumentException("Le titre de l'événement est obligatoire");
}
if (evenement.getDateDebut() == null) {
throw new IllegalArgumentException("La date de début est obligatoire");
}
if (evenement.getDateDebut().isBefore(LocalDateTime.now().minusHours(1))) {
throw new IllegalArgumentException("La date de début ne peut pas être dans le passé");
}
if (evenement.getDateFin() != null && evenement.getDateFin().isBefore(evenement.getDateDebut())) {
throw new IllegalArgumentException("La date de fin ne peut pas être antérieure à la date de début");
}
if (evenement.getCapaciteMax() != null && evenement.getCapaciteMax() <= 0) {
throw new IllegalArgumentException("La capacité maximale doit être positive");
}
if (evenement.getPrix() != null && evenement.getPrix().compareTo(java.math.BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}
}
/**
* Valide un changement de statut
*/
private void validerChangementStatut(StatutEvenement statutActuel, StatutEvenement nouveauStatut) {
// Règles de transition simplifiées pour la version mobile
if (statutActuel == StatutEvenement.TERMINE || statutActuel == StatutEvenement.ANNULE) {
throw new IllegalArgumentException("Impossible de changer le statut d'un événement terminé ou annulé");
}
}
/**
* Vérifie les permissions de modification pour l'application mobile
*/
private boolean peutModifierEvenement(Evenement evenement) {
if (keycloakService.hasRole("ADMIN") || keycloakService.hasRole("ORGANISATEUR_EVENEMENT")) {
return true;
}
String utilisateurActuel = keycloakService.getCurrentUserEmail();
return utilisateurActuel != null && utilisateurActuel.equals(evenement.getCreePar());
}
}

View File

@@ -0,0 +1,306 @@
package dev.lions.unionflow.server.service;
import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Service pour l'intégration avec Keycloak
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@ApplicationScoped
public class KeycloakService {
private static final Logger LOG = Logger.getLogger(KeycloakService.class);
@Inject
SecurityIdentity securityIdentity;
@Inject
JsonWebToken jwt;
/**
* Vérifie si l'utilisateur actuel est authentifié
*
* @return true si l'utilisateur est authentifié
*/
public boolean isAuthenticated() {
return securityIdentity != null && !securityIdentity.isAnonymous();
}
/**
* Obtient l'ID de l'utilisateur actuel depuis Keycloak
*
* @return l'ID de l'utilisateur ou null si non authentifié
*/
public String getCurrentUserId() {
if (!isAuthenticated()) {
return null;
}
try {
return jwt.getSubject();
} catch (Exception e) {
LOG.warnf("Erreur lors de la récupération de l'ID utilisateur: %s", e.getMessage());
return null;
}
}
/**
* Obtient l'email de l'utilisateur actuel
*
* @return l'email de l'utilisateur ou null si non authentifié
*/
public String getCurrentUserEmail() {
if (!isAuthenticated()) {
return null;
}
try {
return jwt.getClaim("email");
} catch (Exception e) {
LOG.warnf("Erreur lors de la récupération de l'email utilisateur: %s", e.getMessage());
return securityIdentity.getPrincipal().getName();
}
}
/**
* Obtient le nom complet de l'utilisateur actuel
*
* @return le nom complet ou null si non disponible
*/
public String getCurrentUserFullName() {
if (!isAuthenticated()) {
return null;
}
try {
String firstName = jwt.getClaim("given_name");
String lastName = jwt.getClaim("family_name");
if (firstName != null && lastName != null) {
return firstName + " " + lastName;
} else if (firstName != null) {
return firstName;
} else if (lastName != null) {
return lastName;
}
// Fallback sur le nom d'utilisateur
return jwt.getClaim("preferred_username");
} catch (Exception e) {
LOG.warnf("Erreur lors de la récupération du nom utilisateur: %s", e.getMessage());
return null;
}
}
/**
* Obtient tous les rôles de l'utilisateur actuel
*
* @return les rôles de l'utilisateur
*/
public Set<String> getCurrentUserRoles() {
if (!isAuthenticated()) {
return Set.of();
}
return securityIdentity.getRoles();
}
/**
* Vérifie si l'utilisateur actuel a un rôle spécifique
*
* @param role le rôle à vérifier
* @return true si l'utilisateur a le rôle
*/
public boolean hasRole(String role) {
if (!isAuthenticated()) {
return false;
}
return securityIdentity.hasRole(role);
}
/**
* Vérifie si l'utilisateur actuel a au moins un des rôles spécifiés
*
* @param roles les rôles à vérifier
* @return true si l'utilisateur a au moins un des rôles
*/
public boolean hasAnyRole(String... roles) {
if (!isAuthenticated()) {
return false;
}
for (String role : roles) {
if (securityIdentity.hasRole(role)) {
return true;
}
}
return false;
}
/**
* Vérifie si l'utilisateur actuel a tous les rôles spécifiés
*
* @param roles les rôles à vérifier
* @return true si l'utilisateur a tous les rôles
*/
public boolean hasAllRoles(String... roles) {
if (!isAuthenticated()) {
return false;
}
for (String role : roles) {
if (!securityIdentity.hasRole(role)) {
return false;
}
}
return true;
}
/**
* Obtient une claim spécifique du JWT
*
* @param claimName le nom de la claim
* @return la valeur de la claim ou null si non trouvée
*/
public <T> T getClaim(String claimName) {
if (!isAuthenticated()) {
return null;
}
try {
return jwt.getClaim(claimName);
} catch (Exception e) {
LOG.warnf("Erreur lors de la récupération de la claim %s: %s", claimName, e.getMessage());
return null;
}
}
/**
* Obtient toutes les claims du JWT
*
* @return toutes les claims ou une map vide si non authentifié
*/
public Set<String> getAllClaimNames() {
if (!isAuthenticated()) {
return Set.of();
}
try {
return jwt.getClaimNames();
} catch (Exception e) {
LOG.warnf("Erreur lors de la récupération des claims: %s", e.getMessage());
return Set.of();
}
}
/**
* Obtient les informations utilisateur pour les logs
*
* @return informations utilisateur formatées
*/
public String getUserInfoForLogging() {
if (!isAuthenticated()) {
return "Utilisateur non authentifié";
}
String email = getCurrentUserEmail();
String fullName = getCurrentUserFullName();
Set<String> roles = getCurrentUserRoles();
return String.format("Utilisateur: %s (%s), Rôles: %s",
fullName != null ? fullName : "N/A",
email != null ? email : "N/A",
roles);
}
/**
* Vérifie si l'utilisateur actuel est un administrateur
*
* @return true si l'utilisateur est administrateur
*/
public boolean isAdmin() {
return hasRole("ADMIN") || hasRole("admin");
}
/**
* Vérifie si l'utilisateur actuel peut gérer les membres
*
* @return true si l'utilisateur peut gérer les membres
*/
public boolean canManageMembers() {
return hasAnyRole("ADMIN", "GESTIONNAIRE_MEMBRE", "PRESIDENT", "SECRETAIRE",
"admin", "gestionnaire_membre", "president", "secretaire");
}
/**
* Vérifie si l'utilisateur actuel peut gérer les finances
*
* @return true si l'utilisateur peut gérer les finances
*/
public boolean canManageFinances() {
return hasAnyRole("ADMIN", "TRESORIER", "PRESIDENT",
"admin", "tresorier", "president");
}
/**
* Vérifie si l'utilisateur actuel peut gérer les événements
*
* @return true si l'utilisateur peut gérer les événements
*/
public boolean canManageEvents() {
return hasAnyRole("ADMIN", "ORGANISATEUR_EVENEMENT", "PRESIDENT", "SECRETAIRE",
"admin", "organisateur_evenement", "president", "secretaire");
}
/**
* Vérifie si l'utilisateur actuel peut gérer les organisations
*
* @return true si l'utilisateur peut gérer les organisations
*/
public boolean canManageOrganizations() {
return hasAnyRole("ADMIN", "PRESIDENT", "admin", "president");
}
/**
* Log les informations de sécurité pour debug
*/
public void logSecurityInfo() {
if (LOG.isDebugEnabled()) {
LOG.debugf("Informations de sécurité: %s", getUserInfoForLogging());
}
}
/**
* Obtient le token d'accès brut
*
* @return le token JWT brut ou null si non disponible
*/
public String getRawAccessToken() {
if (!isAuthenticated()) {
return null;
}
try {
if (jwt instanceof OidcJwtCallerPrincipal) {
return ((OidcJwtCallerPrincipal) jwt).getRawToken();
}
return jwt.getRawToken();
} catch (Exception e) {
LOG.warnf("Erreur lors de la récupération du token brut: %s", e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,353 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import org.jboss.logging.Logger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Service métier pour la gestion des organisations
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@ApplicationScoped
public class OrganisationService {
private static final Logger LOG = Logger.getLogger(OrganisationService.class);
@Inject
OrganisationRepository organisationRepository;
/**
* Crée une nouvelle organisation
*
* @param organisation l'organisation à créer
* @return l'organisation créée
*/
@Transactional
public Organisation creerOrganisation(Organisation organisation) {
LOG.infof("Création d'une nouvelle organisation: %s", organisation.getNom());
// Vérifier l'unicité de l'email
if (organisationRepository.findByEmail(organisation.getEmail()).isPresent()) {
throw new IllegalArgumentException("Une organisation avec cet email existe déjà");
}
// Vérifier l'unicité du nom
if (organisationRepository.findByNom(organisation.getNom()).isPresent()) {
throw new IllegalArgumentException("Une organisation avec ce nom existe déjà");
}
// Vérifier l'unicité du numéro d'enregistrement si fourni
if (organisation.getNumeroEnregistrement() != null &&
!organisation.getNumeroEnregistrement().isEmpty()) {
if (organisationRepository.findByNumeroEnregistrement(organisation.getNumeroEnregistrement()).isPresent()) {
throw new IllegalArgumentException("Une organisation avec ce numéro d'enregistrement existe déjà");
}
}
// Définir les valeurs par défaut
if (organisation.getStatut() == null) {
organisation.setStatut("ACTIVE");
}
if (organisation.getTypeOrganisation() == null) {
organisation.setTypeOrganisation("ASSOCIATION");
}
organisation.persist();
LOG.infof("Organisation créée avec succès: ID=%d, Nom=%s", organisation.id, organisation.getNom());
return organisation;
}
/**
* Met à jour une organisation existante
*
* @param id l'ID de l'organisation
* @param organisationMiseAJour les données de mise à jour
* @param utilisateur l'utilisateur effectuant la modification
* @return l'organisation mise à jour
*/
@Transactional
public Organisation mettreAJourOrganisation(Long id, Organisation organisationMiseAJour, String utilisateur) {
LOG.infof("Mise à jour de l'organisation ID: %d", id);
Organisation organisation = organisationRepository.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
// Vérifier l'unicité de l'email si modifié
if (!organisation.getEmail().equals(organisationMiseAJour.getEmail())) {
if (organisationRepository.findByEmail(organisationMiseAJour.getEmail()).isPresent()) {
throw new IllegalArgumentException("Une organisation avec cet email existe déjà");
}
organisation.setEmail(organisationMiseAJour.getEmail());
}
// Vérifier l'unicité du nom si modifié
if (!organisation.getNom().equals(organisationMiseAJour.getNom())) {
if (organisationRepository.findByNom(organisationMiseAJour.getNom()).isPresent()) {
throw new IllegalArgumentException("Une organisation avec ce nom existe déjà");
}
organisation.setNom(organisationMiseAJour.getNom());
}
// Mettre à jour les autres champs
organisation.setNomCourt(organisationMiseAJour.getNomCourt());
organisation.setDescription(organisationMiseAJour.getDescription());
organisation.setTelephone(organisationMiseAJour.getTelephone());
organisation.setAdresse(organisationMiseAJour.getAdresse());
organisation.setVille(organisationMiseAJour.getVille());
organisation.setCodePostal(organisationMiseAJour.getCodePostal());
organisation.setRegion(organisationMiseAJour.getRegion());
organisation.setPays(organisationMiseAJour.getPays());
organisation.setSiteWeb(organisationMiseAJour.getSiteWeb());
organisation.setObjectifs(organisationMiseAJour.getObjectifs());
organisation.setActivitesPrincipales(organisationMiseAJour.getActivitesPrincipales());
organisation.marquerCommeModifie(utilisateur);
LOG.infof("Organisation mise à jour avec succès: ID=%d", id);
return organisation;
}
/**
* Supprime une organisation
*
* @param id l'ID de l'organisation
* @param utilisateur l'utilisateur effectuant la suppression
*/
@Transactional
public void supprimerOrganisation(Long id, String utilisateur) {
LOG.infof("Suppression de l'organisation ID: %d", id);
Organisation organisation = organisationRepository.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
// Vérifier qu'il n'y a pas de membres actifs
if (organisation.getNombreMembres() > 0) {
throw new IllegalStateException("Impossible de supprimer une organisation avec des membres actifs");
}
// Soft delete - marquer comme inactive
organisation.setActif(false);
organisation.setStatut("DISSOUTE");
organisation.marquerCommeModifie(utilisateur);
LOG.infof("Organisation supprimée (soft delete) avec succès: ID=%d", id);
}
/**
* Trouve une organisation par son ID
*
* @param id l'ID de l'organisation
* @return Optional contenant l'organisation si trouvée
*/
public Optional<Organisation> trouverParId(Long id) {
return organisationRepository.findByIdOptional(id);
}
/**
* Trouve une organisation par son email
*
* @param email l'email de l'organisation
* @return Optional contenant l'organisation si trouvée
*/
public Optional<Organisation> trouverParEmail(String email) {
return organisationRepository.findByEmail(email);
}
/**
* Liste toutes les organisations actives
*
* @return liste des organisations actives
*/
public List<Organisation> listerOrganisationsActives() {
return organisationRepository.findAllActives();
}
/**
* Liste toutes les organisations actives avec pagination
*
* @param page numéro de page
* @param size taille de la page
* @return liste paginée des organisations actives
*/
public List<Organisation> listerOrganisationsActives(int page, int size) {
return organisationRepository.findAllActives(Page.of(page, size), Sort.by("nom").ascending());
}
/**
* Recherche d'organisations par nom
*
* @param recherche terme de recherche
* @param page numéro de page
* @param size taille de la page
* @return liste paginée des organisations correspondantes
*/
public List<Organisation> rechercherOrganisations(String recherche, int page, int size) {
return organisationRepository.findByNomOrNomCourt(recherche,
Page.of(page, size), Sort.by("nom").ascending());
}
/**
* Recherche avancée d'organisations
*
* @param nom nom (optionnel)
* @param typeOrganisation type (optionnel)
* @param statut statut (optionnel)
* @param ville ville (optionnel)
* @param region région (optionnel)
* @param pays pays (optionnel)
* @param page numéro de page
* @param size taille de la page
* @return liste filtrée des organisations
*/
public List<Organisation> rechercheAvancee(String nom, String typeOrganisation, String statut,
String ville, String region, String pays, int page, int size) {
return organisationRepository.rechercheAvancee(nom, typeOrganisation, statut, ville, region, pays,
Page.of(page, size));
}
/**
* Active une organisation
*
* @param id l'ID de l'organisation
* @param utilisateur l'utilisateur effectuant l'activation
* @return l'organisation activée
*/
@Transactional
public Organisation activerOrganisation(Long id, String utilisateur) {
LOG.infof("Activation de l'organisation ID: %d", id);
Organisation organisation = organisationRepository.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
organisation.activer(utilisateur);
LOG.infof("Organisation activée avec succès: ID=%d", id);
return organisation;
}
/**
* Suspend une organisation
*
* @param id l'ID de l'organisation
* @param utilisateur l'utilisateur effectuant la suspension
* @return l'organisation suspendue
*/
@Transactional
public Organisation suspendreOrganisation(Long id, String utilisateur) {
LOG.infof("Suspension de l'organisation ID: %d", id);
Organisation organisation = organisationRepository.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id));
organisation.suspendre(utilisateur);
LOG.infof("Organisation suspendue avec succès: ID=%d", id);
return organisation;
}
/**
* Obtient les statistiques des organisations
*
* @return map contenant les statistiques
*/
public Map<String, Object> obtenirStatistiques() {
LOG.info("Calcul des statistiques des organisations");
long totalOrganisations = organisationRepository.count();
long organisationsActives = organisationRepository.countActives();
long organisationsInactives = totalOrganisations - organisationsActives;
long nouvellesOrganisations30Jours = organisationRepository.countNouvellesOrganisations(
LocalDate.now().minusDays(30));
return Map.of(
"totalOrganisations", totalOrganisations,
"organisationsActives", organisationsActives,
"organisationsInactives", organisationsInactives,
"nouvellesOrganisations30Jours", nouvellesOrganisations30Jours,
"tauxActivite", totalOrganisations > 0 ? (organisationsActives * 100.0 / totalOrganisations) : 0.0,
"timestamp", LocalDateTime.now()
);
}
/**
* Convertit une entité Organisation en DTO
*
* @param organisation l'entité à convertir
* @return le DTO correspondant
*/
public OrganisationDTO convertToDTO(Organisation organisation) {
if (organisation == null) {
return null;
}
OrganisationDTO dto = new OrganisationDTO();
dto.setId(UUID.randomUUID()); // Temporaire - à adapter selon votre logique d'ID
dto.setNom(organisation.getNom());
dto.setNomCourt(organisation.getNomCourt());
dto.setDescription(organisation.getDescription());
dto.setEmail(organisation.getEmail());
dto.setTelephone(organisation.getTelephone());
dto.setAdresse(organisation.getAdresse());
dto.setVille(organisation.getVille());
dto.setCodePostal(organisation.getCodePostal());
dto.setRegion(organisation.getRegion());
dto.setPays(organisation.getPays());
dto.setSiteWeb(organisation.getSiteWeb());
dto.setObjectifs(organisation.getObjectifs());
dto.setActivitesPrincipales(organisation.getActivitesPrincipales());
dto.setNombreMembres(organisation.getNombreMembres());
dto.setDateCreation(organisation.getDateCreation());
dto.setDateModification(organisation.getDateModification());
dto.setActif(organisation.getActif());
dto.setVersion(organisation.getVersion());
return dto;
}
/**
* Convertit un DTO en entité Organisation
*
* @param dto le DTO à convertir
* @return l'entité correspondante
*/
public Organisation convertFromDTO(OrganisationDTO dto) {
if (dto == null) {
return null;
}
return Organisation.builder()
.nom(dto.getNom())
.nomCourt(dto.getNomCourt())
.description(dto.getDescription())
.email(dto.getEmail())
.telephone(dto.getTelephone())
.adresse(dto.getAdresse())
.ville(dto.getVille())
.codePostal(dto.getCodePostal())
.region(dto.getRegion())
.pays(dto.getPays())
.siteWeb(dto.getSiteWeb())
.objectifs(dto.getObjectifs())
.activitesPrincipales(dto.getActivitesPrincipales())
.build();
}
}

View File

@@ -1,24 +1,120 @@
# Configuration de base pour UnionFlow Server
# Configuration UnionFlow Server
quarkus.application.name=unionflow-server
quarkus.application.version=1.0.0
# Configuration HTTP
quarkus.http.port=8080
quarkus.http.host=0.0.0.0
# Configuration de développement
%dev.quarkus.log.level=INFO
%dev.quarkus.log.console.enable=true
# Configuration CORS
quarkus.http.cors=true
quarkus.http.cors.origins=*
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
quarkus.http.cors.headers=Content-Type,Authorization
# Configuration de base de données (PostgreSQL)
# Configuration Base de données PostgreSQL
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=unionflow
quarkus.datasource.password=unionflow
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/unionflow_dev
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=false
quarkus.datasource.username=${DB_USERNAME:unionflow}
quarkus.datasource.password=${DB_PASSWORD:unionflow123}
quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/unionflow}
quarkus.datasource.jdbc.min-size=2
quarkus.datasource.jdbc.max-size=10
# Configuration pour le développement sans base de données externe
%dev.quarkus.datasource.db-kind=h2
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:unionflow;DB_CLOSE_DELAY=-1
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
# Configuration Hibernate
quarkus.hibernate-orm.database.generation=update
quarkus.hibernate-orm.log.sql=false
quarkus.hibernate-orm.jdbc.timezone=UTC
quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity
# Configuration Flyway pour migrations
quarkus.flyway.migrate-at-start=true
quarkus.flyway.baseline-on-migrate=true
quarkus.flyway.baseline-version=1.0.0
# Configuration Keycloak OIDC
quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow
quarkus.oidc.client-id=unionflow-server
quarkus.oidc.credentials.secret=unionflow-secret-2025
quarkus.oidc.tls.verification=none
quarkus.oidc.application-type=service
# Configuration Keycloak Policy Enforcer (temporairement désactivé)
quarkus.keycloak.policy-enforcer.enable=false
quarkus.keycloak.policy-enforcer.lazy-load-paths=true
quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE
# Chemins publics (non protégés)
quarkus.http.auth.permission.public.paths=/health,/q/*,/favicon.ico,/auth/callback,/auth/*
quarkus.http.auth.permission.public.policy=permit
# Configuration OpenAPI
quarkus.smallrye-openapi.info-title=UnionFlow Server API
quarkus.smallrye-openapi.info-version=1.0.0
quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union avec authentification Keycloak
quarkus.smallrye-openapi.servers=http://localhost:8080
# Configuration Swagger UI
quarkus.swagger-ui.always-include=true
quarkus.swagger-ui.path=/swagger-ui
# Configuration santé
quarkus.smallrye-health.root-path=/health
# Configuration logging
quarkus.log.console.enable=true
quarkus.log.console.level=INFO
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n
quarkus.log.category."dev.lions.unionflow".level=INFO
quarkus.log.category."org.hibernate".level=WARN
quarkus.log.category."io.quarkus".level=INFO
# ========================================
# PROFILS DE CONFIGURATION
# ========================================
# Profil de développement
%dev.quarkus.datasource.db-kind=h2
%dev.quarkus.datasource.username=sa
%dev.quarkus.datasource.password=
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:unionflow_dev;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.flyway.migrate-at-start=false
%dev.quarkus.log.category."dev.lions.unionflow".level=DEBUG
%dev.quarkus.log.category."org.hibernate.SQL".level=DEBUG
# Configuration Keycloak pour développement
%dev.quarkus.oidc.tenant-enabled=true
%dev.quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow
%dev.quarkus.oidc.client-id=unionflow-server
%dev.quarkus.oidc.credentials.secret=unionflow-secret-2025
%dev.quarkus.oidc.tls.verification=none
%dev.quarkus.oidc.application-type=service
%dev.quarkus.keycloak.policy-enforcer.enable=false
%dev.quarkus.keycloak.policy-enforcer.lazy-load-paths=true
%dev.quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE
# Profil de test
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.username=sa
%test.quarkus.datasource.password=
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
%test.quarkus.hibernate-orm.database.generation=drop-and-create
%test.quarkus.flyway.migrate-at-start=false
# Configuration Keycloak pour tests (désactivé)
%test.quarkus.oidc.tenant-enabled=false
%test.quarkus.keycloak.policy-enforcer.enable=false
# Profil de production
%prod.quarkus.hibernate-orm.database.generation=validate
%prod.quarkus.hibernate-orm.log.sql=false
%prod.quarkus.log.console.level=WARN
%prod.quarkus.log.category."dev.lions.unionflow".level=INFO
%prod.quarkus.log.category.root.level=WARN
# Configuration Keycloak pour production
%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://192.168.1.11:8180/realms/unionflow}
%prod.quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:unionflow-server}
%prod.quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
%prod.quarkus.oidc.tls.verification=required

View File

@@ -1,137 +0,0 @@
# Configuration UnionFlow Server
quarkus:
application:
name: unionflow-server
version: 1.0.0
# Configuration HTTP
http:
port: 8080
host: 0.0.0.0
cors:
~: true
origins: "*"
methods: "GET,POST,PUT,DELETE,OPTIONS"
headers: "Content-Type,Authorization"
# Configuration Base de données PostgreSQL
datasource:
db-kind: postgresql
username: ${DB_USERNAME:unionflow}
password: ${DB_PASSWORD:unionflow123}
jdbc:
url: ${DB_URL:jdbc:postgresql://localhost:5432/unionflow}
min-size: 2
max-size: 10
# Configuration Hibernate
hibernate-orm:
database:
generation: update
log:
sql: false
jdbc:
timezone: UTC
packages: dev.lions.unionflow.server.entity
# Configuration Flyway pour migrations
flyway:
migrate-at-start: true
baseline-on-migrate: true
baseline-version: 1.0.0
# Configuration Sécurité JWT
smallrye-jwt:
enabled: true
# Configuration OpenAPI
smallrye-openapi:
info-title: UnionFlow Server API
info-version: 1.0.0
info-description: API REST pour la gestion d'union
servers: http://localhost:8080
# Configuration santé
smallrye-health:
root-path: /health
# Configuration JWT
mp:
jwt:
verify:
issuer: unionflow-api
publickey:
algorithm: RS256
# Configuration logging
quarkus:
log:
console:
enable: true
level: INFO
format: "%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n"
category:
"dev.lions.unionflow":
level: INFO
"org.hibernate":
level: WARN
"io.quarkus":
level: INFO
---
# Profil de développement
"%dev":
quarkus:
datasource:
db-kind: h2
username: sa
password: ""
jdbc:
url: jdbc:h2:mem:unionflow_dev;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
hibernate-orm:
database:
generation: drop-and-create
log:
sql: true
flyway:
migrate-at-start: false
log:
category:
"dev.lions.unionflow":
level: DEBUG
"org.hibernate.SQL":
level: DEBUG
---
# Profil de test
"%test":
quarkus:
datasource:
db-kind: h2
username: sa
password: ""
jdbc:
url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
hibernate-orm:
database:
generation: drop-and-create
flyway:
migrate-at-start: false
---
# Profil de production
"%prod":
quarkus:
hibernate-orm:
database:
generation: validate
log:
sql: false
log:
console:
level: WARN
category:
"dev.lions.unionflow":
level: INFO
root:
level: WARN

View File

@@ -0,0 +1,224 @@
-- Migration V1.2: Création de la table organisations
-- Auteur: UnionFlow Team
-- Date: 2025-01-15
-- Description: Création de la table organisations avec toutes les colonnes nécessaires
-- Création de la table organisations
CREATE TABLE organisations (
id BIGSERIAL PRIMARY KEY,
-- Informations de base
nom VARCHAR(200) NOT NULL,
nom_court VARCHAR(50),
type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION',
statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
description TEXT,
date_fondation DATE,
numero_enregistrement VARCHAR(100) UNIQUE,
-- Informations de contact
email VARCHAR(255) NOT NULL UNIQUE,
telephone VARCHAR(20),
telephone_secondaire VARCHAR(20),
email_secondaire VARCHAR(255),
-- Adresse
adresse VARCHAR(500),
ville VARCHAR(100),
code_postal VARCHAR(20),
region VARCHAR(100),
pays VARCHAR(100),
-- Coordonnées géographiques
latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90),
longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180),
-- Web et réseaux sociaux
site_web VARCHAR(500),
logo VARCHAR(500),
reseaux_sociaux VARCHAR(1000),
-- Hiérarchie
organisation_parente_id UUID,
niveau_hierarchique INTEGER NOT NULL DEFAULT 0,
-- Statistiques
nombre_membres INTEGER NOT NULL DEFAULT 0,
nombre_administrateurs INTEGER NOT NULL DEFAULT 0,
-- Finances
budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0),
devise VARCHAR(3) DEFAULT 'XOF',
cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE,
montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0),
-- Informations complémentaires
objectifs TEXT,
activites_principales TEXT,
certifications VARCHAR(500),
partenaires VARCHAR(1000),
notes VARCHAR(1000),
-- Paramètres
organisation_publique BOOLEAN NOT NULL DEFAULT TRUE,
accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE,
-- Métadonnées
actif BOOLEAN NOT NULL DEFAULT TRUE,
date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
date_modification TIMESTAMP,
cree_par VARCHAR(100),
modifie_par VARCHAR(100),
version BIGINT NOT NULL DEFAULT 0,
-- Contraintes
CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')),
CONSTRAINT chk_organisation_type CHECK (type_organisation IN (
'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE',
'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE'
)),
CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')),
CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10),
CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0),
CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0)
);
-- Création des index pour optimiser les performances
CREATE INDEX idx_organisation_nom ON organisations(nom);
CREATE INDEX idx_organisation_email ON organisations(email);
CREATE INDEX idx_organisation_statut ON organisations(statut);
CREATE INDEX idx_organisation_type ON organisations(type_organisation);
CREATE INDEX idx_organisation_ville ON organisations(ville);
CREATE INDEX idx_organisation_pays ON organisations(pays);
CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id);
CREATE INDEX idx_organisation_numero_enregistrement ON organisations(numero_enregistrement);
CREATE INDEX idx_organisation_actif ON organisations(actif);
CREATE INDEX idx_organisation_date_creation ON organisations(date_creation);
CREATE INDEX idx_organisation_publique ON organisations(organisation_publique);
CREATE INDEX idx_organisation_accepte_membres ON organisations(accepte_nouveaux_membres);
-- Index composites pour les recherches fréquentes
CREATE INDEX idx_organisation_statut_actif ON organisations(statut, actif);
CREATE INDEX idx_organisation_type_ville ON organisations(type_organisation, ville);
CREATE INDEX idx_organisation_pays_region ON organisations(pays, region);
CREATE INDEX idx_organisation_publique_actif ON organisations(organisation_publique, actif);
-- Index pour les recherches textuelles
CREATE INDEX idx_organisation_nom_lower ON organisations(LOWER(nom));
CREATE INDEX idx_organisation_nom_court_lower ON organisations(LOWER(nom_court));
CREATE INDEX idx_organisation_ville_lower ON organisations(LOWER(ville));
-- Ajout de la colonne organisation_id à la table membres (si elle n'existe pas déjà)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'membres' AND column_name = 'organisation_id'
) THEN
ALTER TABLE membres ADD COLUMN organisation_id BIGINT;
ALTER TABLE membres ADD CONSTRAINT fk_membre_organisation
FOREIGN KEY (organisation_id) REFERENCES organisations(id);
CREATE INDEX idx_membre_organisation ON membres(organisation_id);
END IF;
END $$;
-- Insertion de données de test pour le développement
INSERT INTO organisations (
nom, nom_court, type_organisation, statut, description,
email, telephone, adresse, ville, region, pays,
objectifs, activites_principales, nombre_membres,
organisation_publique, accepte_nouveaux_membres,
cree_par
) VALUES
(
'Lions Club Abidjan Plateau',
'LC Plateau',
'LIONS_CLUB',
'ACTIVE',
'Lions Club du district 403 A1, zone Plateau d''Abidjan',
'plateau@lionsclub-ci.org',
'+225 27 20 21 22 23',
'Immeuble SCIAM, Boulevard de la République',
'Abidjan',
'Lagunes',
'Côte d''Ivoire',
'Servir la communauté par des actions humanitaires et sociales',
'Actions de santé, éducation, environnement et aide aux démunis',
45,
true,
true,
'system'
),
(
'Lions Club Abidjan Cocody',
'LC Cocody',
'LIONS_CLUB',
'ACTIVE',
'Lions Club du district 403 A1, zone Cocody',
'cocody@lionsclub-ci.org',
'+225 27 22 44 55 66',
'Riviera Golf, Cocody',
'Abidjan',
'Lagunes',
'Côte d''Ivoire',
'Servir la communauté par des actions humanitaires et sociales',
'Actions de santé, éducation, environnement et aide aux démunis',
38,
true,
true,
'system'
),
(
'Association des Femmes Entrepreneures CI',
'AFECI',
'ASSOCIATION',
'ACTIVE',
'Association pour la promotion de l''entrepreneuriat féminin en Côte d''Ivoire',
'contact@afeci.org',
'+225 05 06 07 08 09',
'Marcory Zone 4C',
'Abidjan',
'Lagunes',
'Côte d''Ivoire',
'Promouvoir l''entrepreneuriat féminin et l''autonomisation des femmes',
'Formation, accompagnement, financement de projets féminins',
120,
true,
true,
'system'
),
(
'Coopérative Agricole du Nord',
'COOP-NORD',
'COOPERATIVE',
'ACTIVE',
'Coopérative des producteurs agricoles du Nord de la Côte d''Ivoire',
'info@coop-nord.ci',
'+225 09 10 11 12 13',
'Korhogo Centre',
'Korhogo',
'Savanes',
'Côte d''Ivoire',
'Améliorer les conditions de vie des producteurs agricoles',
'Production, transformation et commercialisation de produits agricoles',
250,
true,
true,
'system'
);
-- Mise à jour des statistiques de la base de données
ANALYZE organisations;
-- Commentaires sur la table et les colonnes principales
COMMENT ON TABLE organisations IS 'Table des organisations (Lions Clubs, Associations, Coopératives, etc.)';
COMMENT ON COLUMN organisations.nom IS 'Nom officiel de l''organisation';
COMMENT ON COLUMN organisations.nom_court IS 'Nom court ou sigle de l''organisation';
COMMENT ON COLUMN organisations.type_organisation IS 'Type d''organisation (LIONS_CLUB, ASSOCIATION, etc.)';
COMMENT ON COLUMN organisations.statut IS 'Statut actuel de l''organisation (ACTIVE, SUSPENDUE, etc.)';
COMMENT ON COLUMN organisations.organisation_parente_id IS 'ID de l''organisation parente pour la hiérarchie';
COMMENT ON COLUMN organisations.niveau_hierarchique IS 'Niveau dans la hiérarchie (0 = racine)';
COMMENT ON COLUMN organisations.nombre_membres IS 'Nombre total de membres actifs';
COMMENT ON COLUMN organisations.organisation_publique IS 'Si l''organisation est visible publiquement';
COMMENT ON COLUMN organisations.accepte_nouveaux_membres IS 'Si l''organisation accepte de nouveaux membres';
COMMENT ON COLUMN organisations.version IS 'Version pour le contrôle de concurrence optimiste';

View File

@@ -0,0 +1,307 @@
{
"realm": "unionflow",
"displayName": "UnionFlow",
"displayNameHtml": "<div class=\"kc-logo-text\"><span>UnionFlow</span></div>",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": true,
"registrationEmailAsUsername": true,
"rememberMe": true,
"verifyEmail": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,
"maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 60,
"quickLoginCheckMilliSeconds": 1000,
"maxDeltaTimeSeconds": 43200,
"failureFactor": 30,
"defaultRoles": ["offline_access", "uma_authorization", "default-roles-unionflow"],
"requiredCredentials": ["password"],
"otpPolicyType": "totp",
"otpPolicyAlgorithm": "HmacSHA1",
"otpPolicyInitialCounter": 0,
"otpPolicyDigits": 6,
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"supportedLocales": ["fr", "en"],
"defaultLocale": "fr",
"internationalizationEnabled": true,
"clients": [
{
"clientId": "unionflow-server",
"name": "UnionFlow Server API",
"description": "Client pour l'API serveur UnionFlow",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "dev-secret",
"redirectUris": ["http://localhost:8080/*"],
"webOrigins": ["http://localhost:8080", "http://localhost:3000"],
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"protocolMappers": [
{
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "email",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "email",
"jsonType.label": "String"
}
},
{
"name": "given_name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "firstName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "given_name",
"jsonType.label": "String"
}
},
{
"name": "family_name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "lastName",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "family_name",
"jsonType.label": "String"
}
},
{
"name": "roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
],
"defaultClientScopes": ["web-origins", "role_list", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
},
{
"clientId": "unionflow-mobile",
"name": "UnionFlow Mobile App",
"description": "Client pour l'application mobile UnionFlow",
"enabled": true,
"publicClient": true,
"redirectUris": ["unionflow://callback", "http://localhost:3000/callback"],
"webOrigins": ["*"],
"protocol": "openid-connect",
"attributes": {
"pkce.code.challenge.method": "S256"
},
"fullScopeAllowed": true,
"defaultClientScopes": ["web-origins", "role_list", "profile", "roles", "email"],
"optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"]
}
],
"roles": {
"realm": [
{
"name": "ADMIN",
"description": "Administrateur système avec tous les droits",
"composite": false,
"clientRole": false,
"containerId": "unionflow"
},
{
"name": "PRESIDENT",
"description": "Président de l'union avec droits de gestion complète",
"composite": false,
"clientRole": false,
"containerId": "unionflow"
},
{
"name": "SECRETAIRE",
"description": "Secrétaire avec droits de gestion des membres et événements",
"composite": false,
"clientRole": false,
"containerId": "unionflow"
},
{
"name": "TRESORIER",
"description": "Trésorier avec droits de gestion financière",
"composite": false,
"clientRole": false,
"containerId": "unionflow"
},
{
"name": "GESTIONNAIRE_MEMBRE",
"description": "Gestionnaire des membres avec droits de CRUD sur les membres",
"composite": false,
"clientRole": false,
"containerId": "unionflow"
},
{
"name": "ORGANISATEUR_EVENEMENT",
"description": "Organisateur d'événements avec droits de gestion des événements",
"composite": false,
"clientRole": false,
"containerId": "unionflow"
},
{
"name": "MEMBRE",
"description": "Membre standard avec droits de consultation",
"composite": false,
"clientRole": false,
"containerId": "unionflow"
}
]
},
"users": [
{
"username": "admin",
"enabled": true,
"emailVerified": true,
"firstName": "Administrateur",
"lastName": "Système",
"email": "admin@unionflow.dev",
"credentials": [
{
"type": "password",
"value": "admin123",
"temporary": false
}
],
"realmRoles": ["ADMIN", "PRESIDENT"],
"clientRoles": {}
},
{
"username": "president",
"enabled": true,
"emailVerified": true,
"firstName": "Jean",
"lastName": "Dupont",
"email": "president@unionflow.dev",
"credentials": [
{
"type": "password",
"value": "president123",
"temporary": false
}
],
"realmRoles": ["PRESIDENT", "MEMBRE"],
"clientRoles": {}
},
{
"username": "secretaire",
"enabled": true,
"emailVerified": true,
"firstName": "Marie",
"lastName": "Martin",
"email": "secretaire@unionflow.dev",
"credentials": [
{
"type": "password",
"value": "secretaire123",
"temporary": false
}
],
"realmRoles": ["SECRETAIRE", "GESTIONNAIRE_MEMBRE", "MEMBRE"],
"clientRoles": {}
},
{
"username": "tresorier",
"enabled": true,
"emailVerified": true,
"firstName": "Pierre",
"lastName": "Durand",
"email": "tresorier@unionflow.dev",
"credentials": [
{
"type": "password",
"value": "tresorier123",
"temporary": false
}
],
"realmRoles": ["TRESORIER", "MEMBRE"],
"clientRoles": {}
},
{
"username": "membre1",
"enabled": true,
"emailVerified": true,
"firstName": "Sophie",
"lastName": "Bernard",
"email": "membre1@unionflow.dev",
"credentials": [
{
"type": "password",
"value": "membre123",
"temporary": false
}
],
"realmRoles": ["MEMBRE"],
"clientRoles": {}
}
],
"groups": [
{
"name": "Administration",
"path": "/Administration",
"realmRoles": ["ADMIN"],
"subGroups": []
},
{
"name": "Bureau",
"path": "/Bureau",
"realmRoles": ["PRESIDENT", "SECRETAIRE", "TRESORIER"],
"subGroups": []
},
{
"name": "Gestionnaires",
"path": "/Gestionnaires",
"realmRoles": ["GESTIONNAIRE_MEMBRE", "ORGANISATEUR_EVENEMENT"],
"subGroups": []
},
{
"name": "Membres",
"path": "/Membres",
"realmRoles": ["MEMBRE"],
"subGroups": []
}
]
}

View File

@@ -0,0 +1,413 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
/**
* Tests d'intégration pour EvenementResource
*
* Tests complets de l'API REST des événements avec authentification
* et validation des permissions. Optimisé pour l'intégration mobile.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("Tests d'intégration - API Événements")
class EvenementResourceTest {
private static Long evenementTestId;
private static Long organisationTestId;
private static Long membreTestId;
@BeforeAll
@Transactional
static void setupTestData() {
// Créer une organisation de test
Organisation organisation = Organisation.builder()
.nom("Union Test API")
.typeOrganisation("ASSOCIATION")
.statut("ACTIVE")
.email("test-api@union.com")
.telephone("0123456789")
.adresse("123 Rue de Test")
.codePostal("75001")
.ville("Paris")
.pays("France")
.actif(true)
.creePar("test@unionflow.dev")
.dateCreation(LocalDateTime.now())
.build();
organisation.persist();
organisationTestId = organisation.id;
// Créer un membre de test
Membre membre = Membre.builder()
.numeroMembre("UF2025-API01")
.prenom("Marie")
.nom("Martin")
.email("marie.martin@test.com")
.telephone("0987654321")
.dateNaissance(LocalDate.of(1990, 5, 15))
.dateAdhesion(LocalDate.now())
.actif(true)
.organisation(organisation)
.build();
membre.persist();
membreTestId = membre.id;
// Créer un événement de test
Evenement evenement = Evenement.builder()
.titre("Conférence API Test")
.description("Conférence de test pour l'API")
.dateDebut(LocalDateTime.now().plusDays(15))
.dateFin(LocalDateTime.now().plusDays(15).plusHours(2))
.lieu("Centre de conférence Test")
.typeEvenement(TypeEvenement.CONFERENCE)
.statut(StatutEvenement.PLANIFIE)
.capaciteMax(50)
.prix(BigDecimal.valueOf(15.00))
.inscriptionRequise(true)
.visiblePublic(true)
.actif(true)
.organisation(organisation)
.organisateur(membre)
.creePar("test@unionflow.dev")
.dateCreation(LocalDateTime.now())
.build();
evenement.persist();
evenementTestId = evenement.id;
}
@Test
@Order(1)
@DisplayName("GET /api/evenements - Lister événements (authentifié)")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testListerEvenements_Authentifie() {
given()
.when()
.get("/api/evenements")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(1))
.body("[0].titre", notNullValue())
.body("[0].dateDebut", notNullValue())
.body("[0].statut", notNullValue());
}
@Test
@Order(2)
@DisplayName("GET /api/evenements - Non authentifié")
void testListerEvenements_NonAuthentifie() {
given()
.when()
.get("/api/evenements")
.then()
.statusCode(401);
}
@Test
@Order(3)
@DisplayName("GET /api/evenements/{id} - Récupérer événement")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testObtenirEvenement() {
given()
.pathParam("id", evenementTestId)
.when()
.get("/api/evenements/{id}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("id", equalTo(evenementTestId.intValue()))
.body("titre", equalTo("Conférence API Test"))
.body("description", equalTo("Conférence de test pour l'API"))
.body("typeEvenement", equalTo("CONFERENCE"))
.body("statut", equalTo("PLANIFIE"))
.body("capaciteMax", equalTo(50))
.body("prix", equalTo(15.0f))
.body("inscriptionRequise", equalTo(true))
.body("visiblePublic", equalTo(true))
.body("actif", equalTo(true));
}
@Test
@Order(4)
@DisplayName("GET /api/evenements/{id} - Événement non trouvé")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testObtenirEvenement_NonTrouve() {
given()
.pathParam("id", 99999)
.when()
.get("/api/evenements/{id}")
.then()
.statusCode(404)
.body("error", equalTo("Événement non trouvé"));
}
@Test
@Order(5)
@DisplayName("POST /api/evenements - Créer événement (organisateur)")
@TestSecurity(user = "marie.martin@test.com", roles = {"ORGANISATEUR_EVENEMENT"})
void testCreerEvenement_Organisateur() {
String nouvelEvenement = String.format("""
{
"titre": "Nouvel Événement Test",
"description": "Description du nouvel événement",
"dateDebut": "%s",
"dateFin": "%s",
"lieu": "Lieu de test",
"typeEvenement": "FORMATION",
"capaciteMax": 30,
"prix": 20.00,
"inscriptionRequise": true,
"visiblePublic": true,
"organisation": {"id": %d},
"organisateur": {"id": %d}
}
""",
LocalDateTime.now().plusDays(20).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
LocalDateTime.now().plusDays(20).plusHours(3).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
organisationTestId,
membreTestId
);
given()
.contentType(ContentType.JSON)
.body(nouvelEvenement)
.when()
.post("/api/evenements")
.then()
.statusCode(201)
.contentType(ContentType.JSON)
.body("titre", equalTo("Nouvel Événement Test"))
.body("typeEvenement", equalTo("FORMATION"))
.body("capaciteMax", equalTo(30))
.body("prix", equalTo(20.0f))
.body("actif", equalTo(true));
}
@Test
@Order(6)
@DisplayName("POST /api/evenements - Permissions insuffisantes")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testCreerEvenement_PermissionsInsuffisantes() {
String nouvelEvenement = """
{
"titre": "Événement Non Autorisé",
"description": "Test permissions",
"dateDebut": "2025-02-15T10:00:00",
"dateFin": "2025-02-15T12:00:00",
"lieu": "Lieu test",
"typeEvenement": "FORMATION"
}
""";
given()
.contentType(ContentType.JSON)
.body(nouvelEvenement)
.when()
.post("/api/evenements")
.then()
.statusCode(403);
}
@Test
@Order(7)
@DisplayName("PUT /api/evenements/{id} - Mettre à jour événement")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testMettreAJourEvenement_Admin() {
String evenementModifie = String.format("""
{
"titre": "Conférence API Test - Modifiée",
"description": "Description mise à jour",
"dateDebut": "%s",
"dateFin": "%s",
"lieu": "Nouveau lieu",
"typeEvenement": "CONFERENCE",
"capaciteMax": 75,
"prix": 25.00,
"inscriptionRequise": true,
"visiblePublic": true
}
""",
LocalDateTime.now().plusDays(16).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
LocalDateTime.now().plusDays(16).plusHours(3).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
);
given()
.pathParam("id", evenementTestId)
.contentType(ContentType.JSON)
.body(evenementModifie)
.when()
.put("/api/evenements/{id}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("titre", equalTo("Conférence API Test - Modifiée"))
.body("description", equalTo("Description mise à jour"))
.body("lieu", equalTo("Nouveau lieu"))
.body("capaciteMax", equalTo(75))
.body("prix", equalTo(25.0f));
}
@Test
@Order(8)
@DisplayName("GET /api/evenements/a-venir - Événements à venir")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testEvenementsAVenir() {
given()
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/evenements/a-venir")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(9)
@DisplayName("GET /api/evenements/publics - Événements publics (non authentifié)")
void testEvenementsPublics_NonAuthentifie() {
given()
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/publics")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(10)
@DisplayName("GET /api/evenements/recherche - Recherche d'événements")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testRechercherEvenements() {
given()
.queryParam("q", "Conférence")
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/recherche")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(11)
@DisplayName("GET /api/evenements/recherche - Terme de recherche manquant")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testRechercherEvenements_TermeManquant() {
given()
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/recherche")
.then()
.statusCode(400)
.body("error", equalTo("Le terme de recherche est obligatoire"));
}
@Test
@Order(12)
@DisplayName("GET /api/evenements/type/{type} - Événements par type")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testEvenementsParType() {
given()
.pathParam("type", "CONFERENCE")
.queryParam("page", 0)
.queryParam("size", 20)
.when()
.get("/api/evenements/type/{type}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@Order(13)
@DisplayName("PATCH /api/evenements/{id}/statut - Changer statut")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testChangerStatut() {
given()
.pathParam("id", evenementTestId)
.queryParam("statut", "CONFIRME")
.when()
.patch("/api/evenements/{id}/statut")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("statut", equalTo("CONFIRME"));
}
@Test
@Order(14)
@DisplayName("GET /api/evenements/statistiques - Statistiques")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testObtenirStatistiques() {
given()
.when()
.get("/api/evenements/statistiques")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("total", notNullValue())
.body("actifs", notNullValue())
.body("timestamp", notNullValue());
}
@Test
@Order(15)
@DisplayName("DELETE /api/evenements/{id} - Supprimer événement")
@TestSecurity(user = "admin@unionflow.dev", roles = {"ADMIN"})
void testSupprimerEvenement() {
given()
.pathParam("id", evenementTestId)
.when()
.delete("/api/evenements/{id}")
.then()
.statusCode(204);
}
@Test
@Order(16)
@DisplayName("Pagination - Paramètres valides")
@TestSecurity(user = "marie.martin@test.com", roles = {"MEMBRE"})
void testPagination() {
given()
.queryParam("page", 0)
.queryParam("size", 5)
.queryParam("sort", "titre")
.queryParam("direction", "asc")
.when()
.get("/api/evenements")
.then()
.statusCode(200)
.contentType(ContentType.JSON);
}
}

View File

@@ -0,0 +1,335 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.UUID;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
/**
* Tests d'intégration pour OrganisationResource
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
class OrganisationResourceTest {
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testCreerOrganisation_Success() {
OrganisationDTO organisation = createTestOrganisationDTO();
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(201)
.body("nom", equalTo("Lions Club Test API"))
.body("email", equalTo("testapi@lionsclub.org"))
.body("actif", equalTo(true));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testCreerOrganisation_EmailInvalide() {
OrganisationDTO organisation = createTestOrganisationDTO();
organisation.setEmail("email-invalide");
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(400);
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testCreerOrganisation_NomVide() {
OrganisationDTO organisation = createTestOrganisationDTO();
organisation.setNom("");
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(400);
}
@Test
void testCreerOrganisation_NonAuthentifie() {
OrganisationDTO organisation = createTestOrganisationDTO();
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(401);
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testListerOrganisations_Success() {
given()
.when()
.get("/api/organisations")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testListerOrganisations_AvecPagination() {
given()
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/organisations")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testListerOrganisations_AvecRecherche() {
given()
.queryParam("recherche", "Lions")
.when()
.get("/api/organisations")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
void testListerOrganisations_NonAuthentifie() {
given()
.when()
.get("/api/organisations")
.then()
.statusCode(401);
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testObtenirOrganisation_NonTrouvee() {
given()
.when()
.get("/api/organisations/99999")
.then()
.statusCode(404)
.body("error", equalTo("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testMettreAJourOrganisation_NonTrouvee() {
OrganisationDTO organisation = createTestOrganisationDTO();
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.put("/api/organisations/99999")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testSupprimerOrganisation_NonTrouvee() {
given()
.when()
.delete("/api/organisations/99999")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testRechercheAvancee_Success() {
given()
.queryParam("nom", "Lions")
.queryParam("ville", "Abidjan")
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/organisations/recherche")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testRechercheAvancee_SansCriteres() {
given()
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/organisations/recherche")
.then()
.statusCode(200)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testActiverOrganisation_NonTrouvee() {
given()
.when()
.post("/api/organisations/99999/activer")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testSuspendreOrganisation_NonTrouvee() {
given()
.when()
.post("/api/organisations/99999/suspendre")
.then()
.statusCode(404)
.body("error", containsString("Organisation non trouvée"));
}
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testObtenirStatistiques_Success() {
given()
.when()
.get("/api/organisations/statistiques")
.then()
.statusCode(200)
.body("totalOrganisations", notNullValue())
.body("organisationsActives", notNullValue())
.body("organisationsInactives", notNullValue())
.body("nouvellesOrganisations30Jours", notNullValue())
.body("tauxActivite", notNullValue())
.body("timestamp", notNullValue());
}
@Test
void testObtenirStatistiques_NonAuthentifie() {
given()
.when()
.get("/api/organisations/statistiques")
.then()
.statusCode(401);
}
/**
* Test de workflow complet : création, lecture, mise à jour, suppression
*/
@Test
@TestSecurity(user = "testUser", roles = {"ADMIN"})
void testWorkflowComplet() {
// 1. Créer une organisation
OrganisationDTO organisation = createTestOrganisationDTO();
organisation.setNom("Lions Club Workflow Test");
organisation.setEmail("workflow@lionsclub.org");
String location = given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.post("/api/organisations")
.then()
.statusCode(201)
.extract()
.header("Location");
// Extraire l'ID de l'organisation créée
String organisationId = location.substring(location.lastIndexOf("/") + 1);
// 2. Lire l'organisation créée
given()
.when()
.get("/api/organisations/" + organisationId)
.then()
.statusCode(200)
.body("nom", equalTo("Lions Club Workflow Test"))
.body("email", equalTo("workflow@lionsclub.org"));
// 3. Mettre à jour l'organisation
organisation.setDescription("Description mise à jour");
given()
.contentType(ContentType.JSON)
.body(organisation)
.when()
.put("/api/organisations/" + organisationId)
.then()
.statusCode(200)
.body("description", equalTo("Description mise à jour"));
// 4. Suspendre l'organisation
given()
.when()
.post("/api/organisations/" + organisationId + "/suspendre")
.then()
.statusCode(200);
// 5. Activer l'organisation
given()
.when()
.post("/api/organisations/" + organisationId + "/activer")
.then()
.statusCode(200);
// 6. Supprimer l'organisation (soft delete)
given()
.when()
.delete("/api/organisations/" + organisationId)
.then()
.statusCode(204);
}
/**
* Crée un DTO d'organisation pour les tests
*/
private OrganisationDTO createTestOrganisationDTO() {
OrganisationDTO dto = new OrganisationDTO();
dto.setId(UUID.randomUUID());
dto.setNom("Lions Club Test API");
dto.setNomCourt("LC Test API");
dto.setEmail("testapi@lionsclub.org");
dto.setDescription("Organisation de test pour l'API");
dto.setTelephone("+225 01 02 03 04 05");
dto.setAdresse("123 Rue de Test API");
dto.setVille("Abidjan");
dto.setCodePostal("00225");
dto.setRegion("Lagunes");
dto.setPays("Côte d'Ivoire");
dto.setSiteWeb("https://testapi.lionsclub.org");
dto.setObjectifs("Servir la communauté");
dto.setActivitesPrincipales("Actions sociales et humanitaires");
dto.setNombreMembres(0);
dto.setDateCreation(LocalDateTime.now());
dto.setActif(true);
dto.setVersion(0L);
return dto;
}
}

View File

@@ -0,0 +1,408 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Evenement.StatutEvenement;
import dev.lions.unionflow.server.entity.Evenement.TypeEvenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import jakarta.inject.Inject;
import org.junit.jupiter.api.*;
import org.mockito.Mockito;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests unitaires pour EvenementService
*
* Tests complets du service de gestion des événements avec
* validation des règles métier et intégration Keycloak.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("Tests unitaires - Service Événements")
class EvenementServiceTest {
@Inject
EvenementService evenementService;
@InjectMock
EvenementRepository evenementRepository;
@InjectMock
MembreRepository membreRepository;
@InjectMock
OrganisationRepository organisationRepository;
@InjectMock
KeycloakService keycloakService;
private Evenement evenementTest;
private Organisation organisationTest;
private Membre membreTest;
@BeforeEach
void setUp() {
// Données de test
organisationTest = Organisation.builder()
.nom("Union Test")
.typeOrganisation("ASSOCIATION")
.statut("ACTIVE")
.email("test@union.com")
.actif(true)
.build();
organisationTest.id = 1L;
membreTest = Membre.builder()
.numeroMembre("UF2025-TEST01")
.prenom("Jean")
.nom("Dupont")
.email("jean.dupont@test.com")
.actif(true)
.build();
membreTest.id = 1L;
evenementTest = Evenement.builder()
.titre("Assemblée Générale 2025")
.description("Assemblée générale annuelle de l'union")
.dateDebut(LocalDateTime.now().plusDays(30))
.dateFin(LocalDateTime.now().plusDays(30).plusHours(3))
.lieu("Salle de conférence")
.typeEvenement(TypeEvenement.ASSEMBLEE_GENERALE)
.statut(StatutEvenement.PLANIFIE)
.capaciteMax(100)
.prix(BigDecimal.valueOf(25.00))
.inscriptionRequise(true)
.visiblePublic(true)
.actif(true)
.organisation(organisationTest)
.organisateur(membreTest)
.build();
evenementTest.id = 1L;
}
@Test
@Order(1)
@DisplayName("Création d'événement - Succès")
void testCreerEvenement_Succes() {
// Given
when(keycloakService.getCurrentUserEmail()).thenReturn("jean.dupont@test.com");
when(evenementRepository.findByTitre(anyString())).thenReturn(Optional.empty());
doNothing().when(evenementRepository).persist(any(Evenement.class));
// When
Evenement resultat = evenementService.creerEvenement(evenementTest);
// Then
assertNotNull(resultat);
assertEquals("Assemblée Générale 2025", resultat.getTitre());
assertEquals(StatutEvenement.PLANIFIE, resultat.getStatut());
assertTrue(resultat.getActif());
assertEquals("jean.dupont@test.com", resultat.getCreePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(2)
@DisplayName("Création d'événement - Titre obligatoire")
void testCreerEvenement_TitreObligatoire() {
// Given
evenementTest.setTitre(null);
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("Le titre de l'événement est obligatoire", exception.getMessage());
verify(evenementRepository, never()).persist(any(Evenement.class));
}
@Test
@Order(3)
@DisplayName("Création d'événement - Date de début obligatoire")
void testCreerEvenement_DateDebutObligatoire() {
// Given
evenementTest.setDateDebut(null);
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La date de début est obligatoire", exception.getMessage());
}
@Test
@Order(4)
@DisplayName("Création d'événement - Date de début dans le passé")
void testCreerEvenement_DateDebutPassee() {
// Given
evenementTest.setDateDebut(LocalDateTime.now().minusDays(1));
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La date de début ne peut pas être dans le passé", exception.getMessage());
}
@Test
@Order(5)
@DisplayName("Création d'événement - Date de fin antérieure à date de début")
void testCreerEvenement_DateFinInvalide() {
// Given
evenementTest.setDateFin(evenementTest.getDateDebut().minusHours(1));
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La date de fin ne peut pas être antérieure à la date de début", exception.getMessage());
}
@Test
@Order(6)
@DisplayName("Mise à jour d'événement - Succès")
void testMettreAJourEvenement_Succes() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole("ADMIN")).thenReturn(true);
when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com");
doNothing().when(evenementRepository).persist(any(Evenement.class));
Evenement evenementMisAJour = Evenement.builder()
.titre("Assemblée Générale 2025 - Modifiée")
.description("Description mise à jour")
.dateDebut(LocalDateTime.now().plusDays(35))
.dateFin(LocalDateTime.now().plusDays(35).plusHours(4))
.lieu("Nouvelle salle")
.typeEvenement(TypeEvenement.ASSEMBLEE_GENERALE)
.capaciteMax(150)
.prix(BigDecimal.valueOf(30.00))
.inscriptionRequise(true)
.visiblePublic(true)
.build();
// When
Evenement resultat = evenementService.mettreAJourEvenement(1L, evenementMisAJour);
// Then
assertNotNull(resultat);
assertEquals("Assemblée Générale 2025 - Modifiée", resultat.getTitre());
assertEquals("Description mise à jour", resultat.getDescription());
assertEquals("Nouvelle salle", resultat.getLieu());
assertEquals(150, resultat.getCapaciteMax());
assertEquals(BigDecimal.valueOf(30.00), resultat.getPrix());
assertEquals("admin@test.com", resultat.getModifiePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(7)
@DisplayName("Mise à jour d'événement - Événement non trouvé")
void testMettreAJourEvenement_NonTrouve() {
// Given
when(evenementRepository.findByIdOptional(999L)).thenReturn(Optional.empty());
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.mettreAJourEvenement(999L, evenementTest)
);
assertEquals("Événement non trouvé avec l'ID: 999", exception.getMessage());
}
@Test
@Order(8)
@DisplayName("Suppression d'événement - Succès")
void testSupprimerEvenement_Succes() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole("ADMIN")).thenReturn(true);
when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com");
when(evenementTest.getNombreInscrits()).thenReturn(0);
doNothing().when(evenementRepository).persist(any(Evenement.class));
// When
assertDoesNotThrow(() -> evenementService.supprimerEvenement(1L));
// Then
assertFalse(evenementTest.getActif());
assertEquals("admin@test.com", evenementTest.getModifiePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(9)
@DisplayName("Recherche d'événements - Succès")
void testRechercherEvenements_Succes() {
// Given
List<Evenement> evenementsAttendus = List.of(evenementTest);
when(evenementRepository.findByTitreOrDescription(anyString(), any(Page.class), any(Sort.class)))
.thenReturn(evenementsAttendus);
// When
List<Evenement> resultat = evenementService.rechercherEvenements(
"Assemblée", Page.of(0, 10), Sort.by("dateDebut"));
// Then
assertNotNull(resultat);
assertEquals(1, resultat.size());
assertEquals("Assemblée Générale 2025", resultat.get(0).getTitre());
verify(evenementRepository).findByTitreOrDescription(eq("Assemblée"), any(Page.class), any(Sort.class));
}
@Test
@Order(10)
@DisplayName("Changement de statut - Succès")
void testChangerStatut_Succes() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole("ADMIN")).thenReturn(true);
when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com");
doNothing().when(evenementRepository).persist(any(Evenement.class));
// When
Evenement resultat = evenementService.changerStatut(1L, StatutEvenement.CONFIRME);
// Then
assertNotNull(resultat);
assertEquals(StatutEvenement.CONFIRME, resultat.getStatut());
assertEquals("admin@test.com", resultat.getModifiePar());
verify(evenementRepository).persist(any(Evenement.class));
}
@Test
@Order(11)
@DisplayName("Statistiques des événements")
void testObtenirStatistiques() {
// Given
Map<String, Long> statsBase = Map.of(
"total", 100L,
"actifs", 80L,
"aVenir", 30L,
"enCours", 5L,
"passes", 45L,
"publics", 70L,
"avecInscription", 25L
);
when(evenementRepository.getStatistiques()).thenReturn(statsBase);
// When
Map<String, Object> resultat = evenementService.obtenirStatistiques();
// Then
assertNotNull(resultat);
assertEquals(100L, resultat.get("total"));
assertEquals(80L, resultat.get("actifs"));
assertEquals(30L, resultat.get("aVenir"));
assertEquals(80.0, resultat.get("tauxActivite"));
assertEquals(37.5, resultat.get("tauxEvenementsAVenir"));
assertEquals(6.25, resultat.get("tauxEvenementsEnCours"));
assertNotNull(resultat.get("timestamp"));
verify(evenementRepository).getStatistiques();
}
@Test
@Order(12)
@DisplayName("Lister événements actifs avec pagination")
void testListerEvenementsActifs() {
// Given
List<Evenement> evenementsAttendus = List.of(evenementTest);
when(evenementRepository.findAllActifs(any(Page.class), any(Sort.class)))
.thenReturn(evenementsAttendus);
// When
List<Evenement> resultat = evenementService.listerEvenementsActifs(
Page.of(0, 20), Sort.by("dateDebut"));
// Then
assertNotNull(resultat);
assertEquals(1, resultat.size());
assertEquals("Assemblée Générale 2025", resultat.get(0).getTitre());
verify(evenementRepository).findAllActifs(any(Page.class), any(Sort.class));
}
@Test
@Order(13)
@DisplayName("Validation des règles métier - Prix négatif")
void testValidation_PrixNegatif() {
// Given
evenementTest.setPrix(BigDecimal.valueOf(-10.00));
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("Le prix ne peut pas être négatif", exception.getMessage());
}
@Test
@Order(14)
@DisplayName("Validation des règles métier - Capacité négative")
void testValidation_CapaciteNegative() {
// Given
evenementTest.setCapaciteMax(-5);
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> evenementService.creerEvenement(evenementTest)
);
assertEquals("La capacité maximale ne peut pas être négative", exception.getMessage());
}
@Test
@Order(15)
@DisplayName("Permissions - Utilisateur non autorisé")
void testPermissions_UtilisateurNonAutorise() {
// Given
when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest));
when(keycloakService.hasRole(anyString())).thenReturn(false);
when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com");
// When & Then
SecurityException exception = assertThrows(
SecurityException.class,
() -> evenementService.mettreAJourEvenement(1L, evenementTest)
);
assertEquals("Vous n'avez pas les permissions pour modifier cet événement", exception.getMessage());
}
}

View File

@@ -0,0 +1,346 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Tests unitaires pour OrganisationService
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-15
*/
@QuarkusTest
class OrganisationServiceTest {
@Inject
OrganisationService organisationService;
@InjectMock
OrganisationRepository organisationRepository;
private Organisation organisationTest;
@BeforeEach
void setUp() {
organisationTest = Organisation.builder()
.nom("Lions Club Test")
.nomCourt("LC Test")
.email("test@lionsclub.org")
.typeOrganisation("LIONS_CLUB")
.statut("ACTIVE")
.description("Organisation de test")
.telephone("+225 01 02 03 04 05")
.adresse("123 Rue de Test")
.ville("Abidjan")
.region("Lagunes")
.pays("Côte d'Ivoire")
.nombreMembres(25)
.actif(true)
.dateCreation(LocalDateTime.now())
.version(0L)
.build();
organisationTest.id = 1L;
}
@Test
void testCreerOrganisation_Success() {
// Given
Organisation organisationToCreate = Organisation.builder()
.nom("Lions Club Test New")
.email("testnew@lionsclub.org")
.typeOrganisation("LIONS_CLUB")
.build();
when(organisationRepository.findByEmail("testnew@lionsclub.org")).thenReturn(Optional.empty());
when(organisationRepository.findByNom("Lions Club Test New")).thenReturn(Optional.empty());
// When
Organisation result = organisationService.creerOrganisation(organisationToCreate);
// Then
assertNotNull(result);
assertEquals("Lions Club Test New", result.getNom());
assertEquals("ACTIVE", result.getStatut());
verify(organisationRepository).findByEmail("testnew@lionsclub.org");
verify(organisationRepository).findByNom("Lions Club Test New");
}
@Test
void testCreerOrganisation_EmailDejaExistant() {
// Given
when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.of(organisationTest));
// When & Then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> organisationService.creerOrganisation(organisationTest));
assertEquals("Une organisation avec cet email existe déjà", exception.getMessage());
verify(organisationRepository).findByEmail("test@lionsclub.org");
verify(organisationRepository, never()).findByNom(anyString());
}
@Test
void testCreerOrganisation_NomDejaExistant() {
// Given
when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty());
when(organisationRepository.findByNom(anyString())).thenReturn(Optional.of(organisationTest));
// When & Then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> organisationService.creerOrganisation(organisationTest));
assertEquals("Une organisation avec ce nom existe déjà", exception.getMessage());
verify(organisationRepository).findByEmail("test@lionsclub.org");
verify(organisationRepository).findByNom("Lions Club Test");
}
@Test
void testMettreAJourOrganisation_Success() {
// Given
Organisation organisationMiseAJour = Organisation.builder()
.nom("Lions Club Test Modifié")
.email("test@lionsclub.org")
.description("Description modifiée")
.telephone("+225 01 02 03 04 06")
.build();
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
when(organisationRepository.findByNom("Lions Club Test Modifié")).thenReturn(Optional.empty());
// When
Organisation result = organisationService.mettreAJourOrganisation(1L, organisationMiseAJour, "testUser");
// Then
assertNotNull(result);
assertEquals("Lions Club Test Modifié", result.getNom());
assertEquals("Description modifiée", result.getDescription());
assertEquals("+225 01 02 03 04 06", result.getTelephone());
assertEquals("testUser", result.getModifiePar());
assertNotNull(result.getDateModification());
assertEquals(1L, result.getVersion());
}
@Test
void testMettreAJourOrganisation_OrganisationNonTrouvee() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.empty());
// When & Then
NotFoundException exception = assertThrows(NotFoundException.class,
() -> organisationService.mettreAJourOrganisation(1L, organisationTest, "testUser"));
assertEquals("Organisation non trouvée avec l'ID: 1", exception.getMessage());
}
@Test
void testSupprimerOrganisation_Success() {
// Given
organisationTest.setNombreMembres(0);
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
organisationService.supprimerOrganisation(1L, "testUser");
// Then
assertFalse(organisationTest.getActif());
assertEquals("DISSOUTE", organisationTest.getStatut());
assertEquals("testUser", organisationTest.getModifiePar());
assertNotNull(organisationTest.getDateModification());
}
@Test
void testSupprimerOrganisation_AvecMembresActifs() {
// Given
organisationTest.setNombreMembres(5);
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When & Then
IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> organisationService.supprimerOrganisation(1L, "testUser"));
assertEquals("Impossible de supprimer une organisation avec des membres actifs", exception.getMessage());
}
@Test
void testTrouverParId_Success() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
Optional<Organisation> result = organisationService.trouverParId(1L);
// Then
assertTrue(result.isPresent());
assertEquals("Lions Club Test", result.get().getNom());
verify(organisationRepository).findByIdOptional(1L);
}
@Test
void testTrouverParId_NonTrouve() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.empty());
// When
Optional<Organisation> result = organisationService.trouverParId(1L);
// Then
assertFalse(result.isPresent());
verify(organisationRepository).findByIdOptional(1L);
}
@Test
void testTrouverParEmail_Success() {
// Given
when(organisationRepository.findByEmail("test@lionsclub.org")).thenReturn(Optional.of(organisationTest));
// When
Optional<Organisation> result = organisationService.trouverParEmail("test@lionsclub.org");
// Then
assertTrue(result.isPresent());
assertEquals("Lions Club Test", result.get().getNom());
verify(organisationRepository).findByEmail("test@lionsclub.org");
}
@Test
void testListerOrganisationsActives() {
// Given
List<Organisation> organisations = Arrays.asList(organisationTest);
when(organisationRepository.findAllActives()).thenReturn(organisations);
// When
List<Organisation> result = organisationService.listerOrganisationsActives();
// Then
assertNotNull(result);
assertEquals(1, result.size());
assertEquals("Lions Club Test", result.get(0).getNom());
verify(organisationRepository).findAllActives();
}
@Test
void testActiverOrganisation_Success() {
// Given
organisationTest.setStatut("SUSPENDUE");
organisationTest.setActif(false);
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
Organisation result = organisationService.activerOrganisation(1L, "testUser");
// Then
assertNotNull(result);
assertEquals("ACTIVE", result.getStatut());
assertTrue(result.getActif());
assertEquals("testUser", result.getModifiePar());
assertNotNull(result.getDateModification());
}
@Test
void testSuspendreOrganisation_Success() {
// Given
when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest));
// When
Organisation result = organisationService.suspendreOrganisation(1L, "testUser");
// Then
assertNotNull(result);
assertEquals("SUSPENDUE", result.getStatut());
assertFalse(result.getAccepteNouveauxMembres());
assertEquals("testUser", result.getModifiePar());
assertNotNull(result.getDateModification());
}
@Test
void testObtenirStatistiques() {
// Given
when(organisationRepository.count()).thenReturn(100L);
when(organisationRepository.countActives()).thenReturn(85L);
when(organisationRepository.countNouvellesOrganisations(any(LocalDate.class))).thenReturn(5L);
// When
Map<String, Object> result = organisationService.obtenirStatistiques();
// Then
assertNotNull(result);
assertEquals(100L, result.get("totalOrganisations"));
assertEquals(85L, result.get("organisationsActives"));
assertEquals(15L, result.get("organisationsInactives"));
assertEquals(5L, result.get("nouvellesOrganisations30Jours"));
assertEquals(85.0, result.get("tauxActivite"));
assertNotNull(result.get("timestamp"));
}
@Test
void testConvertToDTO() {
// When
var dto = organisationService.convertToDTO(organisationTest);
// Then
assertNotNull(dto);
assertEquals("Lions Club Test", dto.getNom());
assertEquals("LC Test", dto.getNomCourt());
assertEquals("test@lionsclub.org", dto.getEmail());
assertEquals("Organisation de test", dto.getDescription());
assertEquals("+225 01 02 03 04 05", dto.getTelephone());
assertEquals("Abidjan", dto.getVille());
assertEquals(25, dto.getNombreMembres());
assertTrue(dto.getActif());
}
@Test
void testConvertToDTO_Null() {
// When
var dto = organisationService.convertToDTO(null);
// Then
assertNull(dto);
}
@Test
void testConvertFromDTO() {
// Given
var dto = organisationService.convertToDTO(organisationTest);
// When
Organisation result = organisationService.convertFromDTO(dto);
// Then
assertNotNull(result);
assertEquals("Lions Club Test", result.getNom());
assertEquals("LC Test", result.getNomCourt());
assertEquals("test@lionsclub.org", result.getEmail());
assertEquals("Organisation de test", result.getDescription());
assertEquals("+225 01 02 03 04 05", result.getTelephone());
assertEquals("Abidjan", result.getVille());
}
@Test
void testConvertFromDTO_Null() {
// When
Organisation result = organisationService.convertFromDTO(null);
// Then
assertNull(result);
}
}