Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -427,14 +427,14 @@ et `user` sont des pre-requis pour la Vague 3 (cluster, project-member).
- `GET /api/v1/admin/users` - Liste complete des utilisateurs (admin)
- `PATCH /api/v1/admin/users` - Modification utilisateurs (admin)

**Dependances sortantes** : `queries-index` (getMatchingUsers, getUsers), hooks (adminRole.upsert)
**Dependances sortantes** : `queries-index` (getMatchingUsers, getUsers), EventEmitter (`adminRole.upsert`)
**Dependances entrantes** : `project-member` (importe `logViaSession`), `utils/controller.ts` (importe `logViaSession`, `logViaToken`)

**Points d'attention** :
- Module critique : `logViaSession` et `logViaToken` sont consommes par le
systeme d'auth. Ces fonctions auront ete portees dans l'AuthGuard (Couche 0),
donc la dependance entrante est deja resolue
- Hooks utilises (adminRole.upsert) : necessite l'EventEmitter
- Evenement utilise (`adminRole.upsert`) : necessite `@nestjs/event-emitter`
- Pre-requis pour `project-member` (Vague 3)

**Estimation** : 2 jours
Expand All @@ -457,11 +457,11 @@ et `user` sont des pre-requis pour la Vague 3 (cluster, project-member).
- `GET /api/v1/admin/roles/member-counts` - Comptage des membres par role
- `DELETE /api/v1/admin/roles/:roleId` - Suppression d'un role

**Dependances sortantes** : `queries-index` (getAdminRoleById, listAdminRoles), hooks (adminRole.upsert, adminRole.delete)
**Dependances sortantes** : `queries-index` (getAdminRoleById, listAdminRoles), EventEmitter (`adminRole.upsert`, `adminRole.delete`)
**Dependances entrantes** : Aucune

**Points d'attention** :
- Valide le pattern hooks + EventEmitter dans un contexte admin
- Valide le pattern EventEmitter dans un contexte admin
- Permissions admin (bitmask) : valide `UserGuard`
- Queries a internaliser dans le module

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@

## 🎯 Progression globale

