Skip to content

refactor(user): migrate admin routes from server - #2738

Open
shikanime wants to merge 9 commits into
mainfrom
pr/user-migration
Open

shikanime wants to merge 9 commits into
mainfrom
pr/user-migration

Conversation

@shikanime

@shikanime shikanime commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

Issues liées

Refs #1889


Quel est le comportement actuel ?

Les routes d'administration des utilisateurs (/api/v1/users) sont servies par l'ancienne application Fastify apps/server.

Quel est le nouveau comportement ?

Migration du module user vers apps/server-nestjs :

  • UserController, UserService, UserModule.
  • user-queries.utils.ts : sélections Prisma typées.
  • Enregistrement du module dans main.module.ts.
  • Parité des contrats et codes HTTP contre apps/server/src/resources/user/ (400 sur rôle admin inconnu, take: 5 sur la recherche, émission user.upsert par rôle).

Cette PR introduit-elle un breaking change ?

Non.

Autres informations

Recréée pour #1889 (remplace #2498, non réouvrable après suppression de branche).

Comment thread apps/server-nestjs/src/modules/user/user.service.ts Fixed
@github-actions github-actions Bot added the built label Sep 17, 2026
@shikanime shikanime self-assigned this Sep 17, 2026
@shikanime shikanime added this to the 9.27.0 milestone Sep 17, 2026
Comment thread apps/server-nestjs/src/modules/user/user.service.ts Outdated
Comment thread apps/server-nestjs/src/modules/user/user.service.ts Outdated
Comment thread apps/server-nestjs/src/modules/user/user.service.ts Outdated
Comment thread apps/server-nestjs/src/modules/user/user.controller.ts Outdated
Comment thread apps/server-nestjs/src/modules/user/user-queries.utils.ts Outdated
Comment thread apps/server-nestjs/src/modules/user/user.service.ts Outdated
Comment thread apps/server-nestjs/src/modules/user/user.module.ts Outdated

@shikanime shikanime left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict : Changements demandés

Migration fidèle sur le plan requêtes (where, take: 5, rejet de rôle inconnu, union before/after des rôles impactés — la sémantique legacy est respectée). Point positif : la logique where est copiée sans « amélioration » silencieuse, ce qui rend la parité vérifiable ligne à ligne. Un point bloquant de pont événements → plugins, détaillé inline.

user.adminRoleIds?.forEach(roleId => impactedRoleIds.add(roleId))
}
for (const roleId of impactedRoleIds) {
await this.eventEmitter.emitAsync('adminRole.upsert', { roleId })

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🔴 Bloquant] emitAsync('adminRole.upsert') n'a aucun consommateur @OnEvent('adminRole.upsert') dans server-nestjs : à la bascule (apps/server sert encore la route aujourd'hui, donc rien de cassé en prod), la synchro Keycloak/GitLab des groupes admin est silencieusement perdue. Suggestion : ajouter le pont dans keycloak.service.ts / gitlab.service.ts sur le modèle de project.upsert → capturePluginResult, ou tracer un ticket de suivi bloquant la bascule.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vérifié : le consommateur existe désormais. La branche de base #2749 (pr/admin-role-event-bridge, tête 01467dc713) ajoute @OnEvent('adminRole.upsert') dans keycloak.service.ts:59 et gitlab.service.ts:87, tous deux via capturePluginResult, avec admin-role-bridge.spec.ts de part et d'autre : le pont demandé existe, plus de perte silencieuse à la bascule.

⚠️ En revanche le contrat de payload diverge et doit être unifié dans #2749, sinon le no-op silencieux revient par la forme : keycloak lit un roleId: string (parité legacy hook.adminRole.upsert(roleId)), gitlab lit un objet AdminRoleEventPayload (app-events.service.ts:38) et admin-role.service.ts:45/124 de #2749 émet cet objet — donc le handler keycloak de #2749 ne matche déjà plus ses propres événements (roles.find(({ id }) => id === roleId)), et user.service.ts:64 qui émet { roleId } ne satisfait ni l'un ni l'autre.

