feat: WebSocket temps réel + Finance Workflow + corrections

- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics)
  * Backend: KafkaEventProducer, KafkaEventConsumer
  * Mobile: WebSocketService (reconnection, heartbeat, typed events)
  * DashboardBloc: Auto-refresh depuis WebSocket events

- Finance Workflow: approbations + budgets (backend + mobile)
  * Backend: entities, services, resources, migrations Flyway V6
  * Mobile: features finance_workflow complète avec BLoC

- Corrections DI: interfaces IRepository partout
  * IProfileRepository, IOrganizationRepository, IMembreRepository
  * GetIt configuré avec @injectable

- Spec-Kit: constitution + templates mis à jour
  * .specify/memory/constitution.md enrichie
  * Templates agent, plan, spec, tasks, checklist

- Nettoyage: fichiers temporaires supprimés

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -0,0 +1,205 @@
# Script PowerShell pour appliquer la sécurisation automatique aux pages XHTML
# Usage: .\apply-page-security.ps1
$ErrorActionPreference = "Stop"
# Mapping des chemins de pages vers leurs rôles autorisés
$pageSecurityMap = @{
# Super Admin
"super-admin/dashboard.xhtml" = "SUPER_ADMIN"
"super-admin/dashboard-enhanced.xhtml" = "SUPER_ADMIN"
"super-admin/entites/gestion-enhanced.xhtml" = "SUPER_ADMIN"
"super-admin/roles/gestion.xhtml" = "SUPER_ADMIN"
"super-admin/types/organisations.xhtml" = "SUPER_ADMIN"
# Admin Organisation
"admin/audit.xhtml" = "ADMIN,SUPER_ADMIN"
"admin/backup.xhtml" = "ADMIN,SUPER_ADMIN"
"admin/settings.xhtml" = "ADMIN,SUPER_ADMIN"
"admin/users.xhtml" = "ADMIN,SUPER_ADMIN"
"secure/admin/utilisateurs.xhtml" = "ADMIN,SUPER_ADMIN"
"secure/admin/audit.xhtml" = "ADMIN,SUPER_ADMIN"
"secure/admin/parametres.xhtml" = "ADMIN,SUPER_ADMIN"
"secure/admin/roles.xhtml" = "ADMIN,SUPER_ADMIN"
"admin/audit/journal.xhtml" = "ADMIN,SUPER_ADMIN"
# Gestion des membres
"secure/membre/inscription.xhtml" = "SECRETAIRE,ADMIN"
"secure/membre/recherche.xhtml" = "SECRETAIRE,TRESORIER,RESPONSABLE_SOCIAL,RESPONSABLE_EVENEMENTS,ADMIN"
"secure/membre/profil.xhtml" = "SECRETAIRE,ADMIN"
"secure/membre/import.xhtml" = "SECRETAIRE,ADMIN"
"secure/membre/export.xhtml" = "SECRETAIRE,TRESORIER,ADMIN"
"secure/membre/cotisations.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
# Gestion financière
"admin/finance/caisse.xhtml" = "TRESORIER,ADMIN"
"secure/finance/tresorerie.xhtml" = "TRESORIER,ADMIN"
"secure/finance/budgets.xhtml" = "TRESORIER,ADMIN"
"secure/finance/bilans.xhtml" = "TRESORIER,ADMIN"
"secure/comptabilite/gestion.xhtml" = "TRESORIER,ADMIN"
"admin/cotisations/gestion.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"secure/cotisation/collect.xhtml" = "TRESORIER,ADMIN"
"secure/cotisation/paiement.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"secure/cotisation/reminders.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"secure/cotisation/report.xhtml" = "TRESORIER,ADMIN"
"secure/cotisation/rapports.xhtml" = "TRESORIER,ADMIN"
# Gestion des événements
"admin/evenements/liste.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"admin/evenements/creation.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"admin/evenements/gestion.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"admin/evenements/participants.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"secure/evenement/creation.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"secure/evenement/gestion.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"secure/evenement/create.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"secure/evenement/planification.xhtml" = "RESPONSABLE_EVENEMENTS,ADMIN"
"secure/evenement/logistique.xhtml" = "RESPONSABLE_EVENEMENTS,ADMIN"
"secure/evenement/bilan.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"secure/evenement/reservations.xhtml" = "RESPONSABLE_EVENEMENTS,ADMIN"
"secure/evenement/participants.xhtml" = "RESPONSABLE_EVENEMENTS,SECRETAIRE,ADMIN"
"secure/evenement/calendar.xhtml" = "ALL"
"secure/evenement/calendrier.xhtml" = "ALL"
"secure/evenement/participation.xhtml" = "ALL"
# Gestion des aides sociales
"admin/aides/gestion.xhtml" = "RESPONSABLE_SOCIAL,ADMIN"
"admin/demandes/gestion.xhtml" = "RESPONSABLE_SOCIAL,ADMIN"
"admin/demandes/aide-sociale.xhtml" = "RESPONSABLE_SOCIAL,ADMIN"
"secure/aide/demande.xhtml" = "ALL"
"secure/aide/statistiques.xhtml" = "RESPONSABLE_SOCIAL,ADMIN"
"secure/aide/historique.xhtml" = "ALL"
# Adhésions
"secure/adhesion/liste.xhtml" = "SECRETAIRE,ADMIN"
"secure/adhesion/demande.xhtml" = "ALL"
"secure/adhesion/new.xhtml" = "SECRETAIRE,ADMIN"
"secure/adhesion/renouvellement.xhtml" = "ALL"
"secure/adhesion/validation.xhtml" = "SECRETAIRE,ADMIN"
"secure/adhesion/history.xhtml" = "SECRETAIRE,ADMIN"
"secure/adhesion/historique.xhtml" = "ALL"
"secure/adhesion/pending.xhtml" = "SECRETAIRE,ADMIN"
"secure/adhesion/cartes-membres.xhtml" = "SECRETAIRE,ADMIN"
# Rapports
"secure/reports.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"secure/rapport/details.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"secure/rapport/export.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"secure/rapport/activites.xhtml" = "SECRETAIRE,ADMIN"
"secure/rapport/finances.xhtml" = "TRESORIER,ADMIN"
"secure/rapport/membres.xhtml" = "SECRETAIRE,ADMIN"
"secure/rapport/tableaux-bord.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"admin/rapports/finances.xhtml" = "TRESORIER,ADMIN"
"admin/rapports/statistiques.xhtml" = "ADMIN"
# Pages personnelles
"secure/profile.xhtml" = "ALL"
"secure/personnel/profil.xhtml" = "ALL"
"secure/personnel/activites.xhtml" = "ALL"
"secure/personnel/agenda.xhtml" = "ALL"
"secure/personnel/documents.xhtml" = "ALL"
"secure/personnel/notifications.xhtml" = "ALL"
"secure/personnel/preferences.xhtml" = "ALL"
"secure/personnel/favoris.xhtml" = "ALL"
"secure/personnel/parametres.xhtml" = "ALL"
"membre/cotisations.xhtml" = "ALL"
"membre/dashboard.xhtml" = "MEMBRE_ACTIF"
# Aide et support
"secure/aide/faq.xhtml" = "ALL"
"secure/aide/guide.xhtml" = "ALL"
"secure/aide/support.xhtml" = "ALL"
"secure/aide/tutoriels.xhtml" = "ALL"
"secure/aide/nouveautes.xhtml" = "ALL"
"secure/aide/apropos.xhtml" = "ALL"
"secure/aide/documentation.xhtml" = "ALL"
"secure/aide/suggestions.xhtml" = "ALL"
"secure/aide/tickets.xhtml" = "ALL"
# Communication
"secure/communication/notifications.xhtml" = "SECRETAIRE,ADMIN"
# Documents
"admin/documents/gestion.xhtml" = "SECRETAIRE,ADMIN"
"secure/documents/mes-documents.xhtml" = "ALL"
# Utilitaires
"secure/outils/exports-masse.xhtml" = "TRESORIER,SECRETAIRE,ADMIN"
"secure/stats.xhtml" = "ADMIN"
"secure/souscription/dashboard.xhtml" = "ADMIN"
}
$basePath = "C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-client-quarkus-primefaces-freya\src\main\resources\META-INF\resources\pages"
$securityComponent = @"
<!-- Sécurisation de la page basée sur les rôles -->
<ui:include src="/templates/components/security/page-access-control.xhtml">
<ui:param name="allowedRoles" value="__ROLES__" />
</ui:include>
"@
$processedCount = 0
$skippedCount = 0
$errorCount = 0
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Application de la Sécurisation des Pages" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
foreach ($page in $pageSecurityMap.Keys) {
$filePath = Join-Path $basePath $page
$roles = $pageSecurityMap[$page]
if (-not (Test-Path $filePath)) {
Write-Host "[SKIP] $page (fichier introuvable)" -ForegroundColor Yellow
$skippedCount++
continue
}
try {
$content = Get-Content $filePath -Raw -Encoding UTF8
# Vérifier si la sécurité est déjà appliquée
if ($content -match "page-access-control\.xhtml") {
Write-Host "[SKIP] $page (déjà sécurisée)" -ForegroundColor Gray
$skippedCount++
continue
}
# Insérer le composant de sécurité après <ui:composition> ou <ui:define name="content">
$securityBlock = $securityComponent -replace "__ROLES__", $roles
if ($content -match '<ui:composition[^>]*>') {
$content = $content -replace '(<ui:composition[^>]*>)', "`$1`n$securityBlock"
}
elseif ($content -match '<ui:define name="content">') {
$content = $content -replace '(<ui:define name="content">)', "`$1`n$securityBlock"
}
else {
Write-Host "[ERROR] $page (impossible de trouver le point d'insertion)" -ForegroundColor Red
$errorCount++
continue
}
# Sauvegarder le fichier
Set-Content -Path $filePath -Value $content -Encoding UTF8 -NoNewline
Write-Host "[OK] $page → Rôles: $roles" -ForegroundColor Green
$processedCount++
}
catch {
Write-Host "[ERROR] $page : $_" -ForegroundColor Red
$errorCount++
}
}
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Résumé" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host "Pages sécurisées : $processedCount" -ForegroundColor Green
Write-Host "Pages ignorées : $skippedCount" -ForegroundColor Yellow
Write-Host "Erreurs : $errorCount" -ForegroundColor Red
Write-Host ""
Write-Host "✓ Script terminé avec succès!" -ForegroundColor Green

View File

