Aller au contenu

Story CP-003 : Entité User + Flux d'invitation

Module : control-plane Slice : Slice 3 from architecture Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 3 Depends on : CP-001, CP-002 Estimated complexity : M


Objectif

Construire les fondations du module identité : entité User, contrôle d'accès basé sur les rôles avec 5 rôles par défaut, flux d'invitation d'utilisateurs (nouvel utilisateur ou utilisateur déjà présent sur la plateforme), et appartenance multi-tenant des utilisateurs. Ce slice crée le modèle de données User, Role, Permission et le processus d'intégration des utilisateurs dans un tenant par invitation. La résolution des permissions et les améliorations UI sont dans CP-014 et CP-015.


Périmètre Backend

Entités & Records

Entité User (base plateforme — cross-tenant) :

Champ Type Contraintes
id UUID v7 PK, généré côté serveur
email String NOT NULL, unique globalement, max 254
first_name String NOT NULL, min 2, max 100
last_name String NOT NULL, min 2, max 100
phone String Nullable, max 15
status UserStatus enum NOT NULL
home_tenant_id UUID NOT NULL, FK vers Tenant
created_date Instant NOT NULL
last_login_date Instant Nullable

Enum UserStatus :

public enum UserStatus { ACTIVE, DISABLED, INVITED }

Entité UserTenantMembership (base plateforme) :

Champ Type Contraintes
id UUID v7 PK
user_id UUID NOT NULL, FK vers User
tenant_id UUID NOT NULL, FK vers Tenant
joined_date Instant NOT NULL

Table de jointure MembershipRole (base plateforme) :

Champ Type Contraintes
membership_id UUID FK vers UserTenantMembership
role_id UUID FK vers Role

Entité Role (base plateforme, globale — 5 rôles par défaut partagés entre tous les tenants) :

Champ Type Contraintes
id UUID v7 PK
name String NOT NULL, max 100
code String NOT NULL (DG, RH_MANAGER, COMPTABLE, EMPLOYE, MANAGER)
is_system boolean NOT NULL, défaut true
created_date Instant NOT NULL

Entité Permission (base plateforme, données de référence) :

Champ Type Contraintes
id UUID v7 PK
code String NOT NULL, unique (ex. "cp.employee.create")
module_code String NOT NULL (ex. "CP", "ABSMGT")
description String NOT NULL

Table de jointure RolePermission :

Champ Type Contraintes
role_id UUID FK vers Role
permission_id UUID FK vers Permission

Rôles par défaut et permissions (initialisés par tenant — depuis PRD FR-CP-034) :

Code rôle Nom du rôle Permissions clés Périmètre
DG Directeur Général Accès complet à tous les modules actifs du tenant Tenant entier
RH_MANAGER Responsable RH Gestion employés, absences, présences, supervision paie Tenant entier
COMPTABLE Comptable Budget, engagements, notes de frais, paie (lecture), comptabilité Tenant entier
EMPLOYE Employé Libre-service : ses propres congés, ses propres bulletins, son propre profil (lecture), pointage Données personnelles uniquement
MANAGER Manager (Chef de Service) Approbations équipe : approuver les congés de ses subordonnés directs, visualiser les données équipe Subordonnés directs

Format des permissions : {module_code}.{entity}.{action} — format composite selon la décision D4 (ex. cp.employee.create, absence.leave_request.approve). Chaque permission est atomique et composite — la chaîne complète est le code de permission stocké dans l'entité Permission. Pas de support des wildcards (cp.*) — les permissions sont explicites et énumérées.

DTOs :

public record UserResponse(UUID id, String email, String firstName, String lastName, String phone, UserStatus status) {}

public record UserListResponse(List<UserResponse> users, int total, int activeCount) {}

public record InviteUserRequest(
    @NotBlank String email,
    @NotEmpty List<String> roleCodes  // ex. ["EMPLOYE", "MANAGER"]
) {}

Remarque : MyPermissionsResponse, MyTenantsResponse, TenantMembershipDto sont définis dans CP-014 et CP-011 respectivement.

Couche Repository

  • UserRepository.java — Platform-scoped :
  • findById(UUID) → Optional<User>
  • findByEmail(String) → Optional<User>
  • existsByEmail(String) → boolean
  • UserTenantMembershipRepository.java — Platform-scoped :
  • findByUserIdAndTenantId(UUID, UUID) → Optional<UserTenantMembership>
  • findByUserId(UUID) → List<UserTenantMembership>
  • findByTenantId(UUID) → List<UserTenantMembership>
  • RoleRepository.java — Platform-scoped :
  • findByTenantIdAndCode(UUID, String) → Optional<Role>
  • findByTenantId(UUID) → List<Role>
  • PermissionRepository.java — Platform-scoped :
  • findByRoleIds(Set<UUID>) → List<Permission>

