- KeycloakAdminHttpClient (nouveau) : client HTTP natif (java.net.http.HttpClient)
pour contourner les problèmes de désérialisation avec RESTEasy sur certains
endpoints Keycloak 26+ (bruteForceStrategy, cpuInfo inconnus).
Utilise ObjectMapper avec FAIL_ON_UNKNOWN_PROPERTIES=false.
- AdminUserService : utilisation correcte de AdminUserServiceClient + AdminRoleServiceClient
avec AdminServiceTokenHeadersFactory pour l'auth.
- ModuleAccessFilter : améliorations de la logique @RequiresModule.
Migrations :
- V25 : numero_transaction nullable dans paiements (legacy V1 NOT NULL bloquant INSERT)
- V26 : autres colonnes legacy NOT NULL V1 (type_paiement, statut_paiement, etc.)
rendues nullables pour alignement avec l'entité Paiement
Refactor Paiement/PaiementObjet : mise à jour entités, repository, resource, service
pour cohérence avec le nouveau module Versement. Tests associés supprimés/ajustés.
verifierOwnershipEtProtectionAdmin() appelé sur les 5 endpoints lifecycle
(radier-adhesion, archiver-adhesion, activer/suspendre/radier par membre):
1. Ownership: un ADMIN_ORGANISATION ne peut agir que sur les membres des
organisations dont il est responsable (sinon 403).
2. Anti-admin: un ADMIN_ORGANISATION ne peut pas agir sur un autre ORGADMIN
ou SUPERADMIN (sinon 403).
3. SUPER_ADMIN/ADMIN passent directement (accès total).
Comble les failles SEC-01/SEC-02 de l'audit technique.
- Nouveau MembreRoleSyncService.ensureOrgAdminRole : auto-crée un MembreRole ORGADMIN
quand un user avec rôle Keycloak ADMIN_ORGANISATION se connecte sans entrée DB
(couvre les comptes créés directement dans Keycloak).
- OrganisationContextFilter appelle syncService.ensureOrgAdminRole quand le rôle
Keycloak est présent mais MembreRole absent (non bloquant sur erreur).
- MembreRoleRepository.countAdminsByOrganisationId : count strict (ORGADMIN + actif
+ dateDebut/dateFin valides) avec fallback sur codes alternatifs si strict=0.
- OrganisationService.convertToResponse : nombreAdministrateurs dynamique via
MembreRoleRepository (remplace le champ Organisation jamais mis à jour).
- V30: ajoute membre_organisation_id/organisation_id/date_debut/fin/commentaire si absents,
rend membre_id nullable (legacy V1), remplace uk_membre_role par uk_mr_membre_org_role,
ajoute indexes. Idempotent via DO blocks.
- V31: rend destinataire_id, titre, nombre_tentatives nullables dans notifications
(colonnes legacy V1 que l'entité n'utilise plus, bloquaient les INSERT).
@Provider enregistre le filtre GLOBALEMENT sur tous les REST clients.
JwtPropagationFilter s'exécutait après AdminServiceTokenHeadersFactory et
écrasait le token de service account avec le JWT utilisateur mobile
→ LUM recevait un token du realm unionflow (kid inconnu) → 401.
La propagation JWT est déjà gérée par OidcTokenPropagationHeadersFactory
sur UserServiceClient/RoleServiceClient via @RegisterClientHeaders.
JwtPropagationFilter est conservé sans @Provider pour référence future.
Le service admin injectait UserServiceClient/RoleServiceClient (propagation du token
utilisateur unionflow) au lieu des clients Admin dédiés (service account lions-user-manager).
Résultat : le token JWT de l'utilisateur mobile était envoyé à LUM → 401 car LUM ne
connaît pas les clés du realm unionflow.
Correctif :
- AdminUserService -> AdminUserServiceClient + AdminRoleServiceClient (service account)
- UserServiceClient + RoleServiceClient remis à OidcTokenPropagationHeadersFactory
(ces clients non-admin propagent le token utilisateur pour des usages futurs)
Les appels vers lions-user-manager nécessitent un token du realm lions-user-manager
(service account). OidcTokenPropagationHeadersFactory transmettait le token utilisateur
du realm unionflow → 401 systématique. AdminServiceTokenHeadersFactory injecte le bon
token via l'OIDC client admin-service.
- application-dev.properties : token.issuer utilise ${DEV_HOST:localhost} pour valider
les JWT émis par Keycloak via l'IP LAN (mobile physique sur réseau local)
- .env : DEV_HOST=192.168.1.13 — source unique côté backend, en sync avec
android/local.properties → dev.host côté mobile
- V24 : suppression des 8 tables fantômes issues des migrations pré-consolidation
(document, permission, favori, ticket, suggestion, suggestion_vote, configuration,
role_permission) — toutes vides, les entités JPA pointaient déjà vers les tables
plurielles correctes. Les contraintes uk_role_permission et uk_suggestion_vote
sont maintenant sur les vraies tables (roles_permissions, suggestion_votes).
ConversationResponse/MessageResponse fields (muted, pinned, archived,
edited, deleted) are primitive booleans — Lombok @Builder generates
.muted() not .isMuted(). Also use Boolean.TRUE.equals() for null-safe
unboxing from entity Boolean wrapper fields.
MembreDashboardServiceTest and OrganisationServiceTest used record
accessor syntax (e.g. result.nom()) on DTO classes that were converted
from records to @Data classes — now using getters (result.getNom()).
BUG-01: BudgetService.toResponse() — remplace doubleValue()>0 par
compareTo(BigDecimal.ZERO)>0 (précision BigDecimal) ; ajoute 2 tests
couvrant varianceRate=0 (totalPlanned=0) et varianceRate=-40%
AUTH: MembreKeycloakSyncService.changerMotDePassePremierLogin() — élargit
le catch de ForbiddenException vers WebApplicationException avec vérification
du statut HTTP (le REST client MicroProfile ne garantit pas la sous-classe)
DATA-01: MembreService.desactiverMembre() — décrémente nombreMembres sur
toutes les orgs actives du membre et passe le statutMembre à DESACTIVE
La colonne version était VARCHAR(20) au lieu de BIGINT, causant une erreur
Hibernate validate au démarrage. L'entité ModuleDisponible n'a pas de champ
version propre — la colonne était orpheline et conflicte avec BaseEntity @Version.
Fix idempotent : suppression VARCHAR + ajout BIGINT DEFAULT 0 (table vide).
- TypeReference: ajout des champs categorie et modulesRequis (colonnes DB existantes depuis V18
mais non mappées en JPA — Hibernate validate échouait silencieusement)
- OrganisationService.creerOrganisation(): lit types_reference.modules_requis pour initialiser
Organisation.modulesActifs, au lieu de dépendre uniquement du switch hardcodé dans
OrganisationModuleService.getModulesParType()
Avant: un type créé via CRUD (ex: TANTANPION) tombait dans le default du switch → aucun
module métier → rôles métier assignables mais menus jamais affichés.
Après: tout type avec modules_requis renseigné dans types_reference active correctement
ses modules à la création de l'organisation.
Hibernate validate mode in prod requires exact table names.
V1 used singular names (permission, document, suggestion, etc.)
but entities use plural names (permissions, documents, suggestions, etc.).
Hibernate update mode was masking this by auto-creating plural tables.
Renamed 23 tables to match entities.
- SystemAlert.onCreate() now calls super.onCreate() to set dateCreation (was null → NOT NULL violation every minute)
- V1: types_reference updated with full schema (domaine, est_defaut, est_systeme, ordre_affichage, modules_requis, organisation_id)
- V9: idempotent guard for categorie nullable ALTER (already nullable in updated V1)
14 fichiers de tests : rôles passés en majuscules dans @TestSecurity(roles={...})
pour correspondre exactement aux valeurs @RolesAllowed du backend.
Mapping : "admin"→"ADMIN", "admin_organisation"→"ADMIN_ORGANISATION",
"membre_actif"→"MEMBRE", "tontine_resp"→"TONTINE_RESP", etc.
Un rôle en minuscules produit un 403 silencieux sans erreur Quarkus.
MembreResource : activerAdhesion/suspendrAdhesion/radierAdhesion/archiverAdhesion
remplacent Map.of() par HashMap pour accepter dateChangementStatut null
(Map.of() lève NullPointerException sur valeur null → HTTP 500).
Sans @Column(name=...), Hibernate génère des noms camelCase (includedatabase)
alors que V16 a créé les colonnes en snake_case (include_database).
Alignement explicite avec les conventions des autres entités.
V4: system_logs colonnes niveau/stacktrace déjà renommées en prod
→ RENAME COLUMN enveloppé dans DO blocks IF EXISTS
V6: conversations/messages/conversation_participants existent déjà en prod
→ CREATE TABLE + CREATE INDEX → IF NOT EXISTS
- MembreKeycloakSyncService: completerPremierLogin retourne un enum PremierLoginResultat
(COMPLETE / REAUTH_REQUIS / NON_APPLICABLE) au lieu d'un boolean
- Détection automatique des anciens comptes (sans UPDATE_PASSWORD ni marqueur KC):
assigne UPDATE_PASSWORD + attribut premiere_password_pending dans Keycloak
et retourne REAUTH_REQUIS pour que Flutter re-déclenche AppAuth
- Détection du mot de passe changé: marqueur présent + UPDATE_PASSWORD absent → COMPLETE
- createUserDTOFromMembre: ajoute l'attribut premiere_password_pending=true à la création
- CompteAdherentResource.getMonStatut: retourne reAuthRequired=true quand REAUTH_REQUIS
formule_abonnement et souscription_organisation n'existent pas dans la DB
prod (nommées formules_abonnement/souscriptions_organisation dans l'ancien
schema). Remplacé par des DO blocks conditionnels (IF EXISTS).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- quarkus.flyway.ignore-migration-patterns=*:missing — migrations V1.2/V1.3
appliquées en DB mais absentes localement (après consolidation V1-V10)
- quarkus.log.file.enable=false — /var/log/unionflow/ absent dans le container
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>