Skip to content

Commit 57aea78

Browse files
committed
refactor(keycloak): migrate Keycloak plugin to NestJS
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
1 parent fd9c07e commit 57aea78

36 files changed

Lines changed: 2843 additions & 93 deletions

AGENTS.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ Plugins use TS module augmentation to extend `ProjectStore` and `Config` interfa
3636
## Database (Prisma)
3737

3838
Multi-file schema in `apps/server/src/prisma/schema/*.prisma` (project, user, token, admin, topography).
39-
Singleton PrismaClient in `apps/server/src/prisma.ts`. Queries centralized per resource, re-exported via `queries-index.ts`.
4039
Migrations: standard Prisma Migrate. Major version data migrations in `migrations/v9/`.
4140

4241
## Environment config

apps/server-nestjs/.env-example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ KEYCLOAK_PROTOCOL=http
1515
KEYCLOAK_CLIENT_ID=dso-console-backend
1616
# Secret du client Keycloak backend (confidentiel)
1717
KEYCLOAK_CLIENT_SECRET=client-secret-backend
18+
# Identifiant de l'administrateur Keycloak
19+
KEYCLOAK_ADMIN=admin
20+
# Mot de passe de l'administrateur Keycloak
21+
KEYCLOAK_ADMIN_PASSWORD=admin
22+
# Identifiant administrateur Keycloak (utilisé pour l'API admin)
23+
KEYCLOAK_ADMIN=admin
24+
# Mot de passe administrateur Keycloak (confidentiel)
25+
KEYCLOAK_ADMIN_PASSWORD=admin
1826
# URL de redirection après authentification Keycloak
1927
KEYCLOAK_REDIRECT_URI=http://localhost:8080
2028
# Port d'écoute du serveur backend

apps/server-nestjs/.env.docker-example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ KEYCLOAK_PROTOCOL=http
1616
KEYCLOAK_CLIENT_ID=dso-console-backend
1717
# Secret du client Keycloak backend (confidentiel)
1818
KEYCLOAK_CLIENT_SECRET=client-secret-backend
19+
# Identifiant de l'administrateur Keycloak
20+
KEYCLOAK_ADMIN=admin
21+
# Mot de passe de l'administrateur Keycloak
22+
KEYCLOAK_ADMIN_PASSWORD=admin
1923
# URL de redirection après authentification Keycloak
2024
KEYCLOAK_REDIRECT_URI=http://localhost:8080
2125
# Port d'écoute du serveur dans le réseau Docker

apps/server-nestjs/.env.integ-example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ KEYCLOAK_CLIENT_SECRET=
1717
KEYCLOAK_DOMAIN=
1818
# Royaume Keycloak d'intégration
1919
KEYCLOAK_REALM=
20+
# Identifiant de l'administrateur Keycloak
21+
KEYCLOAK_ADMIN=
22+
# Mot de passe de l'administrateur Keycloak
23+
KEYCLOAK_ADMIN_PASSWORD=
2024