@@ -0,0 +1,44 @@
#!/bin/bash
KEYCLOAK_URL="http://localhost:8180"
REALM_NAME="unionflow"
TEST_PASSWORD="Test@123"
# Obtenir le token
TOKEN=$(curl -s -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" -d "username=admin" -d "password=admin" -d "grant_type=password" | \
grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
echo "Token obtenu, création des utilisateurs manquants..."
# Fonction de création simplifiée
create() {
USER=$1
EMAIL=$2
FIRST=$3
LAST=$4
ROLE=$5
curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"'$USER'","email":"'$EMAIL'","firstName":"'$FIRST'","lastName":"'$LAST'","enabled":true,"emailVerified":true,"credentials":[{"type":"password","value":"'$TEST_PASSWORD'","temporary":false}]}'
sleep 1
UID=$(curl -s "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users?username=$USER" \
-H "Authorization: Bearer $TOKEN" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
ROLEDATA=$(curl -s "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles/$ROLE" -H "Authorization: Bearer $TOKEN")
curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$UID/role-mappings/realm" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "[$ROLEDATA]"
echo "$USER créé"
}
create "tresorier.mukefi" "tresorier.mukefi@unionflow.test" "Tresorier" "MUKEFI" "TRESORIER"
create "secretaire.mukefi" "secretaire.mukefi@unionflow.test" "Secretaire" "MUKEFI" "SECRETAIRE"
create "credit.mukefi" "credit.mukefi@unionflow.test" "Credit" "MUKEFI" "RESPONSABLE_CREDIT"
create "secretaire.meska" "secretaire.meska@unionflow.test" "Secretaire" "MESKA" "SECRETAIRE"
create "evenements.meska" "evenements.meska@unionflow.test" "Evenements" "MESKA" "RESPONSABLE_EVENEMENTS"
echo "✅ Utilisateurs manquants créés"

View File

@@ -0,0 +1,133 @@
#!/bin/bash
# Création des organisations MUKEFI et MESKA via l'API REST
BACKEND_URL="http://localhost:8085"
KEYCLOAK_URL="http://localhost:8180"
REALM="unionflow"
CLIENT_ID="unionflow-server"
echo "========================================================================"
echo "Création des organisations MUKEFI et MESKA via l'API"
echo "========================================================================"
# 1. Obtenir un token JWT avec le compte superadmin
echo ""
echo "📡 Obtention du token JWT..."
TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token" \
-d "client_id=$CLIENT_ID" \
-d "username=superadmin" \
-d "password=Test@123" \
-d "grant_type=password")
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ Erreur: Impossible d'obtenir le token JWT"
echo " Vérifiez que le backend est démarré sur $BACKEND_URL"
exit 1
fi
echo "✅ Token JWT obtenu"
# 2. Créer MUKEFI (Mutuelle)
echo ""
echo "🏦 Création de MUKEFI (Mutuelle)..."
MUKEFI_JSON='{
"nom": "Mutuelle d'\''Épargne et de Crédit des Fonctionnaires et Indépendants",
"nomCourt": "MUKEFI",
"description": "Mutuelle d'\''épargne et de crédit dédiée aux fonctionnaires et travailleurs indépendants de Côte d'\''Ivoire",
"email": "contact@mukefi.org",
"telephone": "+225 07 00 00 00 01",
"siteWeb": "https://mukefi.org",
"typeOrganisation": "MUTUELLE_EPARGNE_CREDIT",
"statut": "ACTIVE",
"dateFondation": "2020-01-15",
"numeroEnregistrement": "MUT-CI-2020-001",
"devise": "XOF",
"budgetAnnuel": 500000000,
"cotisationObligatoire": true,
"montantCotisationAnnuelle": 50000,
"objectifs": "Favoriser l'\''épargne et l'\''accès au crédit pour les membres",
"activitesPrincipales": "Épargne, crédit, micro-crédit, formation financière",
"partenaires": "Banque Centrale des États de l'\''Afrique de l'\''Ouest (BCEAO)",
"latitude": 5.3364,
"longitude": -4.0267
}'
MUKEFI_RESPONSE=$(curl -s -X POST "$BACKEND_URL/api/organisations" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "$MUKEFI_JSON")
if echo "$MUKEFI_RESPONSE" | grep -q '"id"'; then
echo "✅ MUKEFI créée avec succès"
MUKEFI_ID=$(echo $MUKEFI_RESPONSE | grep -o '"id":"[^"]*' | cut -d'"' -f4)
echo " ID: $MUKEFI_ID"
else
echo "⚠️ MUKEFI: $(echo $MUKEFI_RESPONSE | head -c 200)"
fi
# 3. Créer MESKA (Association)
echo ""
echo "🤝 Création de MESKA (Association)..."
MESKA_JSON='{
"nom": "Mouvement d'\''Entraide et de Solidarité de Koumassi et Adjamé",
"nomCourt": "MESKA",
"description": "Association communautaire d'\''entraide et de solidarité basée à Abidjan",
"email": "contact@meska.org",
"telephone": "+225 07 00 00 00 02",
"siteWeb": "https://meska.org",
"typeOrganisation": "ASSOCIATION",
"statut": "ACTIVE",
"dateFondation": "2018-06-20",
"numeroEnregistrement": "ASSO-CI-2018-045",
"devise": "XOF",
"budgetAnnuel": 25000000,
"cotisationObligatoire": true,
"montantCotisationAnnuelle": 25000,
"objectifs": "Promouvoir la solidarité et l'\''entraide entre les membres des communes de Koumassi et Adjamé",
"activitesPrincipales": "Aide sociale, événements communautaires, formations, projets collectifs",
"partenaires": "Mairie de Koumassi, Mairie d'\''Adjamé",
"latitude": 5.2931,
"longitude": -3.9468
}'
MESKA_RESPONSE=$(curl -s -X POST "$BACKEND_URL/api/organisations" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "$MESKA_JSON")
if echo "$MESKA_RESPONSE" | grep -q '"id"'; then
echo "✅ MESKA créée avec succès"
MESKA_ID=$(echo $MESKA_RESPONSE | grep -o '"id":"[^"]*' | cut -d'"' -f4)
echo " ID: $MESKA_ID"
else
echo "⚠️ MESKA: $(echo $MESKA_RESPONSE | head -c 200)"
fi
# 4. Vérifier les organisations créées
echo ""
echo "📋 Vérification des organisations créées..."
ORGS_LIST=$(curl -s -X GET "$BACKEND_URL/api/organisations" \
-H "Authorization: Bearer $ACCESS_TOKEN")
echo "$ORGS_LIST" | grep -o '"nom":"[^"]*' | cut -d'"' -f4
echo ""
echo "========================================================================"
echo "✅ Création des organisations terminée"
echo "========================================================================"
echo ""
echo "🏦 MUKEFI:"
echo " → Type: Mutuelle d'épargne et de crédit"
echo " → Email: contact@mukefi.org"
echo " → Modules: Cotisations, Épargne/Crédit, Comptabilité, Documents"
echo ""
echo "🤝 MESKA:"
echo " → Type: Association"
echo " → Email: contact@meska.org"
echo " → Modules: Cotisations, Événements, Solidarité, Documents"
echo ""

View File

@@ -0,0 +1,138 @@
-- Script de création des organisations de test MUKEFI et MESKA
-- UnionFlow - Configuration initiale
-- ============================================================================
-- 1. CRÉATION DE L'ORGANISATION MUKEFI (Mutuelle d'épargne et de crédit)
-- ============================================================================
-- Supprimer l'organisation si elle existe déjà
DELETE FROM organisation WHERE code = 'MUKEFI';
-- Créer MUKEFI
INSERT INTO organisation (
id,
code,
nom,
sigle,
type_organisation,
email,
telephone,
adresse,
ville,
pays,
date_creation,
date_modification,
cree_par,
modifie_par,
version,
actif
) VALUES (
gen_random_uuid(),
'MUKEFI',
'Mutuelle d''Épargne et de Crédit des Fonctionnaires et Indépendants',
'MUKEFI',
'MUTUELLE_EPARGNE_CREDIT',
'contact@mukefi.org',
'+225 07 00 00 00 01',
'01 BP 1234 Abidjan 01',
'Abidjan',
'Côte d''Ivoire',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
'superadmin@unionflow.test',
'superadmin@unionflow.test',
0,
true
);
-- Activer les modules pour MUKEFI
-- Note: Vous devrez adapter cette partie selon votre schéma de table module_organisation
COMMENT ON TABLE organisation IS 'MUKEFI - Mutuelle créée pour tests avec modules: Cotisations, Épargne/Crédit, Comptabilité, Documents, Notifications';
-- ============================================================================
-- 2. CRÉATION DE L'ORGANISATION MESKA (Association)
-- ============================================================================
-- Supprimer l'organisation si elle existe déjà
DELETE FROM organisation WHERE code = 'MESKA';
-- Créer MESKA
INSERT INTO organisation (
id,
code,
nom,
sigle,
type_organisation,
email,
telephone,
adresse,
ville,
pays,
date_creation,
date_modification,
cree_par,
modifie_par,
version,
actif
) VALUES (
gen_random_uuid(),
'MESKA',
'Mouvement d''Entraide et de Solidarité de Koumassi et Adjamé',
'MESKA',
'ASSOCIATION',
'contact@meska.org',
'+225 07 00 00 00 02',
'Commune de Koumassi, Abidjan',
'Abidjan',
'Côte d''Ivoire',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
'superadmin@unionflow.test',
'superadmin@unionflow.test',
0,
true
);
-- Activer les modules pour MESKA
COMMENT ON TABLE organisation IS 'MESKA - Association créée pour tests avec modules: Cotisations, Événements, Solidarité, Documents, Notifications';
-- ============================================================================
-- 3. VÉRIFICATION
-- ============================================================================
-- Lister les organisations créées
SELECT
code,
nom,
sigle,
type_organisation,
email,
ville,
actif
FROM organisation
WHERE code IN ('MUKEFI', 'MESKA')
ORDER BY code;
-- Afficher les IDs pour référence
\echo '==================================================================='
\echo 'Organisations créées:'
\echo '==================================================================='
SELECT
'MUKEFI' as organisation,
id,
nom,
email
FROM organisation
WHERE code = 'MUKEFI'
UNION ALL
SELECT
'MESKA' as organisation,
id,
nom,
email
FROM organisation
WHERE code = 'MESKA';
\echo '==================================================================='

View File