![Progress](https://progress-bar.dev/7/?title=modularisation&width=400)

**~7%** complété (1/18 modules métier migrés, 5/75 routes)
**~24%** complété (4/18 modules métier migrés, 21/75 routes)

---

## 📊 Vue d'ensemble

| Statut | Nombre de modules | % du total |
|--------|-------------------|------------|
| ✅ Migré | 1 (ServiceChain) | ~6% |
| ✅ Migré | 4 (ServiceChain, Project, ProjectMembers, AdminRole) | ~22% |
| 🚧 En cours | 0 | 0% |
| 📅 Planifié | 17 | ~94% |
| 📅 Planifié | 14 | ~78% |
| ⏳ En attente de cartographie | 0 | 0% |

---
Expand Down Expand Up @@ -48,17 +46,107 @@ profitant de son isolement complet vis-à-vis du reste du codebase.
| `POST` | `/api/v1/service-chains/:id/retry` | `ManageSystem` |
| `POST` | `/api/v1/service-chains/validate/:id` | `ManageSystem` |

### Project — migré le 2026-05-28

Module cœur de gestion des projets (CRUD, secrets, bulk actions).
Regroupe les sous-modules `project-core`, `project-secrets` et `project-bulk`
découpés dans la cartographie Vague 4, implémentés ici en un seul module
unifié pour accélérer la migration.

- **Routes** : 7 (`/api/v1/projects/...`)
- **Auth** : Token (`x-dso-token`) + guards projet (`ProjectContextGuard`, `ProjectStatusGuard`, `ProjectLockedGuard`)
- **Validation** : Contrats Zod via `projectContract` de `@cpn-console/shared` avec `ZodValidationPipe`
- **Tests** : Controller + Service couverts (Vitest)

| Méthode | Route | Permission |
|---------|-------|------------|
| `GET` | `/api/v1/projects/data` | `Manage` (admin) |
| `GET` | `/api/v1/projects` | Authentifié |
| `POST` | `/api/v1/projects` | `ManageProjects` (admin) + type `human` |
| `POST` | `/api/v1/projects-bulk` | `Manage` (admin) |
| `GET` | `/api/v1/projects/:projectId` | Authentifié + projet |
| `GET` | `/api/v1/projects/:projectId/secrets` | Authentifié + projet (statut ≠ archived) |
| `PUT` | `/api/v1/projects/:projectId` | Authentifié + projet (statut ≠ archived) |

**Infrastructure créée/mise à jour** :
- `ProjectContextGuard` + `Project` decorator : chargement du projet par id/slug, résolution des permissions via bitmask
- `ProjectStatusGuard` + `@RequireProjectStatus()` : filtrage par statut du projet
- `ProjectLockedGuard` : protection contre les modifications de projets verrouillés
- `UserGuard` + `User` decorator : authentification token + injection du contexte utilisateur

**Différences avec le legacy** :
- Utilisation de `projectContract` de `@cpn-console/shared` pour les schemas de validation (cohérence client/serveur)
- ZodValidationPipe au lieu de la validation ts-rest implicite
- Les routes `secrets` et `bulk` sont fusionnées dans le module Project (pas de sous-modules séparés)
- Pas de `DELETE /api/v1/projects/:projectId` (archivage) dans cette version initiale

### ProjectMembers — migré le 2026-05-28

Module de gestion des membres projet (ajout, modification, suppression, liste).
Dédié aux routes `/api/v1/projects/:projectId/members/...`.

- **Routes** : 4 (`/api/v1/projects/:projectId/members/...`)
- **Auth** : Token (`x-dso-token`) + guards projet (`ProjectContextGuard`, `ProjectStatusGuard`, `ProjectLockedGuard`)
- **Validation** : Contrats Zod via `projectMemberContract` de `@cpn-console/shared`

| Méthode | Route | Permission |
|---------|-------|------------|
| `GET` | `/api/v1/projects/:projectId/members` | `ListMembers` (admin) |
| `POST` | `/api/v1/projects/:projectId/members` | `ManageMembers` (admin) + statut non archivé |
| `PATCH` | `/api/v1/projects/:projectId/members` | `ManageMembers` (admin) + statut non archivé |
| `DELETE` | `/api/v1/projects/:projectId/members/:userId` | `ManageMembers` (admin) ou auto-suppression + statut non archivé |

**Fichiers** :
- `src/modules/project/project-members.controller.ts`
- Méthodes ajoutées dans `src/modules/project/project.service.ts`

**Différences avec le legacy** :
- Recherche par email non disponible (nécessite le hook Keycloak `user.retrieveUserByEmail`, non migré)
- Retour de la liste complète des membres après chaque mutation (cohérence avec le legacy)
- Événements `projectMember.upsert` et `projectMember.delete` via EventEmitter (remplacement du hook `projectMember.upsert`/`projectMember.delete`)

### AdminRole — migré le 2026-06-12

Module de gestion des rôles administrateurs (CRUD + tri + comptage membres).
Dédié aux routes `/api/v1/admin/roles/...`.

- **Routes** : 5 (`/api/v1/admin/roles/...`)
- **Auth** : Guards admin (`AdminGuard`) + décorateur `@RequireAdminPermission()`
- **Validation** : Contrats Zod via `adminRoleContract` de `@cpn-console/shared` avec `ZodValidationPipe`
- **Tests** : Controller + Service couverts (Vitest)

| Méthode | Route | Permission |
|---------|-------|------------|
| `GET` | `/api/v1/admin/roles` | `ListRoles` |
| `POST` | `/api/v1/admin/roles` | `ManageRoles` |
| `PATCH` | `/api/v1/admin/roles` | `ManageRoles` |
| `GET` | `/api/v1/admin/roles/member-counts` | `ManageRoles` |
| `DELETE` | `/api/v1/admin/roles/:roleId` | `ManageRoles` |

**Infrastructure réutilisée** :
- `AdminGuard` + `@RequireAdminPermission()` : validation des permissions admin
- `AdminService` : validation bitmask + typage utilisateur

**Fichiers** :
- `src/modules/admin-role/admin-role.controller.ts`
- `src/modules/admin-role/admin-role.service.ts`
- `src/modules/admin-role/admin-role.utils.ts`

**Différences avec le legacy** :
- Déport de la logique de permissions côté NestJS plutôt que dans le legacy Fastify
- Comptage des membres via query Prisma dédiée au lieu d'agrégat

### Infrastructure transverse déployée

En support de cette migration, les éléments d'infrastructure suivants ont été
En support de ces migrations, les éléments d'infrastructure suivants ont été
créés :

- **AuthModule** (`infrastructure/auth/`) : `AuthService` (validation token
SHA256 via Prisma + bearer JWT Keycloak), `DsoTokenModule`,
`KeycloakJwtModule` et décorateur `@AuthUser()`
- **PermissionModule** (`infrastructure/permission/`) : `UserModule`
(`UserGuard`, `UserService`, `UserPolicy`) + `ProjectModule`
(`ProjectGuard`, `ProjectLoaderService`, `ProjectService`, `ProjectPolicy`)
SHA256 via Prisma) + `AdminPermissionGuard` + décorateur
`@RequireAdminPermission()`
- **Guards projet** (`infrastructure/auth/`) : `ProjectContextGuard`,
`ProjectStatusGuard`, `ProjectLockedGuard` + décorateurs `@Project()` et
`@RequireProjectStatus()`
- **Nginx strangler** : Reverse proxy configuré pour router les routes migrées
vers server-nestjs, le reste vers le legacy
- **Docker** : Build order corrigé (shared avant server-nestjs)
Expand All @@ -78,7 +166,7 @@ créés :