2125
# --- ArgoCD ---
2226
# Namespace Kubernetes dans lequel ArgoCD est déployé
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Architecture d’un module (pattern `apps/server-nestjs/src/modules/*`)
2+
3+
Les modules NestJS métier vivent dans `src/modules/<nom-du-module>/` et suivent un découpage “vertical slice” avec des responsabilités explicites : **client**, **service**, **controller service (orchestration)**, **datastore**, **utils** et **tests**.
4+
5+
Exemples concrets :
6+
- Module GitLab : `src/modules/gitlab/`
7+
- Module Keycloak : `src/modules/keycloak/`
8+
9+
## Structure type
10+
11+
```txt
12+
src/modules/<module>/
13+
├── <module>.module.ts
14+
├── <module>.constants.ts
15+
├── <module>-client.service.ts
16+
├── <module>.service.ts
17+
├── <module>-controller.service.ts
18+
├── <module>-datastore.service.ts
19+
├── <module>.utils.ts
20+
├── <module>-testing.utils.ts
21+
└── *.spec.ts
22+
```
23+
24+
## Sens des dépendances (flow recommandé)
25+
26+
Objectif : un flux de dépendances lisible et sans cycles.
27+
28+
```txt
29+
<module>-controller.service.ts
30+
31+
<module>.service.ts
32+
↙ ↘
33+
<module>-client <module>-datastore
34+
.service.ts .service.ts
35+
```
36+
37+
Règles pratiques :
38+
- Le `controller service` orchestre des workflows (cron, events, reconcile) et appelle le `service` et/ou le `datastore`.
39+
- Le `service` contient les règles métier (décisions, transformations, validations) et s’appuie sur le `client` et le `datastore`.
40+
- Le `client` encapsule l’accès à une API externe (initialisation + appels + erreurs bas niveau).
41+
- Le `datastore` encapsule l’accès DB (Prisma) et expose des méthodes de lecture/écriture typées.
42+
- Les `utils` restent “purs” (pas d’IO, pas d’injection Nest).
43+
- Les `testing utils` centralisent les factories/fixtures pour réduire la duplication dans les tests.
44+
45+
## Composants
46+
47+
### `<module>.module.ts`
48+
49+
Rôle :
50+
- Déclare les providers, imports, exports du module.
51+
- Exporte généralement le service principal du module.
52+
53+
### `<module>-client.service.ts`
54+
55+
Rôle :
56+
- Adapter vers le système externe (SDK HTTP, client Keycloak, client GitLab, etc.).
57+
- Conserver un contrat stable pour le reste du module.
58+
- Mapper/normaliser les erreurs externes si nécessaire.
59+
60+
À éviter :
61+
- Décisions métier (permissions, synchronisation, règles de purge) : elles vont dans `<module>.service.ts` ou le controller service.
62+
63+
### `<module>.service.ts`
64+
65+
Rôle :
66+
- Coeur métier du module (logique, mapping, validations, règles).
67+
- Appels aux dépendances (client/datastore) via des méthodes orientées domaine.
68+
69+
À éviter :
70+
- Cron/events : c’est le rôle du controller service.
71+
72+
### `<module>-controller.service.ts`
73+
74+
Rôle :
75+
- Orchestrateur de workflows : `@Cron`, `@OnEvent`, reconcile périodique, tâches “batch”.
76+
- Coordination entre datastore et service (et parfois appels directs au client pour des opérations transverses).
77+
- Garde-fous “safety” avant opérations destructrices (ex: suppression de groupes orphelins).
78+
79+
### `<module>-datastore.service.ts`
80+
81+
Rôle :
82+
- Accès DB via Prisma (select/include, transactions, pagination).
83+
- Exposition de types agrégés utiles au domaine (ex: `ProjectWithDetails`).
84+
85+
À éviter :
86+
- Appliquer des règles métier (ex: calcul de permissions) : on garde le datastore centré persistence.
87+
88+
### `<module>.utils.ts`
89+
90+
Rôle :
91+
- Fonctions utilitaires pures : mapping, helpers de collections, types partagés.
92+
- Aucune dépendance Nest, aucune lecture/écriture DB, aucun appel réseau.
93+
94+
### `<module>-testing.utils.ts`
95+
96+
Rôle :
97+
- Factories typées pour les structures fréquemment utilisées en tests.
98+
- Support d’`overrides` pour construire rapidement des variantes.
99+
- Centralisation des erreurs/fake responses spécifiques au module (quand utile).
100+
101+
## Tests (Vitest)
102+
103+
### `<module>.service.spec.ts`
104+
105+
Cible :
106+
- Logique métier : transformations, décisions, mapping d’erreurs.
107+
108+
Approche :
109+
- Mock du `client` et du `datastore`.
110+
- Pas d’IO réel.
111+
112+
### `<module>-controller.service.spec.ts`
113+
114+
Cible :
115+
- Orchestration : séquences d’appels, side-effects attendus, reconcile.
116+
117+
Approche :
118+
- Mock du `service`, du `datastore`, et des appels externes.
119+
- Vérification des appels effectués et des paramètres attendus.
120+
121+
### `<module>-datastore.service.spec.ts` (si présent)
122+
123+
Cible :
124+
- Forme des requêtes Prisma, mapping de résultat, typage de l’agrégat renvoyé.
125+
126+
Approche :
127+
- Mock de Prisma/DatabaseService, pas de logique métier.

apps/server-nestjs/package.json

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@
1515
"db:migrate": "prisma migrate dev --name dso",
1616
"db:reset": "prisma migrate reset",
1717
"format": "eslint ./ --fix",
18-
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18+
"prestart": "prisma generate",
1919
"start": "nest start",
2020
"start:debug": "nest start --debug --watch",
2121
"start:dev": "nest start --watch",
22-
"start:prod": "node dist/main"
22+
"start:prod": "node dist/main",
23+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
24+
"pretest": "prisma generate",
25+
"test": "vitest run",
26+
"test:watch": "vitest",
27+
"pretest:cov": "prisma generate",
28+
"test:cov": "vitest run --coverage",
29+
"test:debug": "vitest --inspect"
2330
},
2431
"dependencies": {
2532
"@cpn-console/argocd-plugin": "workspace:^",
@@ -38,12 +45,16 @@
3845
"@fastify/swagger-ui": "^4.2.0",
3946
"@gitbeaker/core": "^40.6.0",
4047
"@gitbeaker/rest": "^40.6.0",
48+
"@keycloak/keycloak-admin-client": "^24.0.0",
4149
"@kubernetes-models/argo-cd": "^2.7.2",
4250
"@nestjs/common": "^11.1.16",
4351
"@nestjs/config": "^4.0.3",
4452
"@nestjs/core": "^11.1.16",
53+
"@nestjs/event-emitter": "^3.0.1",
4554
"@nestjs/platform-express": "^11.1.16",
46-
"@prisma/client": "^6.19.2",
55+
"@nestjs/schedule": "^6.1.1",
56+
"@nestjs/terminus": "^11.1.1",
57+
"@opentelemetry/api": "^1.9.0",
4758
"@opentelemetry/auto-instrumentations-node": "^0.70.1",
4859
"@opentelemetry/exporter-metrics-otlp-proto": "^0.213.0",
4960
"@opentelemetry/exporter-trace-otlp-proto": "^0.213.0",
@@ -62,14 +73,18 @@
6273
"fastify": "^4.29.1",
6374
"fastify-keycloak-adapter": "2.3.2",
6475
"json-2-csv": "^5.5.10",
76+
"keycloak-connect": "^25.0.0",
6577
"mustache": "^4.2.0",
78+
"nest-keycloak-connect": "^1.10.1",
6679
"nestjs-pino": "^4.6.0",
6780
"pino-http": "^11.0.0",
6881
"prisma": "^6.19.2",
6982
"reflect-metadata": "^0.2.2",
7083
"rxjs": "^7.8.2",
7184
"undici": "^7.22.0",
72-
"vitest-mock-extended": "^2.0.2"
85+
"vitest-mock-extended": "^2.0.2",
86+
"yaml": "^2.8.2",
87+
"zod": "^3.25.76"
7388
},
7489
"devDependencies": {
7590
"@cpn-console/eslint-config": "workspace:^",
@@ -88,6 +103,7 @@
88103
"eslint": "^9.39.4",
89104
"fastify-plugin": "^5.1.0",
90105
"globals": "^16.5.0",
106+
"msw": "^2.12.10",
91107
"nodemon": "^3.1.14",
92108
"pino-pretty": "^13.1.3",
93109
"rimraf": "^6.1.3",

apps/server-nestjs/src/cpin-module/application-initialization/application-initialization-service/application-initialization.service.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ConfigurationModule } from '@/cpin-module/infrastructure/configuration/
77
import { PluginManagementService } from '../plugin-management/plugin-management.service'
88
import { DatabaseInitializationService } from '../database-initialization/database-initialization.service'
99
import { DatabaseService } from '@/cpin-module/infrastructure/database/database.service'
10+
import { PrismaService } from '@/cpin-module/infrastructure/database/prisma.service'
1011

1112
describe('applicationInitializationServiceService', () => {
1213
let service: ApplicationInitializationService
@@ -19,6 +20,10 @@ describe('applicationInitializationServiceService', () => {
1920
PluginManagementService,
2021
DatabaseInitializationService,
2122
DatabaseService,
23+
{
24+
provide: PrismaService,
25+
useValue: {},
26+
},
2227
],
2328
}).compile()
2429

apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@ import { Test } from '@nestjs/testing'
33
import { describe, beforeEach, it, expect } from 'vitest'
44

55
import { DatabaseInitializationService } from './database-initialization.service'
6+
import { PrismaService } from '@/cpin-module/infrastructure/database/prisma.service'
67

78
describe('databaseInitializationService', () => {
89
let service: DatabaseInitializationService
910

1011
beforeEach(async () => {
1112
const module: TestingModule = await Test.createTestingModule({
12-
providers: [DatabaseInitializationService],
13+
providers: [
14+
DatabaseInitializationService,
15+
{
16+
provide: PrismaService,
17+
useValue: {},
18+
},
19+
],
1320
}).compile()
1421

1522
service = module.get<DatabaseInitializationService>(

apps/server-nestjs/src/cpin-module/application-initialization/database-initialization/database-initialization.service.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import prisma from '@/prisma'
2-
import { Injectable, Logger } from '@nestjs/common'
1+
import { Inject, Injectable, Logger } from '@nestjs/common'
2+
import { PrismaService } from '@/cpin-module/infrastructure/database/prisma.service'
33

44
import { modelKeys } from './utils'
55

6-
type ExtractKeysWithFields<T> = {
7-
[K in keyof T]: T[K] extends { fields: any } ? K : never;
8-
}[keyof T]
9-
10-
type Models = ExtractKeysWithFields<typeof prisma>
11-
12-
type Imports = Partial<Record<Models, object[]>> & {
13-
associations: [Models, any[]]
6+
type ModelKey = (typeof modelKeys)[number]
7+
type Imports = Partial<Record<ModelKey, object[]>> & {
8+
associations: [ModelKey, any[]][]
149
}
1510

1611
@Injectable()
@@ -19,6 +14,8 @@ export class DatabaseInitializationService {
1914
DatabaseInitializationService.name,
2015
)
2116

17+
constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
18+
2219
async initDb(data: Imports) {
2320
const dataStringified = JSON.stringify(data)
2421
const dataParsed = JSON.parse(dataStringified, (key, value) => {

apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export class ConfigurationService {
2424
keycloakRealm = process.env.KEYCLOAK_REALM
2525
keycloakClientId = process.env.KEYCLOAK_CLIENT_ID
2626
keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET
27+
keycloakAdmin = process.env.KEYCLOAK_ADMIN
28+
keycloakAdminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD
2729
keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI
30+
2831
adminsUserId = process.env.ADMIN_KC_USER_ID
2932
? process.env.ADMIN_KC_USER_ID.split(',')
3033
: []

0 commit comments

Comments
 (0)