@@ -0,0 +1,33 @@
#!/bin/bash
KEYCLOAK_URL="http://localhost:8180"
REALM_NAME="unionflow"
TEST_PASSWORD="Test@123"
TOKEN=$(curl -s -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" -d "username=admin" -d "password=admin" -d "grant_type=password" | \
grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
create() {
USER=$1; EMAIL=$2; FIRST=$3; LAST=$4; ROLE=$5
curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"username":"'$USER'","email":"'$EMAIL'","firstName":"'$FIRST'","lastName":"'$LAST'","enabled":true,"emailVerified":true,"credentials":[{"type":"password","value":"'$TEST_PASSWORD'","temporary":false}]}'
sleep 1
USERID=$(curl -s "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users?username=$USER" \
-H "Authorization: Bearer $TOKEN" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
ROLEDATA=$(curl -s "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles/$ROLE" -H "Authorization: Bearer $TOKEN")
curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USERID/role-mappings/realm" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "[$ROLEDATA]" > /dev/null
echo "$USER"
}
echo "Création des utilisateurs manquants..."
create "tresorier.mukefi" "tresorier.mukefi@unionflow.test" "Tresorier" "MUKEFI" "TRESORIER"
create "secretaire.mukefi" "secretaire.mukefi@unionflow.test" "Secretaire" "MUKEFI" "SECRETAIRE"
create "credit.mukefi" "credit.mukefi@unionflow.test" "Credit" "MUKEFI" "RESPONSABLE_CREDIT"
create "secretaire.meska" "secretaire.meska@unionflow.test" "Secretaire" "MESKA" "SECRETAIRE"
create "evenements.meska" "evenements.meska@unionflow.test" "Evenements" "MESKA" "RESPONSABLE_EVENEMENTS"
echo "✅ Terminé"

View File

@@ -0,0 +1,87 @@
-- =============================================================================
-- Script de réparation Flyway — UnionFlow (mode dev)
-- =============================================================================
-- Exécuter UNE SEULE FOIS sur la base PostgreSQL "unionflow" AVANT de
-- relancer le serveur.
--
-- Contexte :
-- Hibernate (mode "update") avait créé certaines tables avant que Flyway
-- ne prenne le relais. Résultat : la table types_reference existe mais
-- manque la colonne "valeur_systeme" qu'attend V3.0.
-- flyway_schema_history s'arrête à la version 2.10.
--
-- Solution :
-- 1. Ajouter les colonnes manquantes (ajoutées par V3.x mais absentes
-- car Hibernate avait créé les tables avant).
-- 2. Marquer V3.0 → V3.6 comme "déjà appliquées" (success=true) dans
-- flyway_schema_history, afin que Flyway ne les reexécute pas.
-- 3. Flyway n'exécutera alors que V3.7 (seed des membres de test).
-- =============================================================================
BEGIN;
-- ─────────────────────────────────────────────────────────────────────────────
-- 1. Ajouter les colonnes manquantes dues au mode Hibernate "update"
-- ─────────────────────────────────────────────────────────────────────────────
-- V3.0 a besoin de cette colonne dans types_reference
ALTER TABLE types_reference
ADD COLUMN IF NOT EXISTS valeur_systeme BOOLEAN NOT NULL DEFAULT FALSE;
-- V3.5 peut avoir ajouté des colonnes d'adresse dans organisations
-- (ajout sécurisé — sans erreur si déjà présentes)
ALTER TABLE organisations ADD COLUMN IF NOT EXISTS adresse VARCHAR(255);
ALTER TABLE organisations ADD COLUMN IF NOT EXISTS ville VARCHAR(100);
ALTER TABLE organisations ADD COLUMN IF NOT EXISTS code_postal VARCHAR(20);
ALTER TABLE organisations ADD COLUMN IF NOT EXISTS pays VARCHAR(100);
ALTER TABLE organisations ADD COLUMN IF NOT EXISTS region VARCHAR(100);
-- ─────────────────────────────────────────────────────────────────────────────
-- 2. Marquer V3.0 à V3.6 comme appliquées dans flyway_schema_history
-- ─────────────────────────────────────────────────────────────────────────────
DO $$
DECLARE
max_rank INT;
BEGIN
SELECT COALESCE(MAX(installed_rank), 0) INTO max_rank FROM flyway_schema_history;
INSERT INTO flyway_schema_history
(installed_rank, version, description, type, script, checksum,
installed_by, installed_on, execution_time, success)
VALUES
(max_rank + 1, '3.0', 'Optimisation Structure Donnees', 'SQL',
'V3.0__Optimisation_Structure_Donnees.sql', 0, current_user, NOW(), 0, true),
(max_rank + 2, '3.1', 'Add Module Disponible FK', 'SQL',
'V3.1__Add_Module_Disponible_FK.sql', 0, current_user, NOW(), 0, true),
(max_rank + 3, '3.2', 'Seed Types Reference', 'SQL',
'V3.2__Seed_Types_Reference.sql', 0, current_user, NOW(), 0, true),
(max_rank + 4, '3.3', 'Optimisation Index Performance', 'SQL',
'V3.3__Optimisation_Index_Performance.sql', 0, current_user, NOW(), 0, true),
(max_rank + 5, '3.4', 'LCB FT Anti Blanchiment', 'SQL',
'V3.4__LCB_FT_Anti_Blanchiment.sql', 0, current_user, NOW(), 0, true),
(max_rank + 6, '3.5', 'Add Organisation Address Fields', 'SQL',
'V3.5__Add_Organisation_Address_Fields.sql', 0, current_user, NOW(), 0, true),
(max_rank + 7, '3.6', 'Create Test Organisations', 'SQL',
'V3.6__Create_Test_Organisations.sql', 0, current_user, NOW(), 0, true)
ON CONFLICT (version) DO NOTHING;
END $$;
COMMIT;
-- ─────────────────────────────────────────────────────────────────────────────
-- 3. Vérification finale
-- ─────────────────────────────────────────────────────────────────────────────
SELECT version, description, success, installed_on
FROM flyway_schema_history
ORDER BY installed_rank;
-- Vous devriez voir : ..., 2.10, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
-- V3.7 sera appliquée par Flyway au prochain démarrage du serveur.

View File

@@ -0,0 +1,215 @@
# 🧪 Guide de Tests - Kafka + WebSocket UnionFlow
## ✅ Statut Actuel
Backend démarré avec succès :
- ✅ 5 Producers Kafka connectés
- ✅ 5 Consumers Kafka connectés (group: `unionflow-websocket-server`)
- ✅ WebSocket endpoint actif sur `/ws/dashboard`
- ✅ Aucune erreur `UNKNOWN_TOPIC_OR_PARTITION`
---
## 🧪 Test 1 : Swagger UI
### Action
Ouvre ton navigateur : **http://localhost:8085/q/swagger-ui**
### Résultat attendu
- Page Swagger UI affichée
- Liste de tous les endpoints REST
- Section "Schemas" avec les DTOs
---
## 🧪 Test 2 : WebSocket - Test HTML
### Action
1. Ouvre le fichier HTML dans ton navigateur :
```
C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\scripts\kafka\test-websocket.html
```
2. Clique sur le bouton **"🔗 Connecter"**
### Résultat attendu
✅ **Status change vers "✅ Connecté"**
✅ **Message dans la console** : "Connexion WebSocket établie avec succès !"
✅ **Dans les logs backend** : Tu devrais voir :
```
INFO WebSocket connection opened from ...
```
### Test Ping/Pong
1. Clique sur **"📤 Envoyer Ping"**
2. Tu devrais recevoir un **pong** du serveur
---
## 🧪 Test 3 : Publier un Event Kafka → Vérifier Réception WebSocket
### Étape 1 : Ouvrir 3 fenêtres
1. **Fenêtre 1** : Logs backend Quarkus (déjà ouverte)
2. **Fenêtre 2** : Navigateur avec `test-websocket.html` (WebSocket connecté)
3. **Fenêtre 3** : PowerShell pour publier l'event
### Étape 2 : Publier un Event de Test
Dans PowerShell (Fenêtre 3) :
```powershell
cd C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\scripts\kafka
.\test-event.bat
```
**Ou manuellement** :
```cmd
echo {"eventType":"APPROVAL_APPROVED","timestamp":"2026-03-14T19:30:00Z","organizationId":"test-org-123","data":{"id":"test-1","amount":50000}} | docker exec -i kafka /opt/kafka/bin/kafka-console-producer.sh --topic unionflow.finance.approvals --bootstrap-server localhost:9092
```
### Résultat attendu
#### Dans les logs backend (Fenêtre 1) :
```
INFO [dev.lions.unionflow.server.messaging.KafkaEventConsumer] (smallrye-kafka-consumer-thread-1)
Received finance approval event: key=..., value={"eventType":"APPROVAL_APPROVED",...}
```
#### Dans le WebSocket HTML (Fenêtre 2) :
```
[19:30:15] RECEIVED: APPROVAL_APPROVED
{
"eventType": "APPROVAL_APPROVED",
"timestamp": "2026-03-14T19:30:00Z",
"organizationId": "test-org-123",
"data": { "id": "test-1", "amount": 50000 }
}
```
✅ **Si tu vois l'event dans le WebSocket HTML → SUCCÈS COMPLET !**
---
## 🧪 Test 4 : Vérifier les Topics Kafka
### Lister tous les messages d'un topic
```cmd
docker exec kafka /opt/kafka/bin/kafka-console-consumer.sh --topic unionflow.finance.approvals --bootstrap-server localhost:9092 --from-beginning
```
Appuie sur `Ctrl+C` pour arrêter.
### Compter les messages
```cmd
docker exec kafka /opt/kafka/bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic unionflow.finance.approvals
```
---
## 🧪 Test 5 : Application Mobile Flutter
### Lancer l'app mobile
```bash
cd C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-mobile-apps
flutter run --dart-define=ENV=dev
```
### Résultat attendu dans la console mobile
```
I/flutter (12345): ✅ WebSocket connecté
I/flutter (12345): DashboardBloc: WebSocket initialisé
I/flutter (12345): DashboardBloc: WebSocket connecté - Temps réel actif
```
### Test complet Mobile + Backend
1. **Mobile** : Ouvre le Dashboard
2. **Backend** : Publie un event via Swagger ou Kafka console
3. **Mobile** : Le dashboard devrait se rafraîchir automatiquement !
---
## 🧪 Test 6 : End-to-End avec Swagger UI
### Scénario : Approbation Finance
1. **Ouvre Swagger UI** : http://localhost:8085/q/swagger-ui
2. **Trouve l'endpoint** : `POST /api/v1/finance/approvals/{id}/approve`
3. **Exécute la requête** avec un ID de test
4. **Vérifie les logs backend** :
```
INFO KafkaEventProducer: Publishing approval event...
INFO KafkaEventConsumer: Received finance approval event...
INFO WebSocketBroadcastService: Broadcasting to X clients...
```
5. **Vérifie le WebSocket HTML** : L'event devrait apparaître !
---
## 📊 Résumé des Tests
| Test | Objectif | Status |
|------|----------|--------|
| 1. Swagger UI | Accès API REST | ⏳ À tester |
| 2. WebSocket HTML | Connexion WebSocket | ⏳ À tester |
| 3. Kafka → WebSocket | Flux complet | ⏳ À tester |
| 4. Kafka Topics | Vérification messages | ⏳ À tester |
| 5. Mobile Flutter | WebSocket mobile | ⏳ À tester |
| 6. End-to-End Swagger | Workflow complet | ⏳ À tester |
---
## 🐛 Troubleshooting
### WebSocket ne se connecte pas
**Vérifier** :
```cmd
netstat -an | findstr 8085
```
Doit afficher : `0.0.0.0:8085 ... LISTENING`
### Events Kafka non reçus
**Vérifier les consumers** :
```cmd
docker exec kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group unionflow-websocket-server
```
Doit afficher :
- LAG = 0 (tous les messages consommés)
- 5 topics assignés au group
### Backend logs
**Augmenter le niveau de log** dans `application.properties` :
```properties
quarkus.log.category."dev.lions.unionflow.server.messaging".level=DEBUG
```
---
## ✅ Succès Final
Quand tous ces tests passent :
- ✅ Backend publie events dans Kafka
- ✅ Consumers consomment events
- ✅ WebSocket broadcast aux clients
- ✅ Mobile reçoit events en temps réel
- ✅ Dashboard auto-refresh
**→ Architecture Event-Driven 100% FONCTIONNELLE ! 🎉**
---
**Commence par le Test 2 (WebSocket HTML)** - c'est le plus visuel et le plus rapide pour vérifier que tout fonctionne !

View File

@@ -0,0 +1,155 @@
# Scripts Kafka pour UnionFlow
## Problème résolu
Les erreurs `UNKNOWN_TOPIC_OR_PARTITION` apparaissent car **les topics Kafka n'existent pas encore**.
Ton Kafka actuel (conteneur `kafka` sur port 9092) fonctionne parfaitement. Il faut juste créer les 5 topics.
---
## ✅ Solution : Créer les Topics
### Option 1 : Exécuter le script (Recommandé)
#### Windows (PowerShell ou CMD)
```cmd
cd C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\scripts\kafka
create-topics.bat
```
#### Linux/Mac
```bash
cd /path/to/unionflow/scripts/kafka
chmod +x create-topics.sh
./create-topics.sh
```
### Option 2 : Commandes manuelles (si le script échoue)
Copie-colle ces commandes une par une dans PowerShell/CMD :
```cmd
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.finance.approvals --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.dashboard.stats --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.notifications.user --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.members.events --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.contributions.events --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
```
---
## 🔍 Vérification des Topics
### Lister tous les topics
```cmd
docker exec kafka /opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092
```
Résultat attendu :
```
unionflow.contributions.events
unionflow.dashboard.stats
unionflow.finance.approvals
unionflow.members.events
unionflow.notifications.user
```
### Voir les détails d'un topic
```cmd
docker exec kafka /opt/kafka/bin/kafka-topics.sh --describe --topic unionflow.finance.approvals --bootstrap-server localhost:9092
```
---
## 📊 Topics Créés
| Topic | Partitions | Replication | Usage |
|-------|------------|-------------|-------|
| `unionflow.finance.approvals` | 3 | 1 | Workflow approbations financières |
| `unionflow.dashboard.stats` | 3 | 1 | Mise à jour stats dashboard |
| `unionflow.notifications.user` | 3 | 1 | Notifications utilisateurs |
| `unionflow.members.events` | 3 | 1 | Events création/modification membres |
| `unionflow.contributions.events` | 3 | 1 | Events paiements cotisations |
**Partitions** : 3 pour parallélisme (peut augmenter en prod si besoin)
**Replication** : 1 (développement single-node)
---
## 🧪 Tester la Configuration
### 1. Publier un message de test
```cmd
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh --topic unionflow.finance.approvals --bootstrap-server localhost:9092
```
Tape un message JSON et appuie sur Entrée :
```json
{"eventType":"TEST","timestamp":"2026-03-14T19:00:00Z","data":{"message":"test"}}
```
Ctrl+C pour quitter.
### 2. Consommer les messages
```cmd
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --topic unionflow.finance.approvals --bootstrap-server localhost:9092 --from-beginning
```
Tu devrais voir le message de test. Ctrl+C pour quitter.
---
## ✅ Résultat Attendu
Après création des topics, redémarre le backend Quarkus :
```bash
cd unionflow-server-impl-quarkus
./mvnw quarkus:dev
```
Les erreurs `UNKNOWN_TOPIC_OR_PARTITION` **disparaîtront** et tu verras :
```
✅ Kafka consumers started successfully
✅ Connected to topics: unionflow.finance.approvals, unionflow.dashboard.stats, ...
```
---
## ❓ FAQ
### Puis-je supprimer mon Kafka actuel ?
**NON !** Ton Kafka actuel est parfait. Garde-le. Il manque juste les topics.
### Dois-je utiliser docker-compose ?
Non, ton setup actuel (conteneur `kafka` standalone) fonctionne très bien pour le développement.
### En production ?
En production (Kubernetes), les topics seront créés automatiquement par le backend au démarrage (via `auto.create.topics.enable=true` dans Kafka) ou via Helm charts.
---
## 🚀 Après Création des Topics
1. **Redémarrer backend Quarkus** : `./mvnw quarkus:dev`
2. **Lancer mobile** : `flutter run --dart-define=ENV=dev`
3. **Tester WebSocket** : Publier un event via Swagger UI → vérifier réception mobile
---
**Status** : ✅ Topics créés → Backend connecté → WebSocket fonctionnel

View File

@@ -0,0 +1,39 @@
@echo off
REM Script pour créer les topics Kafka pour UnionFlow
REM Exécuter depuis le répertoire unionflow/
echo ====================================
echo Création des topics Kafka UnionFlow
echo ====================================
echo.
echo Topic 1/5: unionflow.finance.approvals
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.finance.approvals --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
echo Topic 2/5: unionflow.dashboard.stats
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.dashboard.stats --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
echo Topic 3/5: unionflow.notifications.user
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.notifications.user --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
echo Topic 4/5: unionflow.members.events
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.members.events --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
echo Topic 5/5: unionflow.contributions.events
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create --topic unionflow.contributions.events --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1 --if-not-exists
echo.
echo ====================================
echo Vérification des topics créés
echo ====================================
docker exec kafka /opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092
echo.
echo ====================================
echo Détails des topics UnionFlow
echo ====================================
docker exec kafka /opt/kafka/bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 | findstr unionflow
echo.
echo ✅ Topics Kafka créés avec succès !
pause

View File

@@ -0,0 +1,62 @@
#!/bin/bash
# Script pour créer les topics Kafka pour UnionFlow
echo "===================================="
echo "Création des topics Kafka UnionFlow"
echo "===================================="
echo ""
echo "Topic 1/5: unionflow.finance.approvals"
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create \
--topic unionflow.finance.approvals \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1 \
--if-not-exists
echo "Topic 2/5: unionflow.dashboard.stats"
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create \
--topic unionflow.dashboard.stats \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1 \
--if-not-exists
echo "Topic 3/5: unionflow.notifications.user"
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create \
--topic unionflow.notifications.user \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1 \
--if-not-exists
echo "Topic 4/5: unionflow.members.events"
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create \
--topic unionflow.members.events \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1 \
--if-not-exists
echo "Topic 5/5: unionflow.contributions.events"
docker exec kafka /opt/kafka/bin/kafka-topics.sh --create \
--topic unionflow.contributions.events \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1 \
--if-not-exists
echo ""
echo "===================================="
echo "Vérification des topics créés"
echo "===================================="
docker exec kafka /opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092
echo ""
echo "===================================="
echo "Détails des topics UnionFlow"
echo "===================================="
docker exec kafka /opt/kafka/bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 | grep unionflow
echo ""
echo "✅ Topics Kafka créés avec succès !"

View File

@@ -0,0 +1,31 @@
@echo off
REM Script pour tester la publication et consommation d'events Kafka
echo ====================================
echo Test Event Kafka - UnionFlow
echo ====================================
echo.
echo 📤 Publication d'un event de test dans unionflow.finance.approvals...
echo.
REM Créer un fichier temporaire avec l'event JSON
echo {"eventType":"APPROVAL_APPROVED","timestamp":"2026-03-14T19:30:00Z","organizationId":"test-org-123","data":{"id":"test-approval-1","transactionType":"COTISATION","amount":50000,"currency":"XOF","approvedBy":"admin@test.com","approvedAt":"2026-03-14T19:30:00Z"}} > %TEMP%\kafka-test-event.json
REM Publier l'event dans Kafka
docker exec -i kafka /opt/kafka/bin/kafka-console-producer.sh --topic unionflow.finance.approvals --bootstrap-server localhost:9092 < %TEMP%\kafka-test-event.json
echo.
echo ✅ Event publié dans unionflow.finance.approvals
echo.
echo 📥 Pour consommer les events (dans une autre fenêtre CMD) :
echo docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --topic unionflow.finance.approvals --bootstrap-server localhost:9092 --from-beginning
echo.
echo 📊 Vérifie les logs du backend Quarkus :
echo Tu devrais voir : "Received finance approval event"
echo.
REM Nettoyer
del %TEMP%\kafka-test-event.json
pause

View File

@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test WebSocket UnionFlow</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
border-bottom: 3px solid #3498db;
padding-bottom: 10px;
}
.status {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-weight: bold;
}
.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.disconnected {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
font-size: 14px;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
#messages {
background-color: #2c3e50;
color: #ecf0f1;
padding: 15px;
border-radius: 4px;
height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
margin-top: 20px;
}
.event {
margin: 5px 0;
padding: 8px;
border-left: 3px solid #3498db;
background-color: #34495e;
}
.event-type {
color: #3498db;
font-weight: bold;
}
.timestamp {
color: #95a5a6;
font-size: 11px;
}
.controls {
margin: 20px 0;
}
.info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
padding: 12px;
border-radius: 4px;
margin: 15px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🔌 Test WebSocket UnionFlow</h1>
<div class="info">
<strong>URL WebSocket :</strong> ws://localhost:8085/ws/dashboard
</div>
<div id="status" class="status disconnected">
❌ Déconnecté
</div>
<div class="controls">
<button id="connectBtn" onclick="connect()">🔗 Connecter</button>
<button id="disconnectBtn" onclick="disconnect()" disabled>🔌 Déconnecter</button>
<button id="pingBtn" onclick="sendPing()" disabled>📤 Envoyer Ping</button>
<button id="clearBtn" onclick="clearMessages()">🗑️ Effacer</button>
</div>
<h3>📨 Messages reçus :</h3>
<div id="messages"></div>
</div>
<script>
let ws = null;
let reconnectInterval = null;
function updateStatus(connected) {
const statusDiv = document.getElementById('status');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const pingBtn = document.getElementById('pingBtn');
if (connected) {
statusDiv.className = 'status connected';
statusDiv.innerHTML = '✅ Connecté';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
pingBtn.disabled = false;
} else {
statusDiv.className = 'status disconnected';
statusDiv.innerHTML = '❌ Déconnecté';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
pingBtn.disabled = true;
}
}
function addMessage(type, message, data = null) {
const messagesDiv = document.getElementById('messages');
const timestamp = new Date().toLocaleTimeString('fr-FR');
let content = `<div class="event">`;
content += `<span class="timestamp">[${timestamp}]</span> `;
content += `<span class="event-type">${type}</span>: ${message}`;
if (data) {
content += `<pre style="margin: 5px 0; padding: 5px; background: #1a252f; border-radius: 3px;">${JSON.stringify(data, null, 2)}</pre>`;
}
content += `</div>`;
messagesDiv.innerHTML += content;
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function connect() {
try {
addMessage('SYSTEM', 'Tentative de connexion à ws://localhost:8085/ws/dashboard...');
ws = new WebSocket('ws://localhost:8085/ws/dashboard');
ws.onopen = function() {
updateStatus(true);
addMessage('SYSTEM', '✅ Connexion WebSocket établie avec succès !');
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
const eventType = data.type || data.eventType || 'UNKNOWN';
addMessage('RECEIVED', eventType, data);
} catch (e) {
addMessage('RECEIVED', 'Message brut', event.data);
}
};
ws.onerror = function(error) {
addMessage('ERROR', 'Erreur WebSocket', error);
};
ws.onclose = function() {
updateStatus(false);
addMessage('SYSTEM', '❌ Connexion WebSocket fermée');
};
} catch (error) {
addMessage('ERROR', 'Erreur lors de la connexion', error.message);
}
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
updateStatus(false);
addMessage('SYSTEM', 'Déconnexion manuelle');
}
}
function sendPing() {
if (ws && ws.readyState === WebSocket.OPEN) {
const ping = { type: 'ping' };
ws.send(JSON.stringify(ping));
addMessage('SENT', 'Ping envoyé', ping);
}
}
function clearMessages() {
document.getElementById('messages').innerHTML = '';
addMessage('SYSTEM', 'Messages effacés');
}
// Auto-connect au chargement
window.onload = function() {
addMessage('SYSTEM', 'Page chargée. Prêt à se connecter.');
};
</script>
</body>
</html>

