feat: BackupService real pg_dump, OrganisationService region stats, SystemConfigService overrides
- BackupService: DB-persisted metadata (BackupRecord/BackupConfig entities + V16 Flyway migration), real pg_dump execution via ProcessBuilder, soft-delete on deleteBackup, pg_restore manual guidance - OrganisationService: repartitionRegion now queries Adresse entities (was Map.of() stub) - SystemConfigService: in-memory config overrides via AtomicReference (no DB dependency) - SystemMetricsService: null-guard on MemoryMXBean in getSystemStatus() (fixes test NPE) - Souscription workflow: SouscriptionService, SouscriptionResource, FormuleAbonnementRepository, V11 Flyway migration, admin REST clients - Flyway V8-V15: notes membres, types référence, type orga constraint, seed roles, première connexion, Wave checkout URL, Wave telephone column length fix - .gitignore: added uploads/ and .claude/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.membre.CompteAdherentResponse;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.entity.SouscriptionOrganisation;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.SouscriptionOrganisationRepository;
|
||||
import dev.lions.unionflow.server.service.CompteAdherentService;
|
||||
import dev.lions.unionflow.server.service.MembreKeycloakSyncService;
|
||||
import dev.lions.unionflow.server.service.MembreService;
|
||||
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
||||
import io.quarkus.security.Authenticated;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
@@ -9,6 +19,12 @@ 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.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Endpoint REST pour le compte adhérent du membre connecté.
|
||||
@@ -28,16 +44,36 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
@Tag(name = "Compte Adhérent", description = "Vue financière unifiée du membre connecté")
|
||||
public class CompteAdherentResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(CompteAdherentResource.class);
|
||||
|
||||
@Inject
|
||||
CompteAdherentService compteAdherentService;
|
||||
|
||||
@Inject
|
||||
SecuriteHelper securiteHelper;
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
MembreOrganisationRepository membreOrganisationRepo;
|
||||
|
||||
@Inject
|
||||
SouscriptionOrganisationRepository souscriptionRepo;
|
||||
|
||||
@Inject
|
||||
MembreKeycloakSyncService membreKeycloakSyncService;
|
||||
|
||||
@Inject
|
||||
MembreService membreService;
|
||||
|
||||
/**
|
||||
* Retourne le compte adhérent complet du membre connecté :
|
||||
* numéro de membre, soldes (cotisations + épargne), capacité d'emprunt, taux d'engagement.
|
||||
*/
|
||||
@GET
|
||||
@Path("/mon-compte")
|
||||
@RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN" })
|
||||
@RolesAllowed({ "USER", "MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "SUPER_ADMIN" })
|
||||
@Operation(
|
||||
summary = "Compte adhérent du membre connecté",
|
||||
description = "Agrège cotisations, épargne et crédit en une vue financière unifiée."
|
||||
@@ -46,4 +82,144 @@ public class CompteAdherentResource {
|
||||
CompteAdherentResponse compte = compteAdherentService.getMonCompte();
|
||||
return Response.ok(compte).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le statut du compte du membre connecté.
|
||||
*
|
||||
* <p>Endpoint léger appelé par le mobile juste après le login Keycloak
|
||||
* pour détecter les comptes en attente/suspendus/désactivés avant d'accorder l'accès.
|
||||
*
|
||||
* <p>Si aucun enregistrement membre n'existe (ex. : administrateur créé directement
|
||||
* dans Keycloak sans fiche membre), retourne {@code ACTIF} pour ne pas bloquer les admins.
|
||||
*/
|
||||
@GET
|
||||
@Path("/mon-statut")
|
||||
@Authenticated
|
||||
@Operation(
|
||||
summary = "Statut du compte du membre connecté",
|
||||
description = "Retourne statutCompte : ACTIF, EN_ATTENTE_VALIDATION, SUSPENDU ou DESACTIVE."
|
||||
)
|
||||
public Response getMonStatut() {
|
||||
String email = securiteHelper.resolveEmail();
|
||||
if (email == null || email.isBlank()) {
|
||||
return Response.status(Response.Status.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
Optional<Membre> membreOpt = membreRepository.findByEmail(email.trim())
|
||||
.or(() -> membreRepository.findByEmail(email.trim().toLowerCase()));
|
||||
|
||||
// Pas de fiche membre → administrateur pur → accès autorisé
|
||||
String statutCompte = membreOpt
|
||||
.map(Membre::getStatutCompte)
|
||||
.filter(s -> s != null && !s.isBlank())
|
||||
.orElse("ACTIF");
|
||||
|
||||
// Auto-activer si le membre a été créé par un admin dont l'org a une souscription active.
|
||||
// Couvre les membres créés avant l'auto-activation à la création ET les cas limites futurs.
|
||||
if ("EN_ATTENTE_VALIDATION".equals(statutCompte) && membreOpt.isPresent()) {
|
||||
Membre m = membreOpt.get();
|
||||
UUID orgId = membreOrganisationRepo.findFirstByMembreId(m.getId())
|
||||
.map(mo -> mo.getOrganisation().getId())
|
||||
.orElse(null);
|
||||
if (membreService.orgHasActiveSubscription(orgId)) {
|
||||
LOG.infof("Auto-activation au login de %s (org %s a souscription active)", m.getEmail(), orgId);
|
||||
membreService.activerMembre(m.getId());
|
||||
try {
|
||||
membreKeycloakSyncService.activerMembreDansKeycloak(m.getId());
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Activation Keycloak au login échouée pour %s (non bloquant): %s",
|
||||
m.getEmail(), e.getMessage());
|
||||
}
|
||||
statutCompte = "ACTIF";
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("statutCompte", statutCompte);
|
||||
|
||||
// Signaler si le membre doit changer son mot de passe (premier login)
|
||||
boolean changerMotDePasseRequis = membreOpt
|
||||
.map(m -> Boolean.TRUE.equals(m.getPremiereConnexion()))
|
||||
.orElse(false);
|
||||
response.put("changerMotDePasseRequis", changerMotDePasseRequis);
|
||||
|
||||
// Enrichir avec l'état d'onboarding pour les comptes en attente
|
||||
if ("EN_ATTENTE_VALIDATION".equals(statutCompte)) {
|
||||
membreOpt.flatMap(m -> membreOrganisationRepo.findFirstByMembreId(m.getId())
|
||||
.map(MembreOrganisation::getOrganisation))
|
||||
.ifPresent(org -> {
|
||||
response.put("organisationId", org.getId().toString());
|
||||
Optional<SouscriptionOrganisation> souscOpt =
|
||||
souscriptionRepo.findLatestByOrganisationId(org.getId());
|
||||
|
||||
if (souscOpt.isEmpty()) {
|
||||
response.put("onboardingState", "NO_SUBSCRIPTION");
|
||||
} else {
|
||||
SouscriptionOrganisation sosc = souscOpt.get();
|
||||
String valState = sosc.getStatutValidation() != null
|
||||
? sosc.getStatutValidation().name()
|
||||
: "EN_ATTENTE_PAIEMENT";
|
||||
String onboardingState = switch (valState) {
|
||||
case "EN_ATTENTE_PAIEMENT" -> "AWAITING_PAYMENT";
|
||||
case "PAIEMENT_INITIE" -> "PAYMENT_INITIATED";
|
||||
case "PAIEMENT_CONFIRME" -> "AWAITING_VALIDATION";
|
||||
case "VALIDEE" -> "VALIDATED";
|
||||
case "REJETEE" -> "REJECTED";
|
||||
default -> "AWAITING_PAYMENT";
|
||||
};
|
||||
response.put("onboardingState", onboardingState);
|
||||
response.put("souscriptionId", sosc.getId().toString());
|
||||
if (sosc.getWaveSessionId() != null) {
|
||||
response.put("waveSessionId", sosc.getWaveSessionId());
|
||||
}
|
||||
}
|
||||
response.put("typeOrganisation", org.getTypeOrganisation() != null
|
||||
? org.getTypeOrganisation() : "ASSOCIATION");
|
||||
});
|
||||
if (!response.containsKey("onboardingState")) {
|
||||
response.put("onboardingState", "NO_SUBSCRIPTION");
|
||||
}
|
||||
}
|
||||
|
||||
return Response.ok(response).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Permet au membre connecté de changer son mot de passe lors du premier login.
|
||||
* Appelle Keycloak via lions-user-manager et marque {@code premiereConnexion = false}.
|
||||
*
|
||||
* <p>Body attendu : {@code { "nouveauMotDePasse": "..." }}
|
||||
*/
|
||||
@PUT
|
||||
@Path("/mon-compte/mot-de-passe")
|
||||
@Authenticated
|
||||
@Operation(
|
||||
summary = "Changer le mot de passe au premier login",
|
||||
description = "Met à jour le mot de passe Keycloak et lève le flag premiereConnexion."
|
||||
)
|
||||
public Response changerMotDePasse(Map<String, String> body) {
|
||||
String email = securiteHelper.resolveEmail();
|
||||
if (email == null || email.isBlank()) {
|
||||
return Response.status(Response.Status.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
String nouveauMotDePasse = body == null ? null : body.get("nouveauMotDePasse");
|
||||
if (nouveauMotDePasse == null || nouveauMotDePasse.isBlank()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("message", "Le champ 'nouveauMotDePasse' est requis."))
|
||||
.build();
|
||||
}
|
||||
|
||||
Optional<Membre> membreOpt = membreRepository.findByEmail(email.trim())
|
||||
.or(() -> membreRepository.findByEmail(email.trim().toLowerCase()));
|
||||
|
||||
if (membreOpt.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("message", "Aucun membre trouvé pour ce compte."))
|
||||
.build();
|
||||
}
|
||||
|
||||
membreKeycloakSyncService.changerMotDePassePremierLogin(membreOpt.get().getId(), nouveauMotDePasse);
|
||||
return Response.ok(Map.of("message", "Mot de passe mis à jour avec succès.")).build();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user