Couche Service

UserService.java : - listUsers(UUID tenantId) → UserListResponse — Liste tous les utilisateurs du tenant courant avec leurs rôles et statuts - inviteUser(UUID tenantId, InviteUserRequest) → UserResponse — Flux d'invitation : 1. Vérifier si l'email existe globalement 2. SI nouvel utilisateur : créer User (status=INVITED), créer l'appartenance, créer l'utilisateur Keycloak, envoyer l'email d'invitation 3. SI utilisateur existant (dans un autre tenant) : créer uniquement l'appartenance, envoyer la notification "ajouté à l'entreprise" 4. Publier l'événement UserInvited

Remarque : PermissionService et PermissionCacheService sont dans CP-014.

KeycloakAdminService.java : - createUser(String email, String firstName, String lastName) → void — Crée l'utilisateur dans le realm Keycloak - sendPasswordSetupEmail(String email) → void — Déclenche l'email Keycloak "définir le mot de passe" - sendPasswordResetEmail(String email) → void — Déclenche le flux de réinitialisation de mot de passe Keycloak - Utilise l'API REST Admin Keycloak avec les credentials du compte de service

Endpoints API

Méthode Chemin Corps de requête Réponse Auth
GET /api/v1/users UserListResponse ROLE_DG, ROLE_RH_MANAGER
POST /api/v1/users/invite InviteUserRequest UserResponse ROLE_DG, ROLE_RH_MANAGER

Remarque : GET /api/v1/auth/my-permissions est dans CP-014. GET /api/v1/auth/my-tenants et POST /api/v1/auth/switch-tenant sont dans CP-011.

Règles de validation

  • email : format email valide, max 254 caractères
  • roleCodes : tous doivent être des codes de rôle valides pour le tenant
  • Impossible d'inviter un utilisateur déjà membre de ce tenant
  • Impossible de retirer le dernier DG d'un tenant

Considérations multi-tenant

  • L'entité User est dans la base plateforme (cross-tenant — un utilisateur peut appartenir à plusieurs tenants)
  • UserTenantMembership est dans la base plateforme (jointure cross-tenant)
  • Les rôles sont globaux à la plateforme dans la base plateforme (5 rôles par défaut partagés par tous les tenants, liés via UserTenantMembership.roles[])
  • La résolution des permissions (permissions rôle ∩ modules actifs ∩ statut tenant) est dans CP-014

Migrations Flyway

  • V002__create_users_roles_permissions.sql — Base plateforme : tables users, user_tenant_memberships, membership_roles, roles, permissions, role_permissions + initialisation des permissions par défaut + initialisation des rôles par tenant existant

Périmètre Frontend

Pages & Routes

  • /admin/users — Page liste de gestion des utilisateurs
  • Flux d'invitation : bottom sheet (mobile) / panneau latéral (bureau) depuis la page de gestion des utilisateurs

Composants

  • UserManagementPage.tsx — Liste des utilisateurs du tenant avec badges de statut
  • UserCard.tsx — Élément de liste utilisateur (vue mobile)
  • InviteUserSheet.tsx — Bottom sheet pour l'invitation d'un utilisateur (email + sélection de rôle)
  • RoleSelector.tsx — Groupe radio pour la sélection du rôle avec descriptions

États UI (TOUS REQUIS)

Page Gestion des utilisateurs :

État Comportement Texte en Français
Loading Skeleton : 5 espaces réservés de carte utilisateur
Empty Illustration + message + bouton CTA "Invitez vos collaborateurs pour qu'ils puissent accéder à Papillon." CTA : "Inviter un utilisateur"
Error Toast rouge "Une erreur est survenue. Veuillez réessayer."
Offline Message hors ligne pleine page (gestion utilisateurs nécessite la connectivité) "Gestion des utilisateurs non disponible hors ligne."
Success (invite) Toast vert "Invitation envoyée à {email}"

Comportement responsive

  • 360px (mobile) : Liste de cartes utilisateurs. Chaque carte affiche : avatar/icône, nom (ou email si INVITED), badge de rôle, point de statut. Bouton "+" dans l'en-tête pour inviter.
  • 768px (tablette) : Même layout de cartes mais plus large.
  • 1024px+ (bureau) : Vue tableau : Email, Nom, Rôle, Statut, Actions (re-invitation si expiré).

Interactions

  • Flux d'invitation : Appui sur "+" → bottom sheet avec champ email + sélection de rôle radio → "Envoyer l'invitation"
  • Re-invitation : Pour les invitations expirées, afficher le lien "Expiré — Renvoyer →" sur la carte
  • Descriptions des rôles affichées en ligne dans le sélecteur radio :