View File

@@ -0,0 +1,329 @@
# Configuration Keycloak pour UnionFlow
# Crée les rôles et les comptes de test pour MUKEFI et MESKA
$ErrorActionPreference = "Stop"
# Configuration
$KEYCLOAK_URL = "http://localhost:8180"
$REALM_NAME = "unionflow"
$ADMIN_USERNAME = "admin"
$ADMIN_PASSWORD = "admin"
$TEST_PASSWORD = "Test@123"
function Write-Header {
param([string]$Text)
Write-Host ""
Write-Host "=" -NoNewline -ForegroundColor Cyan
Write-Host (" " * 68) -NoNewline
Write-Host "=" -ForegroundColor Cyan
Write-Host $Text -ForegroundColor White
Write-Host "=" -NoNewline -ForegroundColor Cyan
Write-Host (" " * 68) -NoNewline
Write-Host "=" -ForegroundColor Cyan
}
function Get-KeycloakAdminToken {
Write-Host "`n📡 Connexion à Keycloak..." -ForegroundColor Yellow
$tokenUrl = "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token"
$body = @{
client_id = "admin-cli"
username = $ADMIN_USERNAME
password = $ADMIN_PASSWORD
grant_type = "password"
}
try {
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
Write-Host "✅ Connecté à Keycloak Admin API" -ForegroundColor Green
return $response.access_token
}
catch {
Write-Host "❌ Erreur connexion Keycloak: $_" -ForegroundColor Red
Write-Host " Vérifiez que Keycloak est démarré sur $KEYCLOAK_URL" -ForegroundColor Yellow
return $null
}
}
function Get-Headers {
param([string]$Token)
return @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/json"
}
}
function Get-RealmUsers {
param([string]$Token)
$headers = Get-Headers -Token $Token
$url = "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users"
try {
return Invoke-RestMethod -Uri $url -Method Get -Headers $headers
}
catch {
Write-Host " ⚠️ Erreur récupération utilisateurs: $_" -ForegroundColor Yellow
return @()
}
}
function Remove-RealmUser {
param([string]$Token, [string]$UserId, [string]$Username)
$headers = Get-Headers -Token $Token
$url = "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$UserId"
try {
Invoke-RestMethod -Uri $url -Method Delete -Headers $headers | Out-Null
Write-Host " ✅ Supprimé: $Username" -ForegroundColor Green
return $true
}
catch {
Write-Host " ❌ Erreur suppression $Username : $_" -ForegroundColor Red
return $false
}
}
function Get-RealmRoles {
param([string]$Token)
$headers = Get-Headers -Token $Token
$url = "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles"
try {
return Invoke-RestMethod -Uri $url -Method Get -Headers $headers
}
catch {
Write-Host " ⚠️ Erreur récupération rôles: $_" -ForegroundColor Yellow
return @()
}
}
function New-RealmRole {
param([string]$Token, [string]$Name, [string]$Description)
$headers = Get-Headers -Token $Token
$url = "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles"
$body = @{
name = $Name
description = $Description
composite = $false
clientRole = $false
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body | Out-Null
Write-Host " ✅ Rôle créé: $Name" -ForegroundColor Green
return $true
}
catch {
if ($_.Exception.Response.StatusCode.value__ -eq 409) {
Write-Host " ⚠️ Rôle existe déjà: $Name" -ForegroundColor Yellow
return $true
}
Write-Host " ❌ Erreur création rôle $Name : $_" -ForegroundColor Red
return $false
}
}
function New-RealmUser {
param(
[string]$Token,
[string]$Username,
[string]$Email,
[string]$FirstName,
[string]$LastName,
[string]$Password,
[string[]]$Roles
)
$headers = Get-Headers -Token $Token
$url = "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users"
$body = @{
username = $Username
email = $Email
firstName = $FirstName
lastName = $LastName
enabled = $true
emailVerified = $true
credentials = @(
@{
type = "password"
value = $Password
temporary = $false
}
)
} | ConvertTo-Json -Depth 10
try {
$response = Invoke-WebRequest -Uri $url -Method Post -Headers $headers -Body $body
if ($response.StatusCode -eq 201) {
# Récupérer l'ID de l'utilisateur
$location = $response.Headers.Location
if ($location) {
$userId = $location.Split('/')[-1]
}
else {
# Chercher l'utilisateur par username
$users = Get-RealmUsers -Token $Token
$user = $users | Where-Object { $_.username -eq $Username }
if ($user) {
$userId = $user.id
}
}
if ($userId) {
# Assigner les rôles
Add-RolesToUser -Token $Token -UserId $userId -RoleNames $Roles
Write-Host " ✅ Utilisateur créé: $Username ($Email)" -ForegroundColor Green
return $userId
}
}
}
catch {
if ($_.Exception.Response.StatusCode.value__ -eq 409) {
Write-Host " ⚠️ Utilisateur existe déjà: $Username" -ForegroundColor Yellow
}
else {
Write-Host " ❌ Erreur création utilisateur $Username : $_" -ForegroundColor Red
}
return $null
}
}
function Add-RolesToUser {
param([string]$Token, [string]$UserId, [string[]]$RoleNames)
# Récupérer les objets de rôle
$allRoles = Get-RealmRoles -Token $Token
$rolesToAssign = $allRoles | Where-Object { $RoleNames -contains $_.name }
if ($rolesToAssign.Count -eq 0) {
return
}
$headers = Get-Headers -Token $Token
$url = "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$UserId/role-mappings/realm"
$body = $rolesToAssign | ConvertTo-Json -Depth 10 -AsArray
try {
Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body | Out-Null
Write-Host " → Rôles assignés: $($RoleNames -join ', ')" -ForegroundColor Cyan
}
catch {
Write-Host " ⚠️ Erreur assignation rôles: $_" -ForegroundColor Yellow
}
}
# ============================================================================
# MAIN
# ============================================================================
Write-Header "🔧 Configuration Keycloak pour UnionFlow"
# 1. Connexion
$token = Get-KeycloakAdminToken
if (-not $token) {
Write-Host "`n❌ Impossible de se connecter à Keycloak. Script arrêté." -ForegroundColor Red
exit 1
}
# 2. Audit de l'existant
Write-Host "`n📋 Audit de l'état actuel..." -ForegroundColor Yellow
$existingUsers = Get-RealmUsers -Token $token
$existingRoles = Get-RealmRoles -Token $token
Write-Host " • Utilisateurs existants: $($existingUsers.Count)" -ForegroundColor White
foreach ($user in $existingUsers) {
Write-Host " - $($user.username) ($($user.email))" -ForegroundColor Gray
}
Write-Host " • Rôles existants: $($existingRoles.Count)" -ForegroundColor White
foreach ($role in $existingRoles) {
Write-Host " - $($role.name)" -ForegroundColor Gray
}
# 3. Suppression des utilisateurs existants
Write-Host "`n🗑️ Suppression des utilisateurs existants..." -ForegroundColor Yellow
foreach ($user in $existingUsers) {
Remove-RealmUser -Token $token -UserId $user.id -Username $user.username
}
# 4. Création de la structure de rôles
Write-Host "`n👥 Création de la structure de rôles..." -ForegroundColor Yellow
$rolesToCreate = @(
@{name="SUPER_ADMIN"; description="Super administrateur - Accès total plateforme multi-organisations"},
@{name="ADMIN_ORGANISATION"; description="Administrateur d'une organisation - Accès total à son organisation"},
@{name="TRESORIER"; description="Trésorier - Gestion financière, comptabilité, épargne/crédit"},
@{name="SECRETAIRE"; description="Secrétaire - Gestion administrative, membres, adhésions, documents"},
@{name="RESPONSABLE_SOCIAL"; description="Responsable social - Gestion aide sociale et solidarité"},
@{name="RESPONSABLE_EVENEMENTS"; description="Responsable événements - Gestion événements et logistique"},
@{name="RESPONSABLE_CREDIT"; description="Responsable crédit - Gestion épargne/crédit (mutuelles)"},
@{name="MEMBRE_BUREAU"; description="Membre du bureau - Accès étendu consultation et actions"},
@{name="MEMBRE_ACTIF"; description="Membre actif - Consultation et actions de base"},
@{name="MEMBRE_SIMPLE"; description="Membre simple - Consultation uniquement"},
@{name="MEMBRE"; description="Rôle technique - Membre base"},
@{name="ADMIN"; description="Rôle technique - Admin base"},
@{name="USER"; description="Rôle technique - Utilisateur base"}
)
foreach ($role in $rolesToCreate) {
New-RealmRole -Token $token -Name $role.name -Description $role.description
}
# 5. Création des comptes de test
Write-Host "`n👤 Création des comptes de test..." -ForegroundColor Yellow
Write-Host " Mot de passe pour tous: $TEST_PASSWORD`n" -ForegroundColor Cyan
$usersToCreate = @(
# Super-Admin
@{username="superadmin"; email="superadmin@unionflow.test"; firstName="Super"; lastName="Admin"; roles=@("SUPER_ADMIN", "ADMIN", "USER")},
# MUKEFI
@{username="admin.mukefi"; email="admin.mukefi@unionflow.test"; firstName="Administrateur"; lastName="MUKEFI"; roles=@("ADMIN_ORGANISATION", "ADMIN", "USER")},
@{username="tresorier.mukefi"; email="tresorier.mukefi@unionflow.test"; firstName="Trésorier"; lastName="MUKEFI"; roles=@("TRESORIER", "MEMBRE", "USER")},
@{username="secretaire.mukefi"; email="secretaire.mukefi@unionflow.test"; firstName="Secrétaire"; lastName="MUKEFI"; roles=@("SECRETAIRE", "MEMBRE", "USER")},
@{username="credit.mukefi"; email="credit.mukefi@unionflow.test"; firstName="Responsable Crédit"; lastName="MUKEFI"; roles=@("RESPONSABLE_CREDIT", "MEMBRE", "USER")},
@{username="membre.mukefi"; email="membre.mukefi@unionflow.test"; firstName="Membre"; lastName="MUKEFI"; roles=@("MEMBRE_ACTIF", "MEMBRE", "USER")},
# MESKA
@{username="admin.meska"; email="admin.meska@unionflow.test"; firstName="Administrateur"; lastName="MESKA"; roles=@("ADMIN_ORGANISATION", "ADMIN", "USER")},
@{username="secretaire.meska"; email="secretaire.meska@unionflow.test"; firstName="Secrétaire"; lastName="MESKA"; roles=@("SECRETAIRE", "MEMBRE", "USER")},
@{username="social.meska"; email="social.meska@unionflow.test"; firstName="Responsable Social"; lastName="MESKA"; roles=@("RESPONSABLE_SOCIAL", "MEMBRE", "USER")},
@{username="evenements.meska"; email="evenements.meska@unionflow.test"; firstName="Responsable Événements"; lastName="MESKA"; roles=@("RESPONSABLE_EVENEMENTS", "MEMBRE", "USER")},
@{username="membre.meska"; email="membre.meska@unionflow.test"; firstName="Membre"; lastName="MESKA"; roles=@("MEMBRE_ACTIF", "MEMBRE", "USER")}
)
foreach ($user in $usersToCreate) {
New-RealmUser -Token $token -Username $user.username -Email $user.email `
-FirstName $user.firstName -lastName $user.lastName `
-Password $TEST_PASSWORD -Roles $user.roles
}
# 6. Résumé final
Write-Header "✅ Configuration Keycloak terminée avec succès !"
Write-Host "`n📊 Résumé:" -ForegroundColor Yellow
Write-Host "$($rolesToCreate.Count) rôles créés" -ForegroundColor White
Write-Host "$($usersToCreate.Count) utilisateurs créés" -ForegroundColor White
Write-Host " • Mot de passe: $TEST_PASSWORD" -ForegroundColor Cyan
Write-Host "`n👥 Comptes créés:" -ForegroundColor Yellow
Write-Host "`n 🔧 Super-Admin:" -ForegroundColor Magenta
Write-Host " → superadmin@unionflow.test" -ForegroundColor White
Write-Host "`n 🏦 MUKEFI (Mutuelle):" -ForegroundColor Magenta
Write-Host " → admin.mukefi@unionflow.test (Admin)" -ForegroundColor White
Write-Host " → tresorier.mukefi@unionflow.test (Trésorier)" -ForegroundColor White
Write-Host " → secretaire.mukefi@unionflow.test (Secrétaire)" -ForegroundColor White
Write-Host " → credit.mukefi@unionflow.test (Responsable Crédit)" -ForegroundColor White
Write-Host " → membre.mukefi@unionflow.test (Membre Actif)" -ForegroundColor White
Write-Host "`n 🤝 MESKA (Association):" -ForegroundColor Magenta
Write-Host " → admin.meska@unionflow.test (Admin)" -ForegroundColor White
Write-Host " → secretaire.meska@unionflow.test (Secrétaire)" -ForegroundColor White
Write-Host " → social.meska@unionflow.test (Responsable Social)" -ForegroundColor White
Write-Host " → evenements.meska@unionflow.test (Responsable Événements)" -ForegroundColor White
Write-Host " → membre.meska@unionflow.test (Membre Actif)" -ForegroundColor White
Write-Host "`n🌐 Accès Keycloak:" -ForegroundColor Yellow
Write-Host " • Console Admin: $KEYCLOAK_URL/admin" -ForegroundColor Cyan
Write-Host " • Realm: $REALM_NAME" -ForegroundColor Cyan
Write-Host ""

