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 compteregisteredFrom = 'financing'(inscription PDP).project_financing_request_userseul ne veut rien dire : le hook better-auth en crée une à toute connexion, investisseurs compris (~49k rows en prod). Sans dossier, la ligne porteroles: []etprojectCount: 0: un inscrit PDP pas encore rattaché, ou un utilisateur dont on a révoqué le dernier dossier, reste visible.registeredFromest déclaré par le client : il sert à afficher, jamais à autoriser. Un invité = ≥ 1 invitationpending, 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) etlistPendingInviteesPage(invitationspendinggroupées par email, filtres enHAVING). 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 dansviews/.
Choix¶
- Pas d'index dédié :
_role/_invitationquasi 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é surcustomers(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_actionsans 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 (lastLoginAtinchangé).