Remarque : L'UX de détection d'utilisateur multi-tenant (message d'info pour les utilisateurs déjà sur la plateforme) est dans CP-015. - DG : "Accès complet" - RH Manager : "Gestion employés, congés, paie" - Comptable : "Budget, engagements, notes de frais" - Employé : "Libre-service uniquement" - Manager : "Approbations équipe"


Critères d'acceptation

AC-017: List users for tenant
Given I am authenticated as DG of tenant "Acme SARL" with 3 users
When I call GET /api/v1/users
Then I receive all 3 users with their roles and statuses
And the response includes total=3, activeCount=2 (if 1 is INVITED)

AC-018: Invite new user
Given I am authenticated as DG
When I call POST /api/v1/users/invite with email="[email protected]" and roleCodes=["EMPLOYE"]
Then a user is created with status INVITED
And a membership is created linking the user to my tenant
And Keycloak receives a createUser call
And an invitation email is sent

AC-019: Invite existing user from another tenant
Given a user "[email protected]" exists with membership in tenant "Pharma Plus"
When I (DG of "Acme SARL") invite "[email protected]" with role COMPTABLE
Then a new membership is created for "Acme SARL" (no new User created)
And the user is notified "Vous avez été ajouté à l'entreprise Acme SARL"
And the user sees "Acme SARL" in their tenant switcher

ACs déplacés : AC-020, AC-021, AC-022 → CP-014. AC-023 → CP-011. AC-024, AC-025 → CP-015.


Règles OHADA & Réglementaires

  • Loi 2013-450 (Art. 36) : Les données utilisateur sont des données personnelles. L'email, first_name, last_name, phone doivent être traités conformément à la loi CI sur la protection des données.
  • Données utilisateur cross-tenant : Les informations personnelles d'un utilisateur (email, nom) ne sont PAS tenant-scopées — elles sont au niveau plateforme. L'appartenance (quel tenant, quel rôle) est la partie tenant-spécifique. C'est conforme puisque l'utilisateur a consenti à chaque relation tenant via l'acceptation de l'invitation.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Entities, §Service layer, §Keycloak integration
  • docs/standards/database-guidelines.md — §Platform vs tenant DB separation, §Join tables
  • docs/standards/api-guidelines.md — §Authentication endpoints, §Standard envelope
  • docs/standards/react-typescript-guidelines.md — §List components, §Bottom sheet pattern
  • docs/standards/offline-sync-guidelines.md — §Online-only pages

Exigences de test

Tests unitaires

  • UserService : flux d'invitation d'un nouvel utilisateur
  • UserService : flux d'invitation d'un utilisateur existant (multi-tenant)
  • UserService : rejeter l'invitation d'une appartenance en doublon
  • UserService : liste des utilisateurs pour un tenant

Tests d'intégration

  • POST /api/v1/users/invite — flux complet avec mock Keycloak
  • GET /api/v1/users — isolation tenant (les utilisateurs du tenant A non visibles par le tenant B)
  • POST /api/v1/users/invite — appartenance en doublon → 409

Ce que le QA validera

  • La page liste des utilisateurs s'affiche correctement sur tous les breakpoints
  • Le flux d'invitation fonctionne de bout en bout (bottom sheet, email envoyé)
  • Les descriptions des rôles s'affichent correctement dans le formulaire d'invitation
  • L'invitation expirée affiche le lien de re-invitation
  • La page affiche "non disponible hors ligne" quand hors ligne

Hors périmètre

  • Résolution des permissions + restriction par module — CP-014
  • Badges de statut liste utilisateurs + UX de détection multi-tenant — CP-015
  • Changement de tenant (endpoint + UI) — CP-011
  • Rôles personnalisés — V2. Le MVP a uniquement 5 rôles par défaut fixes.
  • Interface de désactivation/suppression d'utilisateur — V2
  • Politique de mot de passe — Keycloak s'en charge
  • Gestion des utilisateurs en console admin — CP-012

Définition de Done

  • [ ] Tous les endpoints backend implémentés et retournant les réponses correctes
  • [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px
  • [ ] Les 5 états UI implémentés (loading, empty, error, offline, success)
  • [ ] Les micro-textes français correspondent exactement à la spec design
  • [ ] Tous les critères d'acceptation passent manuellement
  • [ ] Tests unitaires écrits et passants
  • [ ] Tests d'intégration écrits et passants
  • [ ] Isolation multi-tenant vérifiée (aucune fuite de données inter-tenant)
  • [ ] Écriture hors ligne + sync fonctionnelle (Papillon) — N/A (gestion utilisateurs toujours en ligne)
  • [ ] Pas de any TypeScript — tous les types explicites
  • [ ] Pas de types Java bruts — tous les génériques explicites
  • [ ] Les réponses API respectent le format d'enveloppe standard
  • [ ] Entrées d'audit créées pour chaque mutation de données