Je laisse ce fil ouvert : la forme canonique doit être tranchée dans #2749 (base), puis #2738 s'alignera dessus. Hors périmètre de #2738 seul.

Comment thread apps/server-nestjs/src/modules/user/user-queries.utils.ts Outdated
Comment thread apps/server-nestjs/src/modules/user/user.controller.ts Outdated

@shikanime shikanime left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And remove // -- comments

@shikanime
shikanime removed this pull request from stack #2747 September 18, 2026 09:45
@shikanime
shikanime changed the base branch from main to pr/admin-role-event-bridge September 18, 2026 09:45
@shikanime
shikanime added this pull request to stack #2751 September 18, 2026 09:46
shikanime added a commit that referenced this pull request Sep 23, 2026
Aligne le module user sur le pattern établi par le module zone (#2487) :
- `user-queries.utils.ts` : suppression du `userSelect`/`UserRecord` morts,
  signatures sur `Prisma.TransactionClient`, `createUser` purement DB (la
  validation du doublon email monte dans le service en `ConflictException`).
- `user.service.ts` : extraction de `resolveAdminRoleIds`,
  `patchUsersInTx`, `emitImpactedRoleEvents` ; patch des rôles sous
  transaction.
- `user.module.ts` : imports repliés sur `InfrastructureModule` (déjà
  exporté par celui-ci).

Refs #2738

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I3bd632f95dd466e585698eba738efcd86a6a6964
shikanime added a commit that referenced this pull request Sep 23, 2026
Aligne le module user sur le pattern établi par le module zone (#2487) :
- `user-queries.utils.ts` : suppression du `userSelect`/`UserRecord` morts,
  signatures sur `Prisma.TransactionClient`, `createUser` purement DB (la
  validation du doublon email monte dans le service en `ConflictException`).
- `user.service.ts` : extraction de `resolveAdminRoleIds`,
  `patchUsersInTx`, `emitImpactedRoleEvents` ; patch des rôles sous
  transaction.
- `user.module.ts` : imports repliés sur `InfrastructureModule` (déjà
  exporté par celui-ci).

Refs #2738

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I3bd632f95dd466e585698eba738efcd86a6a6964
shikanime added a commit that referenced this pull request Sep 23, 2026
Aligne le module user sur le pattern établi par le module zone (#2487) :
- `user-queries.utils.ts` : suppression du `userSelect`/`UserRecord` morts,
  signatures sur `Prisma.TransactionClient`, `createUser` purement DB (la
  validation du doublon email monte dans le service en `ConflictException`).
- `user.service.ts` : extraction de `resolveAdminRoleIds`,
  `patchUsersInTx`, `emitImpactedRoleEvents` ; patch des rôles sous
  transaction.
- `user.module.ts` : imports repliés sur `InfrastructureModule` (déjà
  exporté par celui-ci).

Refs #2738

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I3bd632f95dd466e585698eba738efcd86a6a6964
shikanime added a commit that referenced this pull request Sep 23, 2026
Aligne le module user sur le pattern établi par le module zone (#2487) :
- `user-queries.utils.ts` : suppression du `userSelect`/`UserRecord` morts,
  signatures sur `Prisma.TransactionClient`, `createUser` purement DB (la
  validation du doublon email monte dans le service en `ConflictException`).
- `user.service.ts` : extraction de `resolveAdminRoleIds`,
  `patchUsersInTx`, `emitImpactedRoleEvents` ; patch des rôles sous
  transaction.
- `user.module.ts` : imports repliés sur `InfrastructureModule` (déjà
  exporté par celui-ci).

Refs #2738

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I3bd632f95dd466e585698eba738efcd86a6a6964
shikanime added a commit that referenced this pull request Sep 23, 2026
Aligne le module user sur le pattern établi par le module zone (#2487) :
- `user-queries.utils.ts` : suppression du `userSelect`/`UserRecord` morts,
  signatures sur `Prisma.TransactionClient`, `createUser` purement DB (la
  validation du doublon email monte dans le service en `ConflictException`).
- `user.service.ts` : extraction de `resolveAdminRoleIds`,
  `patchUsersInTx`, `emitImpactedRoleEvents` ; patch des rôles sous
  transaction.
- `user.module.ts` : imports repliés sur `InfrastructureModule` (déjà
  exporté par celui-ci).

Refs #2738

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I3bd632f95dd466e585698eba738efcd86a6a6964
@shikanime
shikanime requested a review from a team as a code owner September 23, 2026 14:49
@shikanime shikanime added the preview Deploy preview app with Argo-cd label Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Hey !

A preview of the application is available at : https://console-pr-2738.dso.cpin-hp.numerique-interieur.fr

Please be patient, deployment may take a few minutes.

Comment thread apps/server-nestjs/src/modules/user/user.controller.spec.ts Fixed
@shikanime shikanime removed the preview Deploy preview app with Argo-cd label Sep 28, 2026
const users = [makeUser()]
service.getAllUsers.mockResolvedValue(users)

const result = await controller.getAllUsers({ adminRoleIds: ['r1'], relationType: 'OR' } as never)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid the use of as never

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé : voir 4131482912 — plus aucun as never, entrées parsées par les schémas partagés.

Comment on lines +21 to +24
const relationType = query.relationType ?? 'AND'
const { relationType: _, ...listQuery } = query
const users = await this.userService.getAllUsers(listQuery, relationType)
return users.map(toContractUser)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just pass query at this point

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé : le contrôleur passe query tel quel ; le défaut relationType ?? 'AND' vit dans buildAllUsersWhere.

users: PatchUsersBody,
): Promise<User[]> {
const usersBefore = await getUsers(this.prisma, { id: { in: users.map(({ id }) => id) } })
await this.prisma.$transaction(tx => patchUsers(tx, users))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably useless ? Just pass this.prisma ? Or wrap usersBefore in transaction

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conservé tel quel : usersBefore doit être lu avant la transaction, la lecture dans la transaction ne réduirait pas la fenêtre de course.

@shikanime shikanime moved this to Backlog in Cloud Pi Native Sep 29, 2026

@StephaneTrebel StephaneTrebel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changements demandés. Le payload adminRole.upsert émis par cette PR est incompatible avec les consommateurs Keycloak et GitLab de sa base #2749, ce qui laisse les groupes externes non synchronisés après un patch utilisateur. Les échappatoires as never des tests restent interdites ; la CI du head est verte, mais une preuve de bout en bout de la chaîne événementielle manque.

user.adminRoleIds?.forEach(roleId => impactedRoleIds.add(roleId))
}
for (const roleId of impactedRoleIds) {
await this.eventEmitter.emitAsync('adminRole.upsert', { roleId })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 blocking: Cette émission { roleId } ne correspond à aucun consommateur de la branche de base #2749 : KeycloakService.handleAdminRoleUpsert attend un string, tandis que GitlabService.handleAdminRoleUpsert attend un AdminRoleEventPayload (id, oidcGroup, members). Après un PATCH, Keycloak ne trouve donc aucun rôle et GitLab ne peut associer aucun groupe : la synchronisation legacy est perdue silencieusement. Trancher le payload canonique dans #2749, aligner ses deux consommateurs puis émettre exactement cette forme ici, avec un test de chaîne PATCH → événement → plugins.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé : emitImpactedRoleEvents charge désormais les rôles (id, oidcGroup) et leurs membres, puis émet le payload canonique { id, oidcGroup, members } attendu par KeycloakService.handleAdminRoleUpsert et GitlabService.handleAdminRoleUpsert.

service.patchUsers.mockResolvedValue(users)
const body = [{ id: users[0].id, adminRoleIds: ['r1'] }]

expect(await controller.patchUsers(body as never)).toEqual(users.map(toContractUser))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 important: as never est interdit dans ce dépôt : il coupe la preuve de type du test et masque ici la compatibilité réelle de PatchUsersBody. Le même échappatoire demeure à la ligne 41. Construire des entrées typées par les schémas partagés, sans assertion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé : les deux as never disparaissent — entrées typées via AllUsersQuerySchema.parse / PatchUsersBodySchema.parse dans le spec contrôleur.

@shikanime shikanime added the refactor Refactor code label Sep 29, 2026
@shikanime
shikanime requested review from a team and StephaneTrebel and removed request for StephaneTrebel September 29, 2026 13:07
@shikanime
shikanime removed this pull request from stack #2751 September 29, 2026 13:28
@shikanime
shikanime changed the base branch from pr/admin-role-event-bridge to main September 29, 2026 13:43
@shikanime
shikanime added this pull request to stack #2794 September 29, 2026 13:51

@shikanime shikanime left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict : Commentaire

L'extraction des schémas de contrat dans packages/shared est mécanique et les deux côtés client/serveur restent alignés. Le pont d'événements adminRole.upsert émet désormais le payload complet {id, oidcGroup, members} attendu par les consommateurs — la forme { roleId } signalée en review est corrigée. Rien de bloquant côté diff.

select: { id: true, email: true, firstName: true, lastName: true, adminRoleIds: true },
})
for (const role of roles) {
await this.eventEmitter.emitAsync('adminRole.upsert', {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[✨ Éloge] emitImpactedRoleEvents calcule l'union des rôles avant/après puis relit les membres depuis la base après commit du $transaction : payload idempotent au replay et aligné sur le contrat des consommateurs Keycloak/GitLab — c'est le patron d'émission post-commit qu'il faut généraliser.

shikanime and others added 8 commits October 1, 2026 11:13
…ion semantics

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: Shikanime Deva <22115108+shikanime@users.noreply.github.com>
Aligne le module user sur le pattern établi par le module zone (#2487) :
- `user-queries.utils.ts` : suppression du `userSelect`/`UserRecord` morts,
  signatures sur `Prisma.TransactionClient`, `createUser` purement DB (la
  validation du doublon email monte dans le service en `ConflictException`).
- `user.service.ts` : extraction de `resolveAdminRoleIds`,
  `patchUsersInTx`, `emitImpactedRoleEvents` ; patch des rôles sous
  transaction.
- `user.module.ts` : imports repliés sur `InfrastructureModule` (déjà
  exporté par celui-ci).

Refs #2738

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I3bd632f95dd466e585698eba738efcd86a6a6964
`AllUsersQuerySchema` étant exporté, l'alias `AllUsersQuery` est
redondant ; les consommateurs dérivent le type par `z.infer`.
`LettersQuery` est conservé (utilisé par le client).

Refs #1889

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I5d954715680b8fbcd866edf4bb300dc36a6a6964
Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
- supprimer UserService.createUser et les helpers createUser/getUserByEmail
  (aucun appelant ni route au contrat : chemin mort signalé en revue)
- supprimer l'import faker inutilisé de user.controller.spec.ts
- verrouiller par spec : 400 (BadRequestException) sur adminRole inconnue,
  take:5 sur la recherche par lettres, émission union avant ∪ après

Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Reflect.getMetadata assertions pass whether or not the guard is enforced
and add no behavioural coverage; the guard logic stays covered in
user.guard.spec.ts and the controller delegation tests are untouched.

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
…nputs

Emit the full { id, oidcGroup, members } payload that the 2749 event
bridge consumers expect, drop as never from the controller spec by
parsing inputs through the shared schemas, and move the relationType
default into buildAllUsersWhere so the controller passes the query
straight through.

Co-authored-by: Automata <automata@shikanime.studio>
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: I28676aa618050a7042ed24dfa9e66b896a6a6964
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
Change-Id: Ieb2ee298f5d0547bf988787721ed029a6a6a6964
@cloud-pi-native-sonarqube

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

built refactor Refactor code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants