Aller au contenu

Utilisateurs espace financement (BO Projets)

administration/project-financing-request/users* et …/invitations* (AdminAuthGuard, sans @AllowedProjectRoles : pas de :projectId pour résoudre un rôle) servent la page « Utilisateurs espace financement » du BO Projets (BRI-2197). Les ops suivent qui accède à quel dossier, corrigent une identité et débloquent une connexion ; inviter reste côté financement — c'est le représentant qui invite.

Source : project-financing-request-user-administration.controller.ts, contrat projectFinancingRequestUsers.endpoint.ts (api-communication-bricksoffice, zod).

Routes

Méthode Chemin Effet
GET users Une page d'un onglet : status obligatoire (active = utilisateurs avec compte, invitation-pending = invités), offset / take ≤ 200, q, role répété. Lignes maigres : identité, roles, projectCount, dates
GET users/by-email/:email Fiche complète d'une personne : accès aux dossiers, invitations en attente, date d'acceptation, dernière connexion. 404 si l'email n'est ni utilisateur ni invité
PATCH users/:projectUserId Identité d'un utilisateur — nameValidator_zod / phoneValidator_zod, ceux des additionalFields better-auth. Écrit better_auth_user et customers.email (pont auth legacy) dans une transaction TypeORM, 409 email-already-used sur l'une ou l'autre unicité. Un changement d'email re-clé aussi les invitations pending de l'ancienne adresse
PUT users/:projectUserId/projects Réécrit les accès aux dossiers — { projects: [{ projectId, role }] } remplace l'actuelle, un rôle par dossier (404 si un id n'existe pas, 400 si un id est répété)
POST invitations/:invitationId/link Émet un lien pour une invitation en attente
GET users/:projectUserId/admin-login Connexion en tant que, même tronc que la route investisseur

Liste

  • Un utilisateur (onglet active) = a détenu ≥ 1 rôle (actif ou révoqué) ou compte registeredFrom = 'financing' (inscription PDP). project_financing_request_user seul ne veut rien dire : le hook better-auth en crée une à toute connexion, investisseurs compris (~49k rows en prod). Sans dossier, la ligne porte roles: [] et projectCount: 0 : un inscrit PDP pas encore rattaché, ou un utilisateur dont on a révoqué le dernier dossier, reste visible. registeredFrom est déclaré par le client : il sert à afficher, jamais à autoriser. Un invité = ≥ 1 invitation pending, aucune row user requise, clé = email.
  • Deux tables, deux onglets, deux requêtes plates : listUsersPage (user ⋈ compte, left join rôles actifs agrégés, dernière session en sous-requête corrélée — une CTE agrégerait les ~870k sessions de prod pour une page de 50) et listPendingInviteesPage (invitations pending groupées par email, filtres en HAVING). Pas d'union : un utilisateur invité sur un autre dossier apparaît dans les deux onglets, c'est l'information attendue.
  • Page et total sont deux requêtes sur le même filtre (Promise.all), comme la liste des demandes : le total reste juste sur une page vide.
  • q : ILIKE sur prénom, nom, email, téléphone (utilisateurs) ; prénom, nom, email (invités).
  • Ordre fixe, du plus récent au plus ancien ; pas de whitelist de tri (le BO n'en propose pas).

Fiche

  • L'email est la seule clé commune aux deux côtés : la fiche se lit par email, un utilisateur l'emporte sur un invité (même adresse = même personne, montrée avec son compte).
  • Trois lectures d'une personne — utilisateur (findUserByEmail), accès aux dossiers (ProjectFinancingRequestUserRoleRepository), invitations en attente + acceptées (…InvitationRepository) — assemblées dans views/.

Choix

  • Pas d'index dédié : _role / _invitation quasi vides en prod, better_auth_session("userId") déjà indexé par better-auth.
  • Copier le lien fait tourner le token (rotateTokenHashIfPending, même mécanique que la relance) : seul le hash est stocké, le mail précédent meurt avec la copie. Aucun mail envoyé.
  • Le rôle est porté par le dossier, comme en base et sur le PDP : le BO n'impose pas un rôle unique par utilisateur.
  • Un apporteur d'affaires n'est jamais promu — apporteur-affaires-not-promotable (409), la règle des chemins d'invitation.
  • L'identité s'écrit via BetterAuthRepository, jamais via la slice Kysely — better-auth possède la table. L'email est recopié sur customers (CustomersAuthRepository), comme le BO investisseur (CustomersUpdateService.updateCustomer) : le transport Customer.io des codes de sécurité résout le compte par cet email.
  • Pas de PATCH sur l'identité d'un invité : il la renseigne lui-même à l'acceptation ; la fiche BO l'affiche en lecture seule avec l'explication en tooltip.
  • Audit admin_action sans PII : le PATCH identité ne persiste que la cible (params), jamais les nouvelles valeurs.
  • Connexion en tant que refusée sur une adresse @bricks.co (admin-login-forbidden-account) ; l'impersonation ne compte pas comme une connexion (lastLoginAt inchangé).