## 📅 Modules planifiés

> Ces informations seront affinées après la cartographie (fin S2)
> Ces informations seront affinées après la cartographie (fin S2).

### Sprint 3-4 (27 janvier - 9 février)
- **Module** : Auth
Expand Down Expand Up @@ -140,9 +228,9 @@ créés :
### Routes par statut

- **Total** : ~75 routes métier
- **Migrés** : 5 (~7%)
- **Migrés** : 21 (~28%)
- **En cours** : 0 (0%)
- **Restants** : ~70 (~93%)
- **Restants** : ~54 (~72%)

---

Expand All @@ -154,16 +242,17 @@ créés :
| 26/01/2026 | Fin de la cartographie (S2) |
| 27/01/2026 | Début modularisation Auth (S3) |
| 09/02/2026 | Fin modularisation Auth (S4) - 20% complété |
| 09/03/2026 | Point mi-parcours - 60% complété |
| 26/03/2026 | Migration ServiceChain (OpenCDS) finalisée — 1er module métier migré |
| 28/05/2026 | Migration du module **Project** (7 routes) — CRUD + secrets + bulk |
| 28/05/2026 | Migration du module **ProjectMembers** (4 routes) — membres projet |
| 12/06/2026 | Migration du module **AdminRole** (5 routes) — gestion des rôles admin |
| 06/04/2026 | Fin de modularisation - 100% complété |

---

## 📞 Contacts & Communication

### Canaux
- **Slack** : #backend-modularisation
- **Mattermost** : #backend-modularisation
- **Meeting hebdo** : Vendredi 16h (30min)
- **Lead technique** : @stephane.trebel

Expand All @@ -188,6 +277,19 @@ créés :

## 🔄 Historique des changements

### 2026-06-12
- ✅ Migration du module **AdminRole** — 5 routes : gestion des rôles admin (list, create, patch, member-counts, delete)
- ✅ Mise à jour du contrôleur pour utiliser `AdminGuard` + `@RequireAdminPermission()`
- ✅ Création des utilitaires de mapping `AdminRole` Prisma → `@cpn-console/shared`

### 2026-05-28
- ✅ Migration du module **Project** — 7 routes : CRUD projets, secrets, bulk actions, export CSV
- ✅ Migration du module **ProjectMembers** — 4 routes : membres projet (list, add, patch, delete)
- ✅ Création des guards projet : `ProjectContextGuard`, `ProjectStatusGuard`, `ProjectLockedGuard`
- ✅ Création des décorateurs `@Project()` et `@RequireProjectStatus()`
- ✅ Utilisation de `projectContract` de `@cpn-console/shared` pour la validation Zod
- ✅ Build et lint verts (server-nestjs)

### 2026-04-09
- ✅ Migration du module **ServiceChain (OpenCDS)** — 5 routes, proxy HTTP vers API externe
- ✅ Création de l'**AuthModule** (infrastructure/auth/) : auth par token `x-dso-token`
Expand All @@ -210,5 +312,5 @@ créés :

---

**Version du fichier** : 1.1
**Version du fichier** : 1.2
**Responsable de mise à jour** : Lead technique backend
2 changes: 2 additions & 0 deletions apps/server-nestjs/src/main.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common'
import { ScheduleModule } from '@nestjs/schedule'
import { AdminRoleModule } from './modules/admin-role/admin-role.module'
import { DeploymentModule } from './modules/deployment/deployment.module'
import { EnvironmentModule } from './modules/environment/environment.module'
import { HealthzModule } from './modules/healthz/healthz.module'
Expand All @@ -19,6 +20,7 @@ import { VersionModule } from './modules/version/version.module'