View File

@@ -0,0 +1,365 @@
#!/usr/bin/env python3
"""
Script de configuration Keycloak pour UnionFlow
Crée les rôles et les comptes de test pour MUKEFI et MESKA
"""
import requests
import json
import sys
from typing import Dict, List, Optional
# Configuration
KEYCLOAK_URL = "http://localhost:8180"
REALM_NAME = "unionflow"
ADMIN_USERNAME = "admin"
ADMIN_PASSWORD = "admin" # Modifier si différent
TEST_PASSWORD = "Test@123"
class KeycloakManager:
def __init__(self):
self.base_url = KEYCLOAK_URL
self.realm = REALM_NAME
self.token = None
def get_admin_token(self) -> bool:
"""Obtient un token admin pour l'API Keycloak"""
url = f"{self.base_url}/realms/master/protocol/openid-connect/token"
data = {
"client_id": "admin-cli",
"username": ADMIN_USERNAME,
"password": ADMIN_PASSWORD,
"grant_type": "password"
}
try:
response = requests.post(url, data=data)
response.raise_for_status()
self.token = response.json()["access_token"]
print("✅ Connecté à Keycloak Admin API")
return True
except Exception as e:
print(f"❌ Erreur connexion Keycloak: {e}")
return False
def _headers(self) -> Dict:
"""Headers pour les requêtes API"""
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def list_users(self) -> List[Dict]:
"""Liste tous les utilisateurs du realm"""
url = f"{self.base_url}/admin/realms/{self.realm}/users"
response = requests.get(url, headers=self._headers())
response.raise_for_status()
return response.json()
def delete_user(self, user_id: str) -> bool:
"""Supprime un utilisateur"""
url = f"{self.base_url}/admin/realms/{self.realm}/users/{user_id}"
response = requests.delete(url, headers=self._headers())
return response.status_code == 204
def list_roles(self) -> List[Dict]:
"""Liste tous les rôles du realm"""
url = f"{self.base_url}/admin/realms/{self.realm}/roles"
response = requests.get(url, headers=self._headers())
response.raise_for_status()
return response.json()
def create_role(self, name: str, description: str) -> bool:
"""Crée un rôle realm"""
url = f"{self.base_url}/admin/realms/{self.realm}/roles"
data = {
"name": name,
"description": description,
"composite": False,
"clientRole": False
}
try:
response = requests.post(url, headers=self._headers(), json=data)
if response.status_code == 201:
print(f" ✅ Rôle créé: {name}")
return True
elif response.status_code == 409:
print(f" ⚠️ Rôle existe déjà: {name}")
return True
else:
print(f" ❌ Erreur création rôle {name}: {response.status_code}")
return False
except Exception as e:
print(f" ❌ Exception création rôle {name}: {e}")
return False
def create_user(self, username: str, email: str, first_name: str, last_name: str,
password: str, roles: List[str], enabled: bool = True) -> Optional[str]:
"""Crée un utilisateur avec ses rôles"""
# 1. Créer l'utilisateur
url = f"{self.base_url}/admin/realms/{self.realm}/users"
data = {
"username": username,
"email": email,
"firstName": first_name,
"lastName": last_name,
"enabled": enabled,
"emailVerified": True,
"credentials": [{
"type": "password",
"value": password,
"temporary": False
}]
}
try:
response = requests.post(url, headers=self._headers(), json=data)
if response.status_code == 201:
# Récupérer l'ID de l'utilisateur créé
location = response.headers.get("Location")
user_id = location.split("/")[-1] if location else None
if not user_id:
# Chercher l'utilisateur par username
users = self.list_users()
user = next((u for u in users if u["username"] == username), None)
if user:
user_id = user["id"]
if user_id:
# 2. Assigner les rôles
self.assign_roles_to_user(user_id, roles)
print(f" ✅ Utilisateur créé: {username} ({email})")
return user_id
else:
print(f" ⚠️ Utilisateur créé mais ID non trouvé: {username}")
return None
elif response.status_code == 409:
print(f" ⚠️ Utilisateur existe déjà: {username}")
# Récupérer l'ID et mettre à jour les rôles
users = self.list_users()
user = next((u for u in users if u["username"] == username), None)
if user:
self.assign_roles_to_user(user["id"], roles)
return None
else:
print(f" ❌ Erreur création utilisateur {username}: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f" ❌ Exception création utilisateur {username}: {e}")
return None
def assign_roles_to_user(self, user_id: str, role_names: List[str]):
"""Assigne des rôles à un utilisateur"""
# Récupérer les objets de rôle
all_roles = self.list_roles()
roles_to_assign = [r for r in all_roles if r["name"] in role_names]
if not roles_to_assign:
return
url = f"{self.base_url}/admin/realms/{self.realm}/users/{user_id}/role-mappings/realm"
response = requests.post(url, headers=self._headers(), json=roles_to_assign)
if response.status_code in [204, 200]:
print(f" → Rôles assignés: {', '.join(role_names)}")
else:
print(f" ⚠️ Erreur assignation rôles: {response.status_code}")
def main():
print("=" * 70)
print("🔧 Configuration Keycloak pour UnionFlow")
print("=" * 70)
kc = KeycloakManager()
# 1. Connexion
print("\n📡 Connexion à Keycloak...")
if not kc.get_admin_token():
print("\n❌ Impossible de se connecter à Keycloak.")
print(" Vérifiez que Keycloak est démarré sur http://localhost:8180")
print(" Credentials par défaut: admin / admin")
sys.exit(1)
# 2. Audit de l'existant
print("\n📋 Audit de l'état actuel...")
existing_users = kc.list_users()
existing_roles = kc.list_roles()
print(f" • Utilisateurs existants: {len(existing_users)}")
for user in existing_users:
print(f" - {user['username']} ({user.get('email', 'no email')})")
print(f" • Rôles existants: {len(existing_roles)}")
for role in existing_roles:
print(f" - {role['name']}")
# 3. Suppression des utilisateurs existants
print("\n🗑️ Suppression des utilisateurs existants...")
for user in existing_users:
if kc.delete_user(user["id"]):
print(f" ✅ Supprimé: {user['username']}")
else:
print(f" ❌ Erreur suppression: {user['username']}")
# 4. Création de la structure de rôles
print("\n👥 Création de la structure de rôles...")
roles_to_create = [
("SUPER_ADMIN", "Super administrateur - Accès total plateforme multi-organisations"),
("ADMIN_ORGANISATION", "Administrateur d'une organisation - Accès total à son organisation"),
("TRESORIER", "Trésorier - Gestion financière, comptabilité, épargne/crédit"),
("SECRETAIRE", "Secrétaire - Gestion administrative, membres, adhésions, documents"),
("RESPONSABLE_SOCIAL", "Responsable social - Gestion aide sociale et solidarité"),
("RESPONSABLE_EVENEMENTS", "Responsable événements - Gestion événements et logistique"),
("RESPONSABLE_CREDIT", "Responsable crédit - Gestion épargne/crédit (mutuelles)"),
("MEMBRE_BUREAU", "Membre du bureau - Accès étendu consultation et actions"),
("MEMBRE_ACTIF", "Membre actif - Consultation et actions de base"),
("MEMBRE_SIMPLE", "Membre simple - Consultation uniquement"),
("MEMBRE", "Rôle technique - Membre base"),
("ADMIN", "Rôle technique - Admin base"),
("USER", "Rôle technique - Utilisateur base")
]
for role_name, description in roles_to_create:
kc.create_role(role_name, description)
# 5. Création des comptes de test
print("\n👤 Création des comptes de test...")
users_to_create = [
# Super-Admin
{
"username": "superadmin",
"email": "superadmin@unionflow.test",
"first_name": "Super",
"last_name": "Admin",
"roles": ["SUPER_ADMIN", "ADMIN", "USER"]
},
# MUKEFI (Mutuelle d'épargne et de crédit)
{
"username": "admin.mukefi",
"email": "admin.mukefi@unionflow.test",
"first_name": "Administrateur",
"last_name": "MUKEFI",
"roles": ["ADMIN_ORGANISATION", "ADMIN", "USER"]
},
{
"username": "tresorier.mukefi",
"email": "tresorier.mukefi@unionflow.test",
"first_name": "Trésorier",
"last_name": "MUKEFI",
"roles": ["TRESORIER", "MEMBRE", "USER"]
},
{
"username": "secretaire.mukefi",
"email": "secretaire.mukefi@unionflow.test",
"first_name": "Secrétaire",
"last_name": "MUKEFI",
"roles": ["SECRETAIRE", "MEMBRE", "USER"]
},
{
"username": "credit.mukefi",
"email": "credit.mukefi@unionflow.test",
"first_name": "Responsable Crédit",
"last_name": "MUKEFI",
"roles": ["RESPONSABLE_CREDIT", "MEMBRE", "USER"]
},
{
"username": "membre.mukefi",
"email": "membre.mukefi@unionflow.test",
"first_name": "Membre",
"last_name": "MUKEFI",
"roles": ["MEMBRE_ACTIF", "MEMBRE", "USER"]
},
# MESKA (Association)
{
"username": "admin.meska",
"email": "admin.meska@unionflow.test",
"first_name": "Administrateur",
"last_name": "MESKA",
"roles": ["ADMIN_ORGANISATION", "ADMIN", "USER"]
},
{
"username": "secretaire.meska",
"email": "secretaire.meska@unionflow.test",
"first_name": "Secrétaire",
"last_name": "MESKA",
"roles": ["SECRETAIRE", "MEMBRE", "USER"]
},
{
"username": "social.meska",
"email": "social.meska@unionflow.test",
"first_name": "Responsable Social",
"last_name": "MESKA",
"roles": ["RESPONSABLE_SOCIAL", "MEMBRE", "USER"]
},
{
"username": "evenements.meska",
"email": "evenements.meska@unionflow.test",
"first_name": "Responsable Événements",
"last_name": "MESKA",
"roles": ["RESPONSABLE_EVENEMENTS", "MEMBRE", "USER"]
},
{
"username": "membre.meska",
"email": "membre.meska@unionflow.test",
"first_name": "Membre",
"last_name": "MESKA",
"roles": ["MEMBRE_ACTIF", "MEMBRE", "USER"]
}
]
print(f"\n📝 Création de {len(users_to_create)} comptes utilisateurs...")
print(f" Mot de passe pour tous: {TEST_PASSWORD}\n")
for user_data in users_to_create:
kc.create_user(
username=user_data["username"],
email=user_data["email"],
first_name=user_data["first_name"],
last_name=user_data["last_name"],
password=TEST_PASSWORD,
roles=user_data["roles"]
)
# 6. Résumé final
print("\n" + "=" * 70)
print("✅ Configuration Keycloak terminée avec succès !")
print("=" * 70)
print("\n📊 Résumé:")
print(f"{len(roles_to_create)} rôles créés")
print(f"{len(users_to_create)} utilisateurs créés")
print(f" • Mot de passe: {TEST_PASSWORD}")
print("\n👥 Comptes créés:")
print("\n 🔧 Super-Admin:")
print(" → superadmin@unionflow.test")
print("\n 🏦 MUKEFI (Mutuelle):")
print(" → admin.mukefi@unionflow.test (Admin)")
print(" → tresorier.mukefi@unionflow.test (Trésorier)")
print(" → secretaire.mukefi@unionflow.test (Secrétaire)")
print(" → credit.mukefi@unionflow.test (Responsable Crédit)")
print(" → membre.mukefi@unionflow.test (Membre Actif)")
print("\n 🤝 MESKA (Association):")
print(" → admin.meska@unionflow.test (Admin)")
print(" → secretaire.meska@unionflow.test (Secrétaire)")
print(" → social.meska@unionflow.test (Responsable Social)")
print(" → evenements.meska@unionflow.test (Responsable Événements)")
print(" → membre.meska@unionflow.test (Membre Actif)")
print("\n🌐 Accès Keycloak:")
print(f" • Console Admin: {KEYCLOAK_URL}/admin")
print(f" • Realm: {REALM_NAME}")
print("\n")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,189 @@
#!/bin/bash
# Configuration Keycloak pour UnionFlow
# Crée les rôles et les comptes de test pour MUKEFI et MESKA
set -e
# Configuration
KEYCLOAK_URL="http://localhost:8180"
REALM_NAME="unionflow"
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="admin"
TEST_PASSWORD="Test@123"
echo "========================================================================"
echo "Configuration Keycloak pour UnionFlow"
echo "========================================================================"
# 1. Obtenir le token admin
echo ""
echo "📡 Connexion à Keycloak..."
TOKEN_RESPONSE=$(curl -s -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=$ADMIN_USERNAME" \
-d "password=$ADMIN_PASSWORD" \
-d "grant_type=password")
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)
if [ -z "$ACCESS_TOKEN" ]; then
echo "❌ Erreur: Impossible d'obtenir le token admin"
exit 1
fi
echo "✅ Connecté à Keycloak Admin API"
# 2. Lister et supprimer les utilisateurs existants
echo ""
echo "📋 Audit et nettoyage des utilisateurs existants..."
USERS=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users" \
-H "Authorization: Bearer $ACCESS_TOKEN")
echo "$USERS" | grep -o '"username":"[^"]*' | cut -d'"' -f4 | while read username; do
USER_ID=$(echo "$USERS" | grep -B5 "\"username\":\"$username\"" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
if [ ! -z "$USER_ID" ]; then
curl -s -X DELETE "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USER_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"
echo " ✅ Supprimé: $username"
fi
done
# 3. Créer les rôles
echo ""
echo "👥 Création de la structure de rôles..."
declare -a ROLES=(
"SUPER_ADMIN|Super administrateur - Accès total plateforme"
"ADMIN_ORGANISATION|Administrateur organisation - Accès total"
"TRESORIER|Trésorier - Gestion financière et comptabilité"
"SECRETAIRE|Secrétaire - Gestion administrative et membres"
"RESPONSABLE_SOCIAL|Responsable social - Aide sociale"
"RESPONSABLE_EVENEMENTS|Responsable événements - Événements"
"RESPONSABLE_CREDIT|Responsable crédit - Épargne et crédit"
"MEMBRE_BUREAU|Membre du bureau - Accès étendu"
"MEMBRE_ACTIF|Membre actif - Accès de base"
"MEMBRE_SIMPLE|Membre simple - Consultation uniquement"
"MEMBRE|Rôle technique - Membre base"
"ADMIN|Rôle technique - Admin base"
"USER|Rôle technique - Utilisateur base"
)
for role_data in "${ROLES[@]}"; do
ROLE_NAME=$(echo $role_data | cut -d'|' -f1)
ROLE_DESC=$(echo $role_data | cut -d'|' -f2)
curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$ROLE_NAME\",\"description\":\"$ROLE_DESC\"}" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " ✅ Rôle créé: $ROLE_NAME"
else
echo " ⚠️ Rôle existe ou erreur: $ROLE_NAME"
fi
done
# Fonction pour créer un utilisateur
create_user() {
local USERNAME=$1
local EMAIL=$2
local FIRSTNAME=$3
local LASTNAME=$4
shift 4
local ROLES=("$@")
# Créer l'utilisateur
curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"username\":\"$USERNAME\",
\"email\":\"$EMAIL\",
\"firstName\":\"$FIRSTNAME\",
\"lastName\":\"$LASTNAME\",
\"enabled\":true,
\"emailVerified\":true,
\"credentials\":[{\"type\":\"password\",\"value\":\"$TEST_PASSWORD\",\"temporary\":false}]
}"
# Récupérer l'ID de l'utilisateur
sleep 1
USER_ID=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users?username=$USERNAME" \
-H "Authorization: Bearer $ACCESS_TOKEN" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -1)
if [ ! -z "$USER_ID" ]; then
# Assigner les rôles
for ROLE in "${ROLES[@]}"; do
# Récupérer les détails du rôle
ROLE_DATA=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM_NAME/roles/$ROLE" \
-H "Authorization: Bearer $ACCESS_TOKEN")
curl -s -X POST "$KEYCLOAK_URL/admin/realms/$REALM_NAME/users/$USER_ID/role-mappings/realm" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "[$ROLE_DATA]" > /dev/null 2>&1
done
echo " ✅ Utilisateur créé: $USERNAME ($EMAIL) - Rôles: ${ROLES[*]}"
else
echo " ❌ Erreur création: $USERNAME"
fi
}
# 4. Créer les utilisateurs
echo ""
echo "👤 Création des comptes de test..."
echo " Mot de passe pour tous: $TEST_PASSWORD"
echo ""
# Super-Admin
create_user "superadmin" "superadmin@unionflow.test" "Super" "Admin" "SUPER_ADMIN" "ADMIN" "USER"
# MUKEFI (Mutuelle)
create_user "admin.mukefi" "admin.mukefi@unionflow.test" "Administrateur" "MUKEFI" "ADMIN_ORGANISATION" "ADMIN" "USER"
create_user "tresorier.mukefi" "tresorier.mukefi@unionflow.test" "Trésorier" "MUKEFI" "TRESORIER" "MEMBRE" "USER"
create_user "secretaire.mukefi" "secretaire.mukefi@unionflow.test" "Secrétaire" "MUKEFI" "SECRETAIRE" "MEMBRE" "USER"
create_user "credit.mukefi" "credit.mukefi@unionflow.test" "Responsable Crédit" "MUKEFI" "RESPONSABLE_CREDIT" "MEMBRE" "USER"
create_user "membre.mukefi" "membre.mukefi@unionflow.test" "Membre" "MUKEFI" "MEMBRE_ACTIF" "MEMBRE" "USER"
# MESKA (Association)
create_user "admin.meska" "admin.meska@unionflow.test" "Administrateur" "MESKA" "ADMIN_ORGANISATION" "ADMIN" "USER"
create_user "secretaire.meska" "secretaire.meska@unionflow.test" "Secrétaire" "MESKA" "SECRETAIRE" "MEMBRE" "USER"
create_user "social.meska" "social.meska@unionflow.test" "Responsable Social" "MESKA" "RESPONSABLE_SOCIAL" "MEMBRE" "USER"
create_user "evenements.meska" "evenements.meska@unionflow.test" "Responsable Événements" "MESKA" "RESPONSABLE_EVENEMENTS" "MEMBRE" "USER"
create_user "membre.meska" "membre.meska@unionflow.test" "Membre" "MESKA" "MEMBRE_ACTIF" "MEMBRE" "USER"
echo ""
echo "========================================================================"
echo "✅ Configuration Keycloak terminée avec succès !"
echo "========================================================================"
echo ""
echo "📊 Résumé:"
echo " • 10 rôles créés"
echo " • 11 utilisateurs créés"
echo " • Mot de passe: $TEST_PASSWORD"
echo ""
echo "👥 Comptes créés:"
echo ""
echo " 🔧 Super-Admin:"
echo " → superadmin@unionflow.test"
echo ""
echo " 🏦 MUKEFI (Mutuelle):"
echo " → admin.mukefi@unionflow.test (Admin)"
echo " → tresorier.mukefi@unionflow.test (Trésorier)"
echo " → secretaire.mukefi@unionflow.test (Secrétaire)"
echo " → credit.mukefi@unionflow.test (Responsable Crédit)"
echo " → membre.mukefi@unionflow.test (Membre Actif)"
echo ""
echo " 🤝 MESKA (Association):"
echo " → admin.meska@unionflow.test (Admin)"
echo " → secretaire.meska@unionflow.test (Secrétaire)"
echo " → social.meska@unionflow.test (Responsable Social)"
echo " → evenements.meska@unionflow.test (Responsable Événements)"
echo " → membre.meska@unionflow.test (Membre Actif)"
echo ""
echo "🌐 Accès Keycloak:"
echo " • Console Admin: $KEYCLOAK_URL/admin"
echo " • Realm: $REALM_NAME"
echo ""

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Test différentes combinaisons de login
KEYCLOAK_URL="http://localhost:8180"
CLIENT_ID="unionflow-server"
CLIENT_SECRET="unionflow-secret-2025"
echo "Test 1: username=superadmin"
curl -s -X POST "$KEYCLOAK_URL/realms/unionflow/protocol/openid-connect/token" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "username=superadmin" \
-d "password=Test@123" \
-d "grant_type=password" | head -c 200
echo ""
echo "Test 2: username=admin.mukefi"
curl -s -X POST "$KEYCLOAK_URL/realms/unionflow/protocol/openid-connect/token" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "username=admin.mukefi" \
-d "password=Test@123" \
-d "grant_type=password" | grep -o '"access_token"' | head -1
echo ""

View File

@@ -0,0 +1,196 @@
#!/bin/bash
# Script de vérification de la configuration Keycloak pour UnionFlow
# Vérifie que les rôles sont bien configurés et mappés dans les tokens
set -e
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8180}"
REALM="unionflow"
ADMIN_USER="${KEYCLOAK_ADMIN:-admin}"
ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
CLIENT_ID_NAME="unionflow-client"
echo "======================================"
echo " Vérification Keycloak - UnionFlow"
echo "======================================"
echo ""
echo "Keycloak URL: $KEYCLOAK_URL"
echo "Realm: $REALM"
echo ""
# 1. Obtenir token admin
echo "[1/6] Obtention du token admin..."
ADMIN_TOKEN=$(curl -s -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=$ADMIN_USER" \
-d "password=$ADMIN_PASSWORD" \
-d "grant_type=password" 2>/dev/null | jq -r '.access_token')
if [ "$ADMIN_TOKEN" == "null" ] || [ -z "$ADMIN_TOKEN" ]; then
echo "❌ ERREUR: Impossible d'obtenir le token admin"
echo " Vérifiez vos credentials et que Keycloak est accessible sur $KEYCLOAK_URL"
exit 1
fi
echo "✅ Token admin obtenu"
echo ""
# 2. Lister les realm roles
echo "[2/6] Vérification des realm roles..."
REALM_ROLES=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM/roles" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null | jq -r '.[].name')
EXPECTED_ROLES=(
"SUPER_ADMIN"
"ADMIN_ORGANISATION"
"TRESORIER"
"SECRETAIRE"
"RESPONSABLE_SOCIAL"
"RESPONSABLE_EVENEMENTS"
"RESPONSABLE_CREDIT"
"MEMBRE_BUREAU"
"MEMBRE_ACTIF"
"MEMBRE_SIMPLE"
)
echo "Rôles personnalisés trouvés:"
MISSING_ROLES=()
for role in "${EXPECTED_ROLES[@]}"; do
if echo "$REALM_ROLES" | grep -q "^$role$"; then
echo "$role"
else
echo "$role (MANQUANT)"
MISSING_ROLES+=("$role")
fi
done
echo ""
if [ ${#MISSING_ROLES[@]} -gt 0 ]; then
echo "⚠️ WARNING: ${#MISSING_ROLES[@]} rôle(s) manquant(s)"
echo " Exécutez d'abord: unionflow/scripts/keycloak-setup.sh"
echo ""
fi
# 3. Vérifier le client unionflow-client
echo "[3/6] Vérification du client '$CLIENT_ID_NAME'..."
CLIENT_UUID=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM/clients?clientId=$CLIENT_ID_NAME" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null | jq -r '.[0].id')
if [ "$CLIENT_UUID" == "null" ] || [ -z "$CLIENT_UUID" ]; then
echo "❌ ERREUR: Client '$CLIENT_ID_NAME' non trouvé dans le realm $REALM"
exit 1
fi
echo "✅ Client trouvé: $CLIENT_ID_NAME (UUID: $CLIENT_UUID)"
echo ""
# 4. Vérifier les rôles de admin.mukefi@unionflow.test
echo "[4/6] Vérification des rôles de 'admin.mukefi@unionflow.test'..."
USER_ID=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM/users?username=admin.mukefi@unionflow.test" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null | jq -r '.[0].id')
if [ "$USER_ID" == "null" ] || [ -z "$USER_ID" ]; then
echo "❌ ERREUR: Utilisateur 'admin.mukefi@unionflow.test' non trouvé"
echo " Exécutez d'abord: unionflow/scripts/keycloak-setup.sh"
exit 1
fi
USER_ROLES=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM/users/$USER_ID/role-mappings/realm" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null | jq -r '.[].name')
echo "Rôles assignés à admin.mukefi@unionflow.test:"
if echo "$USER_ROLES" | grep -q "ADMIN_ORGANISATION"; then
echo "$USER_ROLES" | while read role; do
echo "$role"
done
else
echo " ❌ ADMIN_ORGANISATION (MANQUANT)"
echo " $USER_ROLES"
echo ""
echo "⚠️ WARNING: Le rôle ADMIN_ORGANISATION n'est pas assigné à cet utilisateur"
fi
echo ""
# 5. Vérifier le client scope 'roles'
echo "[5/6] Vérification du client scope 'roles'..."
ROLES_SCOPE=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM/client-scopes" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null | jq -r '.[] | select(.name=="roles") | .name')
if [ "$ROLES_SCOPE" == "roles" ]; then
echo "✅ Client scope 'roles' trouvé"
else
echo "❌ ERREUR: Client scope 'roles' non trouvé"
echo " Ce scope est nécessaire pour inclure les rôles dans le token"
fi
echo ""
# 6. Vérifier les mappers du client
echo "[6/6] Vérification des protocol mappers du client..."
MAPPERS=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/$REALM/clients/$CLIENT_UUID/protocol-mappers/models" \
-H "Authorization: Bearer $ADMIN_TOKEN" 2>/dev/null)
REALM_ROLE_MAPPER=$(echo "$MAPPERS" | jq -r '.[] | select(.protocolMapper=="oidc-usermodel-realm-role-mapper") | .name')
if [ -n "$REALM_ROLE_MAPPER" ]; then
echo "✅ Realm role mapper trouvé: '$REALM_ROLE_MAPPER'"
# Vérifier la configuration du mapper
MAPPER_CONFIG=$(echo "$MAPPERS" | jq -r '.[] | select(.protocolMapper=="oidc-usermodel-realm-role-mapper")')
TOKEN_CLAIM=$(echo "$MAPPER_CONFIG" | jq -r '.config["claim.name"]')
ADD_TO_ID_TOKEN=$(echo "$MAPPER_CONFIG" | jq -r '.config["id.token.claim"]')
ADD_TO_ACCESS_TOKEN=$(echo "$MAPPER_CONFIG" | jq -r '.config["access.token.claim"]')
echo " - Token claim name: $TOKEN_CLAIM"
echo " - Add to ID token: $ADD_TO_ID_TOKEN"
echo " - Add to access token: $ADD_TO_ACCESS_TOKEN"
if [ "$ADD_TO_ID_TOKEN" != "true" ]; then
echo " ⚠️ WARNING: 'Add to ID token' devrait être 'true'"
fi
if [ "$ADD_TO_ACCESS_TOKEN" != "true" ]; then
echo " ⚠️ WARNING: 'Add to access token' devrait être 'true'"
fi
else
echo "❌ ERREUR: Aucun mapper de type 'oidc-usermodel-realm-role-mapper' trouvé"
echo " Les rôles ne seront PAS inclus dans les tokens !"
echo ""
echo " Pour corriger, dans Keycloak Admin:"
echo " 1. Allez dans Clients > $CLIENT_ID_NAME > Client scopes"
echo " 2. Cliquez sur le scope dédié (unionflow-client-dedicated)"
echo " 3. Add mapper > By configuration > User Realm Role"
echo " 4. Name: realm roles"
echo " 5. Token Claim Name: roles"
echo " 6. Add to ID token: ON"
echo " 7. Add to access token: ON"
fi
echo ""
# Résumé
echo "======================================"
echo " RÉSUMÉ"
echo "======================================"
if [ ${#MISSING_ROLES[@]} -eq 0 ] && [ -n "$REALM_ROLE_MAPPER" ] && echo "$USER_ROLES" | grep -q "ADMIN_ORGANISATION"; then
echo "✅ Configuration Keycloak OK"
echo ""
echo "Si les rôles n'apparaissent toujours pas dans le token:"
echo "1. Vérifiez que 'roles' est dans quarkus.oidc.authentication.scopes"
echo "2. Redémarrez le frontend Quarkus"
echo "3. Déconnectez-vous et reconnectez-vous dans l'application"
else
echo "❌ Configuration Keycloak INCOMPLÈTE"
echo ""
if [ ${#MISSING_ROLES[@]} -gt 0 ]; then
echo "- Rôles manquants: ${MISSING_ROLES[@]}"
fi
if [ -z "$REALM_ROLE_MAPPER" ]; then
echo "- Mapper de rôles manquant"
fi
if ! echo "$USER_ROLES" | grep -q "ADMIN_ORGANISATION"; then
echo "- Rôle ADMIN_ORGANISATION non assigné à admin.mukefi@unionflow.test"
fi
echo ""
echo "Exécutez: unionflow/scripts/keycloak-setup.sh"
fi
echo ""