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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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é");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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';
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user