@Module({
imports: [
AdminRoleModule,
InfrastructureModule,
HealthzModule,
KeycloakModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Prisma } from '@prisma/client'

export const adminRoleSelect = {
id: true,
name: true,
permissions: true,
position: true,
oidcGroup: true,
type: true,
} satisfies Prisma.AdminRoleSelect

export type AdminRole = Prisma.AdminRoleGetPayload<{
select: typeof adminRoleSelect
}>

export async function getAdminRoleMaxPosition(tx: Prisma.TransactionClient): Promise<number> {
const role = await tx.adminRole.findFirst({
orderBy: { position: 'desc' },
select: { position: true },
})

return role?.position ?? -1
}

export async function createAdminRole(tx: Prisma.TransactionClient, name: string): Promise<{ role: AdminRole, members: { id: string, email: string, firstName: string, lastName: string }[] }> {
const maxPosition = await getAdminRoleMaxPosition(tx)
const role = await tx.adminRole.create({
data: {
name,
permissions: 0n,
position: maxPosition + 1,
},
select: adminRoleSelect,
})

const members = await tx.user.findMany({
where: { adminRoleIds: { has: role.id } },
select: { id: true, email: true, firstName: true, lastName: true },
})

return { role, members }
}

export async function getAdminRoleMemberCounts(tx: Prisma.TransactionClient): Promise<Record<string, number>> {
const roles = await tx.adminRole.findMany({
where: { oidcGroup: { equals: '' } },
select: { id: true },
})
const roleIds = roles.map(role => role.id)
const users = await tx.user.findMany({
where: { adminRoleIds: { hasSome: roleIds } },
select: { adminRoleIds: true },
})

const counts: Record<string, number> = Object.fromEntries(roleIds.map(roleId => [roleId, 0]))
for (const { adminRoleIds } of users) {
for (const roleId of adminRoleIds) {
if (typeof counts[roleId] === 'number') counts[roleId]++
}
}

return counts
}

export async function getRoles(tx: Prisma.TransactionClient): Promise<AdminRole[]> {
return await tx.adminRole.findMany({
orderBy: { position: 'asc' },
select: adminRoleSelect,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { AdminRoleService } from './admin-role.service'
import type { adminRoleContract } from '@cpn-console/shared'
import { z } from 'zod'
import type { AdminRole } from './admin-role-queries.utils'

export type AdminRoleContract = Parameters<AdminRoleService['patch']>[0][number]
export type AdminRoleResponse = NonNullable<Awaited<ReturnType<AdminRoleService['list']>>>[number]

export interface AdminRoleMember {
id: string
email: string
firstName: string
lastName: string
}

export function makeAdminRole(overrides: Partial<AdminRole> = {}): AdminRole {
return {
id: overrides.id ?? crypto.randomUUID(),
name: overrides.name ?? 'New role',
permissions: overrides.permissions ?? 0n,
position: overrides.position ?? 0,
oidcGroup: overrides.oidcGroup ?? '',
type: overrides.type ?? 'managed',
...overrides,
}
}

export function makeAdminRoleMember(overrides: Partial<AdminRoleMember> = {}): AdminRoleMember {
return {
id: crypto.randomUUID(),
email: 'user@example.com',
firstName: 'First',
lastName: 'Last',
...overrides,
}
}

export function makeCreateAdminRoleBody(overrides: { name?: string } = {}): z.infer<typeof adminRoleContract.createAdminRole.body> {
return {
name: overrides.name ?? 'New role',
}
}

export function makePatchAdminRoleBody(
role: AdminRole,
overrides: Partial<z.infer<typeof adminRoleContract.patchAdminRoles.body>[number]> = {},
): z.infer<typeof adminRoleContract.patchAdminRoles.body>[number] {
return {
id: role.id,
name: overrides.name ?? role.name,
permissions:
overrides.permissions
?? (typeof role.permissions === 'bigint' ? role.permissions.toString() : String(role.permissions)),
position: overrides.position ?? role.position,
oidcGroup: overrides.oidcGroup ?? role.oidcGroup,
type: overrides.type ?? role.type,
...overrides,
}
}
Loading
Loading