Table des matières
Sprint 1 — Authentification
Objectif
Implémenter le premier domaine métier réellement exécutable de la plateforme.
À l'issue du Sprint 1 :
✓ Register ✓ Login ✓ JWT ✓ Refresh Token ✓ RBAC ✓ Current User ✓ Swagger ✓ Unit Tests ✓ E2E Tests
Le système devient capable :
Créer un utilisateur S'authentifier Gérer les rôles Sécuriser les endpoints
Périmètre
Modules concernés
AuthModule UsersModule SecurityModule
Entités Prisma
User Role Permission UserRole RolePermission RefreshToken Session
Sprint 1-A — Modèle Prisma
Vérification User
model User {
id String @id @default(uuid())
tenantId String
email String @unique
passwordHash String
firstName String
lastName String
phone String?
status UserStatus
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(
fields:[tenantId],
references:[id]
)
roles UserRole[]
refreshTokens RefreshToken[]
sessions Session[]
}
RefreshToken
model RefreshToken {
id String @id @default(uuid())
userId String
tokenHash String
expiresAt DateTime
revokedAt DateTime?
createdAt DateTime @default(now())
user User
@relation(
fields:[userId],
references:[id]
)
}
Session
model Session {
id String @id @default(uuid())
userId String
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
lastSeenAt DateTime
user User
@relation(
fields:[userId],
references:[id]
)
}
Sprint 1-B — Installation
Packages NestJS
npm install @nestjs/jwt npm install @nestjs/passport npm install passport npm install passport-jwt npm install passport-local npm install bcrypt npm install class-validator npm install class-transformer
Types
npm install -D @types/bcrypt @types/passport-jwt @types/passport-local
Sprint 1-C — Structure Module
Arborescence
src/modules/auth ├── application ├── domain ├── infrastructure ├── presentation └── auth.module.ts
Application
commands queries handlers dto
Sprint 1-D — DTO
RegisterDto
export class RegisterDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsString()
firstName: string;
@IsString()
lastName: string;
}
LoginDto
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
RefreshTokenDto
export class RefreshTokenDto {
@IsString()
refreshToken: string;
}
Sprint 1-E — Service Password
PasswordService
src/modules/auth/domain/services
Méthodes
hash() compare()
Implémentation
const hash = await bcrypt.hash(password, 12);
Sprint 1-F — JWT
Variables
JWT_SECRET JWT_EXPIRES_IN REFRESH_SECRET REFRESH_EXPIRES_IN
.env
JWT_SECRET=super-secret JWT_EXPIRES_IN=15m REFRESH_SECRET=refresh-secret REFRESH_EXPIRES_IN=30d
Payload
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
}
Sprint 1-G — Register
Endpoint
POST /auth/register
Workflow
Validation ↓ Email unique ↓ Hash password ↓ Create User ↓ Assign Role USER ↓ Return JWT
Response
{
"accessToken": "...",
"refreshToken": "...",
"user": {}
}
Sprint 1-H — Login
Endpoint
POST /auth/login
Workflow
Find User ↓ Compare Password ↓ Generate JWT ↓ Generate Refresh Token ↓ Create Session
Sprint 1-I — Refresh Token
Endpoint
POST /auth/refresh
Workflow
Validate Refresh Token ↓ Generate New JWT ↓ Rotate Refresh Token
Sprint 1-J — Logout
Endpoint
POST /auth/logout
Action
Révocation Refresh Token Suppression Session
Sprint 1-K — Current User
Endpoint
GET /auth/me
Response
{
"id": "...",
"email": "...",
"roles": ["USER"]
}
Sprint 1-L — JWT Strategy
Fichier
jwt.strategy.ts
Validation
Bearer Token ↓ Verify JWT ↓ Load User ↓ Inject Request.user
Sprint 1-M — RBAC
Decorator
@Roles('ADMIN')
Guard
RolesGuard
Exemple
@Roles('ADMIN')
@Get()
findAll()
Sprint 1-N — Permissions
Decorator
@Permissions( 'users.read' )
Guard
PermissionsGuard
Source
RolePermission Permission
Sprint 1-O — Swagger
Tag
@ApiTags('Auth')
Security
@ApiBearerAuth()
Endpoints
POST /auth/register POST /auth/login POST /auth/refresh POST /auth/logout GET /auth/me
Sprint 1-P — Seed Initial
Permissions
users.read users.create users.update users.delete
Roles
SUPER_ADMIN ADMIN MANAGER OWNER CUSTOMER
Super Admin
admin@platform.local
Sprint 1-Q — Tests Unitaires
Services
AuthService PasswordService JwtService RolesGuard PermissionsGuard
Commande
nx test api
Sprint 1-R — Tests E2E
Scénario
Register ↓ Login ↓ Access Protected Route ↓ Refresh Token ↓ Logout
Commande
nx e2e api-e2e
Sprint 1-S — Critères d'acceptation
Register
Utilisateur créé Mot de passe hashé JWT retourné
Login
JWT valide Refresh Token valide
RBAC
ADMIN autorisé USER refusé
Swagger
100 % documenté
Livrables
AuthModule JwtStrategy AuthController AuthService RolesGuard PermissionsGuard RegisterDto LoginDto RefreshTokenDto Swagger Tests
Sprint 1 — Authentification
Objectif
Implémenter le premier domaine métier réellement exécutable de la plateforme.
À l'issue du Sprint 1 :
✓ Register ✓ Login ✓ JWT ✓ Refresh Token ✓ RBAC ✓ Current User ✓ Swagger ✓ Unit Tests ✓ E2E Tests
Le système devient capable :
Créer un utilisateur S'authentifier Gérer les rôles Sécuriser les endpoints
Périmètre
Modules concernés
AuthModule UsersModule SecurityModule
Entités Prisma
User Role Permission UserRole RolePermission RefreshToken Session
Sprint 1-A — Modèle Prisma
Vérification User
model User {
id String @id @default(uuid())
tenantId String
email String @unique
passwordHash String
firstName String
lastName String
phone String?
status UserStatus
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(
fields:[tenantId],
references:[id]
)
roles UserRole[]
refreshTokens RefreshToken[]
sessions Session[]
}
RefreshToken
model RefreshToken {
id String @id @default(uuid())
userId String
tokenHash String
expiresAt DateTime
revokedAt DateTime?
createdAt DateTime @default(now())
user User
@relation(
fields:[userId],
references:[id]
)
}
Session
model Session {
id String @id @default(uuid())
userId String
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
lastSeenAt DateTime
user User
@relation(
fields:[userId],
references:[id]
)
}
Sprint 1-B — Installation
Packages NestJS
npm install @nestjs/jwt npm install @nestjs/passport npm install passport npm install passport-jwt npm install passport-local npm install bcrypt npm install class-validator npm install class-transformer
Types
npm install -D @types/bcrypt @types/passport-jwt @types/passport-local
Sprint 1-C — Structure Module
Arborescence
src/modules/auth ├── application ├── domain ├── infrastructure ├── presentation └── auth.module.ts
Application
commands queries handlers dto
Sprint 1-D — DTO
RegisterDto
export class RegisterDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsString()
firstName: string;
@IsString()
lastName: string;
}
LoginDto
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
RefreshTokenDto
export class RefreshTokenDto {
@IsString()
refreshToken: string;
}
Sprint 1-E — Service Password
PasswordService
src/modules/auth/domain/services
Méthodes
hash() compare()
Implémentation
const hash = await bcrypt.hash(password, 12);
Sprint 1-F — JWT
Variables
JWT_SECRET JWT_EXPIRES_IN REFRESH_SECRET REFRESH_EXPIRES_IN
.env
JWT_SECRET=super-secret JWT_EXPIRES_IN=15m REFRESH_SECRET=refresh-secret REFRESH_EXPIRES_IN=30d
Payload
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
}
Sprint 1-G — Register
Endpoint
POST /auth/register
Workflow
Validation ↓ Email unique ↓ Hash password ↓ Create User ↓ Assign Role USER ↓ Return JWT
Response
{
"accessToken": "...",
"refreshToken": "...",
"user": {}
}
Sprint 1-H — Login
Endpoint
POST /auth/login
Workflow
Find User ↓ Compare Password ↓ Generate JWT ↓ Generate Refresh Token ↓ Create Session
Sprint 1-I — Refresh Token
Endpoint
POST /auth/refresh
Workflow
Validate Refresh Token ↓ Generate New JWT ↓ Rotate Refresh Token
Sprint 1-J — Logout
Endpoint
POST /auth/logout
Action
Révocation Refresh Token Suppression Session
Sprint 1-K — Current User
Endpoint
GET /auth/me
Response
{
"id": "...",
"email": "...",
"roles": ["USER"]
}
Sprint 1-L — JWT Strategy
Fichier
jwt.strategy.ts
Validation
Bearer Token ↓ Verify JWT ↓ Load User ↓ Inject Request.user
Sprint 1-M — RBAC
Decorator
@Roles('ADMIN')
Guard
RolesGuard
Exemple
@Roles('ADMIN')
@Get()
findAll()
Sprint 1-N — Permissions
Decorator
@Permissions( 'users.read' )
Guard
PermissionsGuard
Source
RolePermission Permission
Sprint 1-O — Swagger
Tag
@ApiTags('Auth')
Security
@ApiBearerAuth()
Endpoints
POST /auth/register POST /auth/login POST /auth/refresh POST /auth/logout GET /auth/me
Sprint 1-P — Seed Initial
Permissions
users.read users.create users.update users.delete
Roles
SUPER_ADMIN ADMIN MANAGER OWNER CUSTOMER
Super Admin
admin@platform.local
Sprint 1-Q — Tests Unitaires
Services
AuthService PasswordService JwtService RolesGuard PermissionsGuard
Commande
nx test api
Sprint 1-R — Tests E2E
Scénario
Register ↓ Login ↓ Access Protected Route ↓ Refresh Token ↓ Logout
Commande
nx e2e api-e2e
Sprint 1-S — Critères d'acceptation
Register
Utilisateur créé Mot de passe hashé JWT retourné
Login
JWT valide Refresh Token valide
RBAC
ADMIN autorisé USER refusé
Swagger
100 % documenté
Livrables
AuthModule JwtStrategy AuthController AuthService RolesGuard PermissionsGuard RegisterDto LoginDto RefreshTokenDto Swagger Tests
Sprint 1-A.1 — Implémentation Prisma Auth
Objectif
Implémenter réellement les entités Prisma nécessaires à :
Register Login JWT Refresh Token RBAC Sessions
avant toute écriture de code NestJS.
Cette étape produit :
User Role Permission UserRole RolePermission RefreshToken Session
dans le fichier :
apps/api/prisma/schema.prisma
Étape 1 — Vérification des Enums
Ajouter
enum UserStatus {
ACTIVE
INACTIVE
BLOCKED
PENDING
}
Étape 2 — Modèle User
Ajouter
model User {
id String @id @default(uuid())
tenantId String
email String @unique
passwordHash String
firstName String
lastName String
phone String?
status UserStatus @default(ACTIVE)
emailVerified Boolean @default(false)
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
createdBy String?
updatedBy String?
tenant Tenant
@relation(
fields:[tenantId],
references:[id]
)
userRoles UserRole[]
refreshTokens RefreshToken[]
sessions Session[]
@@index([tenantId])
@@index([email])
@@index([status])
@@index([deletedAt])
}
Étape 3 — Modèle Role
Ajouter
model Role {
id String @id @default(uuid())
tenantId String
code String
name String
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant
@relation(
fields:[tenantId],
references:[id]
)
userRoles UserRole[]
rolePermissions RolePermission[]
@@unique([tenantId, code])
@@index([tenantId])
@@index([code])
}
Étape 4 — Modèle Permission
Ajouter
model Permission {
id String @id @default(uuid())
code String @unique
name String
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
rolePermissions RolePermission[]
@@index([code])
}
Étape 5 — Modèle UserRole
Ajouter
model UserRole {
id String @id @default(uuid())
userId String
roleId String
assignedAt DateTime @default(now())
assignedBy String?
user User
@relation(
fields:[userId],
references:[id]
)
role Role
@relation(
fields:[roleId],
references:[id]
)
@@unique([userId, roleId])
@@index([userId])
@@index([roleId])
}
Étape 6 — Modèle RolePermission
Ajouter
model RolePermission {
id String @id @default(uuid())
roleId String
permissionId String
role Role
@relation(
fields:[roleId],
references:[id]
)
permission Permission
@relation(
fields:[permissionId],
references:[id]
)
@@unique([
roleId,
permissionId
])
@@index([roleId])
@@index([permissionId])
}
Étape 7 — Modèle RefreshToken
Ajouter
model RefreshToken {
id String @id @default(uuid())
userId String
tokenHash String
expiresAt DateTime
revokedAt DateTime?
createdAt DateTime @default(now())
user User
@relation(
fields:[userId],
references:[id]
)
@@index([userId])
@@index([expiresAt])
@@index([revokedAt])
}
Étape 8 — Modèle Session
Ajouter
model Session {
id String @id @default(uuid())
userId String
ipAddress String?
country String?
userAgent String?
device String?
lastSeenAt DateTime
createdAt DateTime @default(now())
revokedAt DateTime?
user User
@relation(
fields:[userId],
references:[id]
)
@@index([userId])
@@index([lastSeenAt])
@@index([revokedAt])
}
Étape 9 — Vérification des relations Tenant
Vérifier
Le modèle :
Tenant
doit contenir :
users User[] roles Role[]
Exemple
model Tenant {
id String @id @default(uuid())
code String @unique
name String
active Boolean @default(true)
users User[]
roles Role[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Étape 10 — Validation Prisma
Vérifier
cd apps/api npx prisma validate
Résultat attendu
The schema at prisma/schema.prisma is valid
Étape 11 — Formatage
npx prisma format
Étape 12 — Création de la migration
Générer
npx prisma migrate dev \
--name auth_module
Prisma génère
prisma/migrations
└── xxxx_auth_module
└── migration.sql
Étape 13 — Génération du Client Prisma
Exécuter
npx prisma generate
Étape 14 — Vérification SQL
Ouvrir PostgreSQL
docker exec -it postgres psql -U postgres
Base
\c rental_platform
Vérifier
\d "User" \d "Role" \d "Permission" \d "UserRole" \d "RolePermission" \d "RefreshToken" \d "Session"
Définition de terminé
Le Sprint 1-A.1 est terminé lorsque :
✓ User créé ✓ Role créé ✓ Permission créée ✓ UserRole créé ✓ RolePermission créé ✓ RefreshToken créé ✓ Session créée ✓ Migration exécutée ✓ Prisma Client généré ✓ Validation Prisma verte
Livrables
schema.prisma migration auth_module Prisma Client
Sprint 1-A.2 — Prisma Seed Auth
Objectif
Créer automatiquement les données minimales nécessaires au démarrage de la plateforme.
À l'issue de cette étape :
✓ Tenant principal créé ✓ Permissions créées ✓ Rôles créés ✓ Super Administrateur créé ✓ Affectation des permissions ✓ Affectation du rôle Super Admin ✓ Première connexion possible
Architecture du Seed
Fichier
apps/api/prisma/seed.ts
Responsabilités
Créer Tenant Créer Permissions Créer Rôles Créer Super Admin Associer les permissions Associer les rôles
Étape 1 — Installation bcrypt
Vérifier
npm install bcrypt npm install -D @types/bcrypt
Étape 2 — Configuration package.json
Ajouter
{
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
Installer TSX
npm install -D tsx
Étape 3 — Initialisation du Seed
Créer
apps/api/prisma/seed.ts
Base
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
console.log('Starting seed...');
}
main()
.then(() => prisma.$disconnect())
.catch(async (error) => {
console.error(error);
await prisma.$disconnect();
process.exit(1);
});
Étape 4 — Création du Tenant principal
Ajouter
const tenant =
await prisma.tenant.upsert({
where: {
code: 'MAIN'
},
update: {},
create: {
code: 'MAIN',
name: 'Main Tenant',
active: true
}
});
Résultat
MAIN
devient le tenant par défaut.
Étape 5 — Création des Permissions
Liste minimale
users.read users.create users.update users.delete roles.read roles.manage properties.read properties.create properties.update properties.delete reservations.read reservations.create reservations.update reservations.delete payments.read payments.create crm.read crm.manage security.read security.manage
Implémentation
const permissions = [ 'users.read', 'users.create', 'users.update', 'users.delete', 'roles.read', 'roles.manage', 'properties.read', 'properties.create', 'properties.update', 'properties.delete', 'reservations.read', 'reservations.create', 'reservations.update', 'reservations.delete', 'payments.read', 'payments.create', 'crm.read', 'crm.manage', 'security.read', 'security.manage' ];
Création
for (const code of permissions) {
await prisma.permission.upsert({
where: { code },
update: {},
create: {
code,
name: code
}
});
}
Étape 6 — Création des rôles
Rôles système
SUPER_ADMIN ADMIN MANAGER OWNER CUSTOMER
Création
const superAdminRole =
await prisma.role.upsert({
where: {
tenantId_code: {
tenantId: tenant.id,
code: 'SUPER_ADMIN'
}
},
update: {},
create: {
tenantId: tenant.id,
code: 'SUPER_ADMIN',
name: 'Super Administrateur'
}
});
Répéter pour
ADMIN MANAGER OWNER CUSTOMER
Étape 7 — Attribution des permissions
Charger
const allPermissions = await prisma.permission.findMany();
Affectation
for (const permission of allPermissions) {
await prisma.rolePermission.upsert({
where: {
roleId_permissionId: {
roleId: superAdminRole.id,
permissionId: permission.id
}
},
update: {},
create: {
roleId: superAdminRole.id,
permissionId: permission.id
}
});
}
Résultat
SUPER_ADMIN ↓ Toutes les permissions
Étape 8 — Création du compte Super Admin
Variables
admin@platform.local Admin123!
Hash
const passwordHash =
await bcrypt.hash(
'Admin123!',
12
);
Création
const admin =
await prisma.user.upsert({
where: {
email: 'admin@platform.local'
},
update: {},
create: {
tenantId: tenant.id,
email: 'admin@platform.local',
passwordHash,
firstName: 'Super',
lastName: 'Admin',
emailVerified: true,
status: 'ACTIVE'
}
});
Étape 9 — Affectation du rôle
Ajouter
await prisma.userRole.upsert({
where: {
userId_roleId: {
userId: admin.id,
roleId: superAdminRole.id
}
},
update: {},
create: {
userId: admin.id,
roleId: superAdminRole.id
}
});
Étape 10 — Logs
Ajouter
console.log( 'Tenant created:', tenant.code ); console.log( 'Admin:', admin.email ); console.log( 'Seed completed' );
Étape 11 — Exécution
Lancer
cd apps/api npx prisma db seed
Résultat attendu
Starting seed... Tenant created: MAIN Admin: admin@platform.local Seed completed
Étape 12 — Vérification Prisma Studio
Ouvrir
npx prisma studio
Vérifier
Tenant User Role Permission UserRole RolePermission
Étape 13 — Vérification SQL
Utilisateur
SELECT email FROM "User";
Résultat
admin@platform.local
Permissions
SELECT COUNT(*) FROM "Permission";
Résultat
> 15
Sécurisation future
Ne jamais conserver
Admin123!
en production.
Prévoir
ADMIN_EMAIL ADMIN_PASSWORD
dans :
.env
pour les environnements réels.
Définition de terminé
Le Sprint 1-A.2 est terminé lorsque :
✓ Tenant créé ✓ Permissions créées ✓ Rôles créés ✓ Super Admin créé ✓ Affectation des permissions ✓ Affectation du rôle ✓ Seed exécutable ✓ Première connexion possible
Livrables
prisma/seed.ts package.json Permissions système Rôles système Compte Super Admin
Sprint 1-B.1 — Création du AuthModule NestJS
Objectif
Implémenter le premier module métier réellement exécutable de la plateforme.
À l'issue de cette étape :
✓ AuthModule ✓ AuthController ✓ AuthService ✓ PasswordService ✓ JwtStrategy ✓ JwtAuthGuard ✓ Register ✓ Login ✓ Refresh ✓ Logout ✓ Current User
Étape 1 — Création de la structure
Répertoire
Créer :
apps/api/src/modules/auth
Arborescence
auth ├── application │ │ ├── dto │ ├── commands │ ├── queries │ └── handlers │ ├── domain │ │ └── services │ ├── infrastructure │ │ └── strategies │ ├── presentation │ │ ├── controllers │ └── guards │ └── auth.module.ts
Étape 2 — Création du AuthModule
Fichier
auth.module.ts
Implémentation
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './presentation/controllers/auth.controller';
import { AuthService } from './domain/services/auth.service';
import { PasswordService } from './domain/services/password.service';
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
@Module({
imports: [
JwtModule.register({})
],
controllers: [
AuthController
],
providers: [
AuthService,
PasswordService,
JwtStrategy
],
exports: [
AuthService
]
})
export class AuthModule {}
Étape 3 — RegisterDto
Fichier
application/dto/register.dto.ts
Code
import {
IsEmail,
IsString,
MinLength
} from 'class-validator';
export class RegisterDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsString()
firstName: string;
@IsString()
lastName: string;
}
Étape 4 — LoginDto
Fichier
application/dto/login.dto.ts
Code
import {
IsEmail,
IsString
} from 'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
Étape 5 — RefreshTokenDto
Fichier
application/dto/refresh-token.dto.ts
Code
import {
IsString
} from 'class-validator';
export class RefreshTokenDto {
@IsString()
refreshToken: string;
}
Étape 6 — PasswordService
Fichier
domain/services/password.service.ts
Implémentation
import * as bcrypt from 'bcrypt';
export class PasswordService {
async hash(
password: string
): Promise<string> {
return bcrypt.hash(
password,
12
);
}
async compare(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(
password,
hash
);
}
}
Étape 7 — AuthService
Fichier
domain/services/auth.service.ts
Responsabilités
Register Login Refresh Logout Current User
Squelette
@Injectable()
export class AuthService {
async register() {}
async login() {}
async refresh() {}
async logout() {}
async me() {}
}
Étape 8 — JWT Payload
Fichier
infrastructure/strategies/jwt-payload.interface.ts
Code
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
}
Étape 9 — JwtStrategy
Fichier
infrastructure/strategies/jwt.strategy.ts
Implémentation
@Injectable()
export class JwtStrategy
extends PassportStrategy(
Strategy
) {
constructor() {
super({
jwtFromRequest:
ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey:
process.env.JWT_SECRET
});
}
async validate(
payload: JwtPayload
) {
return payload;
}
}
Étape 10 — JwtAuthGuard
Fichier
presentation/guards/jwt-auth.guard.ts
Code
@Injectable()
export class JwtAuthGuard
extends AuthGuard('jwt') {}
Étape 11 — AuthController
Fichier
presentation/controllers/auth.controller.ts
Déclaration
@ApiTags('Auth')
@Controller('auth')
export class AuthController {
}
Étape 12 — Endpoint Register
Route
POST /auth/register
Code
@Post('register')
register(
@Body()
dto: RegisterDto
) {
return this.authService
.register(dto);
}
Étape 13 — Endpoint Login
Route
POST /auth/login
Code
@Post('login')
login(
@Body()
dto: LoginDto
) {
return this.authService
.login(dto);
}
Étape 14 — Endpoint Refresh
Route
POST /auth/refresh
Code
@Post('refresh')
refresh(
@Body()
dto: RefreshTokenDto
) {
return this.authService
.refresh(dto);
}
Étape 15 — Endpoint Logout
Route
POST /auth/logout
Sécurisé
@UseGuards( JwtAuthGuard )
Code
@Post('logout')
logout(
@Req() req
) {
return this.authService
.logout(
req.user.sub
);
}
Étape 16 — Endpoint Me
Route
GET /auth/me
Sécurisé
@UseGuards( JwtAuthGuard )
Code
@Get('me')
me(
@Req() req
) {
return this.authService
.me(
req.user.sub
);
}
Étape 17 — Configuration JWT
.env
JWT_SECRET=change-me JWT_EXPIRES_IN=15m REFRESH_SECRET=change-me-too REFRESH_EXPIRES_IN=30d
Étape 18 — Import Module
AppModule
Ajouter :
imports: [ AuthModule ]
Étape 19 — Vérification compilation
Build
nx build api
Résultat attendu
SUCCESS
Étape 20 — Swagger
Vérification
/auth/register /auth/login /auth/refresh /auth/logout /auth/me
doivent apparaître dans :
http://localhost:3000/api
Définition de terminé
Le Sprint 1-B.1 est terminé lorsque :
✓ AuthModule créé ✓ DTO créés ✓ PasswordService créé ✓ JwtStrategy créée ✓ JwtAuthGuard créé ✓ AuthController créé ✓ Endpoints exposés ✓ Swagger visible ✓ Build vert
Livrables
AuthModule AuthController AuthService PasswordService JwtStrategy JwtAuthGuard RegisterDto LoginDto RefreshTokenDto
Sprint 1-B.2 — Implémentation réelle du Register
Objectif
Rendre le endpoint :
POST /auth/register
entièrement fonctionnel.
À l'issue de cette étape :
✓ Vérification email unique ✓ Hash du mot de passe ✓ Création User ✓ Attribution rôle CUSTOMER ✓ Création RefreshToken ✓ Génération JWT ✓ Retour utilisateur connecté
Workflow complet
Register Request ↓ Validation DTO ↓ Vérification email ↓ Hash Password ↓ Create User ↓ Assign CUSTOMER Role ↓ Generate JWT ↓ Generate Refresh Token ↓ Persist Refresh Token ↓ Response
Étape 1 — Création des types de réponse
AuthResponse
Créer :
application/dto/auth-response.dto.ts
Code
export class AuthResponseDto {
accessToken: string;
refreshToken: string;
user: {
id: string;
email: string;
firstName: string;
lastName: string;
};
}
Étape 2 — Injection Prisma
Prérequis
Le :
PrismaModule
doit exporter :
PrismaService
AuthModule
Ajouter :
imports: [
PrismaModule,
JwtModule.register({})
]
Étape 3 — Injection des dépendances
AuthService
Modifier :
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly passwordService: PasswordService
) {}
}
Étape 4 — Recherche du rôle CUSTOMER
Constante
Créer :
shared/constants/roles.constants.ts
Code
export const SYSTEM_ROLES = {
SUPER_ADMIN: 'SUPER_ADMIN',
ADMIN: 'ADMIN',
MANAGER: 'MANAGER',
OWNER: 'OWNER',
CUSTOMER: 'CUSTOMER'
};
Étape 5 — Création du Register
Signature
async register( dto: RegisterDto ): Promise<AuthResponseDto>
Étape 6 — Vérification email
Ajouter
const existingUser =
await this.prisma.user.findUnique({
where: {
email: dto.email
}
});
if (existingUser) {
throw new ConflictException(
'Email already exists'
);
}
Étape 7 — Charger le Tenant principal
Ajouter
const tenant =
await this.prisma.tenant.findUnique({
where: {
code: 'MAIN'
}
});
if (!tenant) {
throw new Error(
'Main tenant not found'
);
}
Étape 8 — Hash mot de passe
Ajouter
const passwordHash =
await this.passwordService.hash(
dto.password
);
Étape 9 — Création User
Ajouter
const user =
await this.prisma.user.create({
data: {
tenantId: tenant.id,
email: dto.email,
passwordHash,
firstName: dto.firstName,
lastName: dto.lastName,
status: 'ACTIVE'
}
});
Étape 10 — Charger rôle CUSTOMER
Ajouter
const customerRole =
await this.prisma.role.findFirst({
where: {
tenantId: tenant.id,
code: SYSTEM_ROLES.CUSTOMER
}
});
if (!customerRole) {
throw new Error(
'Customer role missing'
);
}
Étape 11 — Affecter le rôle
Ajouter
await this.prisma.userRole.create({
data: {
userId: user.id,
roleId: customerRole.id
}
});
Étape 12 — Génération JWT
Payload
const payload = {
sub: user.id,
tenantId: tenant.id,
email: user.email
};
Token
const accessToken =
await this.jwtService.signAsync(
payload,
{
secret:
process.env.JWT_SECRET,
expiresIn:
process.env.JWT_EXPIRES_IN
}
);
Étape 13 — Génération Refresh Token
Token brut
const refreshToken = randomUUID();
Hash
const refreshTokenHash =
await this.passwordService.hash(
refreshToken
);
Sauvegarde
await this.prisma.refreshToken.create({
data: {
userId: user.id,
tokenHash: refreshTokenHash,
expiresAt: addDays(
new Date(),
30
)
}
});
Étape 14 — Réponse
Retour
return {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
}
};
Étape 15 — Controller
Route
@Post('register')
@ApiOperation({
summary:
'Register a new user'
})
Response
@ApiCreatedResponse({
type: AuthResponseDto
})
Étape 16 — Validation globale
main.ts
Vérifier :
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true
})
);
Étape 17 — Test manuel
Requête
POST /auth/register
Body
{
"email": "john@example.com",
"password": "Password123!",
"firstName": "John",
"lastName": "Doe"
}
Réponse attendue
{
"accessToken": "...",
"refreshToken": "...",
"user": {
"id": "...",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe"
}
}
Étape 18 — Vérification SQL
User
SELECT email FROM "User";
UserRole
SELECT * FROM "UserRole";
RefreshToken
SELECT * FROM "RefreshToken";
Cas d'erreur
Email déjà utilisé
{
"statusCode": 409,
"message":
"Email already exists"
}
Tenant absent
{
"statusCode": 500,
"message":
"Main tenant not found"
}
Définition de terminé
Le Sprint 1-B.2 est terminé lorsque :
✓ Register fonctionnel ✓ User créé ✓ Password hashé ✓ CUSTOMER assigné ✓ JWT généré ✓ Refresh Token généré ✓ Swagger documenté ✓ Test manuel réussi
Livrables
AuthService.register() AuthResponseDto roles.constants.ts Swagger Register Validation complète
Sprint 1-B.3 — Implémentation réelle du Login
Objectif
Rendre le endpoint :
POST /auth/login
entièrement fonctionnel.
À l'issue de cette étape :
✓ Recherche utilisateur ✓ Vérification mot de passe ✓ Vérification statut utilisateur ✓ Création Session ✓ Génération JWT ✓ Génération Refresh Token ✓ Historisation connexion ✓ Retour utilisateur authentifié
Workflow complet
Login Request ↓ Find User ↓ Verify Password ↓ Verify Status ↓ Create Session ↓ Generate JWT ↓ Generate Refresh Token ↓ Store Refresh Token ↓ Return Auth Response
Étape 1 — Création de SessionDto
Créer
application/dto/session-info.dto.ts
Contenu
export class SessionInfoDto {
ipAddress?: string;
userAgent?: string;
}
Étape 2 — Mise à jour AuthController
Login
Modifier :
@Post('login')
login(
@Body()
dto: LoginDto,
@Req()
request: Request
) {
return this.authService.login(
dto,
{
ipAddress: request.ip,
userAgent:
request.headers['user-agent']
}
);
}
Étape 3 — Signature Login
AuthService
Modifier :
async login( dto: LoginDto, session: SessionInfoDto ): Promise<AuthResponseDto>
Étape 4 — Recherche utilisateur
Ajouter
const user =
await this.prisma.user.findUnique({
where: {
email: dto.email
},
include: {
userRoles: {
include: {
role: true
}
}
}
});
Vérification
if (!user) {
throw new UnauthorizedException(
'Invalid credentials'
);
}
Étape 5 — Vérification statut
Ajouter
if (
user.status !== 'ACTIVE'
) {
throw new ForbiddenException(
'Account disabled'
);
}
Étape 6 — Vérification mot de passe
Ajouter
const validPassword =
await this.passwordService.compare(
dto.password,
user.passwordHash
);
Contrôle
if (!validPassword) {
throw new UnauthorizedException(
'Invalid credentials'
);
}
Étape 7 — Génération du JWT
Payload
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email
};
Génération
const accessToken =
await this.jwtService.signAsync(
payload,
{
secret:
process.env.JWT_SECRET,
expiresIn:
process.env.JWT_EXPIRES_IN
}
);
Étape 8 — Génération Refresh Token
Token
const refreshToken = randomUUID();
Hash
const refreshTokenHash =
await this.passwordService.hash(
refreshToken
);
Sauvegarde
await this.prisma.refreshToken.create({
data: {
userId: user.id,
tokenHash: refreshTokenHash,
expiresAt: addDays(
new Date(),
30
)
}
});
Étape 9 — Création Session
Ajouter
await this.prisma.session.create({
data: {
userId: user.id,
ipAddress:
session.ipAddress,
userAgent:
session.userAgent,
lastSeenAt:
new Date()
}
});
Étape 10 — Historisation connexion
Mise à jour User
await this.prisma.user.update({
where: {
id: user.id
},
data: {
lastLoginAt:
new Date()
}
});
Étape 11 — Chargement des rôles
Extraire
const roles =
user.userRoles.map(
item => item.role.code
);
Étape 12 — Retour réponse
Réponse
return {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
roles
}
};
Étape 13 — Swagger
Login
Ajouter :
@ApiOperation({
summary:
'Authenticate user'
})
Response
@ApiOkResponse({
type: AuthResponseDto
})
Étape 14 — Cas d'erreurs
Mauvais mot de passe
{
"statusCode": 401,
"message":
"Invalid credentials"
}
Utilisateur absent
{
"statusCode": 401,
"message":
"Invalid credentials"
}
Compte bloqué
{
"statusCode": 403,
"message":
"Account disabled"
}
Étape 15 — Test Swagger
Requête
POST /auth/login
Body
{
"email": "admin@platform.local",
"password": "Admin123!"
}
Réponse
{
"accessToken": "...",
"refreshToken": "...",
"user": {
"id": "...",
"email": "admin@platform.local",
"firstName": "Super",
"lastName": "Admin",
"roles": [
"SUPER_ADMIN"
]
}
}
Étape 16 — Vérification SQL
Session
SELECT * FROM "Session";
RefreshToken
SELECT * FROM "RefreshToken";
User
SELECT email, "lastLoginAt" FROM "User";
Refactoring recommandé
À partir de maintenant, factoriser la génération des tokens.
Créer :
domain/services/token.service.ts
Responsabilités
generateAccessToken() generateRefreshToken() verifyRefreshToken()
Définition de terminé
Le Sprint 1-B.3 est terminé lorsque :
✓ Login fonctionnel ✓ Password vérifié ✓ Statut vérifié ✓ JWT généré ✓ Refresh Token généré ✓ Session créée ✓ Historique connexion mis à jour ✓ Swagger documenté ✓ Test manuel réussi
Livrables
AuthService.login() SessionInfoDto Swagger Login Création Session Historisation connexion
Sprint 1-B.4 — Implémentation réelle du Refresh Token
Objectif
Rendre le endpoint :
POST /auth/refresh
entièrement fonctionnel.
À l'issue de cette étape :
✓ Validation Refresh Token ✓ Recherche utilisateur ✓ Vérification expiration ✓ Rotation Refresh Token ✓ Révocation ancien token ✓ Nouveau JWT ✓ Nouveau Refresh Token ✓ Réponse authentifiée
Workflow complet
Refresh Request ↓ Find Valid Refresh Token ↓ Verify Token ↓ Load User ↓ Revoke Old Token ↓ Generate New JWT ↓ Generate New Refresh Token ↓ Persist New Refresh Token ↓ Return Tokens
Étape 1 — Création du TokenService
Créer
src/modules/auth/domain/services/token.service.ts
Responsabilités
generateAccessToken() generateRefreshToken() hashRefreshToken() verifyRefreshToken()
Squelette
@Injectable()
export class TokenService {
constructor(
private readonly jwtService: JwtService,
private readonly passwordService: PasswordService
) {}
}
Étape 2 — Génération Access Token
Ajouter
async generateAccessToken(
payload: JwtPayload
): Promise<string> {
return this.jwtService.signAsync(
payload,
{
secret:
process.env.JWT_SECRET,
expiresIn:
process.env.JWT_EXPIRES_IN
}
);
}
Étape 3 — Génération Refresh Token
Ajouter
async generateRefreshToken() {
const rawToken =
randomUUID();
return rawToken;
}
Étape 4 — Hash Refresh Token
Ajouter
async hashRefreshToken(
token: string
) {
return this.passwordService.hash(
token
);
}
Étape 5 — Enregistrement Refresh Token
Ajouter
async storeRefreshToken(
userId: string,
token: string
) {
const tokenHash =
await this.hashRefreshToken(
token
);
return prisma.refreshToken.create({
data: {
userId,
tokenHash,
expiresAt: addDays(
new Date(),
30
)
}
});
}
Étape 6 — Injection dans AuthModule
Ajouter
providers: [ AuthService, PasswordService, TokenService, JwtStrategy ]
Étape 7 — Injection dans AuthService
Constructeur
constructor(
private readonly prisma:
PrismaService,
private readonly tokenService:
TokenService,
private readonly passwordService:
PasswordService
) {}
Étape 8 — Signature Refresh
AuthService
async refresh( dto: RefreshTokenDto ): Promise<AuthResponseDto>
Étape 9 — Charger les Refresh Tokens
Ajouter
const refreshTokens =
await this.prisma.refreshToken.findMany({
where: {
revokedAt: null,
expiresAt: {
gt: new Date()
}
},
include: {
user: {
include: {
userRoles: {
include: {
role: true
}
}
}
}
}
});
Étape 10 — Vérification du token
Recherche
let matchedToken = null;
for (
const refreshToken
of refreshTokens
) {
const valid =
await this.passwordService.compare(
dto.refreshToken,
refreshToken.tokenHash
);
if (valid) {
matchedToken =
refreshToken;
break;
}
}
Contrôle
if (!matchedToken) {
throw new UnauthorizedException(
'Invalid refresh token'
);
}
Étape 11 — Vérifier utilisateur
Ajouter
const user = matchedToken.user;
Contrôle
if (
user.status !== 'ACTIVE'
) {
throw new UnauthorizedException(
'User inactive'
);
}
Étape 12 — Révocation ancien Refresh Token
Ajouter
await this.prisma.refreshToken.update({
where: {
id: matchedToken.id
},
data: {
revokedAt:
new Date()
}
});
Étape 13 — Génération nouveau JWT
Payload
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email
};
Génération
const accessToken =
await this.tokenService
.generateAccessToken(
payload
);
Étape 14 — Génération nouveau Refresh Token
Ajouter
const refreshToken =
await this.tokenService
.generateRefreshToken();
Persistance
await this.tokenService
.storeRefreshToken(
user.id,
refreshToken
);
Étape 15 — Chargement des rôles
Ajouter
const roles =
user.userRoles.map(
role => role.role.code
);
Étape 16 — Retour
Réponse
return {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
firstName:
user.firstName,
lastName:
user.lastName,
roles
}
};
Étape 17 — Swagger
Route
@Post('refresh')
Documentation
@ApiOperation({
summary:
'Refresh access token'
})
@ApiOkResponse({
type: AuthResponseDto
})
Étape 18 — Cas d'erreurs
Token invalide
{
"statusCode": 401,
"message":
"Invalid refresh token"
}
Utilisateur inactif
{
"statusCode": 401,
"message":
"User inactive"
}
Étape 19 — Test Swagger
Requête
POST /auth/refresh
Body
{
"refreshToken":
"xxxxxxxx"
}
Réponse
{
"accessToken": "...",
"refreshToken": "...",
"user": {
"id": "...",
"email": "...",
"roles": [
"CUSTOMER"
]
}
}
Étape 20 — Vérification SQL
Ancien token
SELECT * FROM "RefreshToken" WHERE "revokedAt" IS NOT NULL;
Nouveau token
SELECT * FROM "RefreshToken" WHERE "revokedAt" IS NULL;
Optimisation recommandée
Problème
La recherche par :
findMany()
puis comparaison BCrypt n'est pas scalable.
Amélioration Sprint 1.5
Ajouter :
tokenId String @unique
dans :
RefreshToken
et stocker :
tokenId.rawToken
afin de retrouver directement le token.
Définition de terminé
Le Sprint 1-B.4 est terminé lorsque :
✓ Refresh fonctionnel ✓ Ancien token révoqué ✓ Nouveau JWT généré ✓ Nouveau Refresh Token généré ✓ Rotation opérationnelle ✓ Swagger documenté ✓ Tests manuels réussis
Livrables
TokenService AuthService.refresh() Swagger Refresh Rotation Refresh Token
Sprint 1-B.5 — Implémentation réelle du Logout
Objectif
Rendre le endpoint :
POST /auth/logout
entièrement fonctionnel.
À l'issue de cette étape :
✓ JWT requis ✓ Session révoquée ✓ Refresh Tokens révoqués ✓ Déconnexion complète ✓ Audit de déconnexion ✓ Swagger documenté
Workflow complet
Logout Request ↓ JWT Validation ↓ Load User ↓ Load Active Session ↓ Revoke Session ↓ Revoke Refresh Tokens ↓ Audit Log ↓ Success Response
Étape 1 — Création du DTO de réponse
Créer
application/dto/logout-response.dto.ts
Code
export class LogoutResponseDto {
success: boolean;
message: string;
}
Étape 2 — Amélioration du JWT Payload
Fichier
jwt-payload.interface.ts
Ajouter
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
sessionId?: string;
}
Objectif
Pouvoir relier :
JWT ↓ Session
afin de révoquer précisément la session active.
Étape 3 — Adapter le Login
Lors de la création Session
Modifier :
const createdSession =
await this.prisma.session.create({
data: {
userId: user.id,
ipAddress: session.ipAddress,
userAgent: session.userAgent,
lastSeenAt: new Date()
}
});
JWT Payload
Ajouter :
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email,
sessionId: createdSession.id
};
Résultat
Chaque JWT référence désormais :
Session.id
Étape 4 — Signature Logout
AuthService
Ajouter :
async logout( userId: string, sessionId?: string ): Promise<LogoutResponseDto>
Étape 5 — Révocation Session
Ajouter
if (sessionId) {
await this.prisma.session.updateMany({
where: {
id: sessionId,
userId
},
data: {
revokedAt:
new Date()
}
});
}
Résultat
La session courante devient :
Révoquée
Étape 6 — Révocation Refresh Tokens
Ajouter
await this.prisma.refreshToken.updateMany({
where: {
userId,
revokedAt: null
},
data: {
revokedAt:
new Date()
}
});
Résultat
Tous les refresh tokens actifs :
INVALIDES
Étape 7 — Mise à jour dernière activité
Optionnel
Ajouter :
await this.prisma.user.update({
where: {
id: userId
},
data: {
updatedAt:
new Date()
}
});
Étape 8 — Réponse
Retour
return {
success: true,
message:
'Successfully logged out'
};
Étape 9 — Controller
Modifier
@Post('logout')
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Implémentation
logout(
@Req() req
) {
return this.authService.logout(
req.user.sub,
req.user.sessionId
);
}
Étape 10 — Swagger
Documentation
@ApiOperation({
summary:
'Logout current user'
})
Réponse
@ApiOkResponse({
type:
LogoutResponseDto
})
Étape 11 — Cas particulier
Déconnexion globale
Créer un futur endpoint :
POST /auth/logout-all
Fonction
Révoquer :
Toutes les sessions Tous les refresh tokens
sur tous les appareils.
Étape 12 — Vérification Session
SQL
SELECT id, "revokedAt" FROM "Session";
Résultat attendu
revokedAt != null
Étape 13 — Vérification Refresh Tokens
SQL
SELECT id, "revokedAt" FROM "RefreshToken";
Résultat attendu
Tous les tokens actifs deviennent :
revokedAt != null
Étape 14 — Test Swagger
Login
POST /auth/login
Récupérer :
accessToken
Logout
POST /auth/logout Authorization: Bearer xxx
Réponse
{
"success": true,
"message": "Successfully logged out"
}
Étape 15 — Vérification sécurité
Tester
Réutiliser :
Refresh Token précédent
Résultat attendu
{
"statusCode": 401,
"message": "Invalid refresh token"
}
Amélioration Sprint 1-C
Pour renforcer la sécurité Enterprise :
Ajouter dans :
model Session
le champ :
sessionToken String? @unique
afin de permettre :
Gestion multi-appareils Révocation ciblée Historique complet
Définition de terminé
Le Sprint 1-B.5 est terminé lorsque :
✓ Logout fonctionnel ✓ Session révoquée ✓ Refresh Tokens révoqués ✓ Swagger documenté ✓ Test manuel réussi ✓ Sécurité validée
Livrables
AuthService.logout() LogoutResponseDto Swagger Logout Révocation Session Révocation RefreshToken
Sprint 1-B.6 — Implémentation réelle du Current User
Objectif
Rendre le endpoint :
GET /auth/me
entièrement fonctionnel.
À l'issue de cette étape :
✓ JWT validé ✓ Utilisateur chargé ✓ Rôles chargés ✓ Permissions chargées ✓ Tenant chargé ✓ Profil utilisateur complet ✓ Swagger documenté
Workflow complet
GET /auth/me ↓ JWT Validation ↓ Extract UserId ↓ Load User ↓ Load Roles ↓ Load Permissions ↓ Build Profile ↓ Return Response
Étape 1 — DTO de réponse
Créer
application/dto/current-user.dto.ts
Contenu
export class CurrentUserDto {
id: string;
tenantId: string;
email: string;
firstName: string;
lastName: string;
phone?: string;
status: string;
emailVerified: boolean;
roles: string[];
permissions: string[];
tenant: {
id: string;
code: string;
name: string;
};
}
Étape 2 — Signature du service
AuthService
Ajouter :
async me( userId: string ): Promise<CurrentUserDto>
Étape 3 — Chargement utilisateur
Ajouter
const user =
await this.prisma.user.findUnique({
where: {
id: userId
},
include: {
tenant: true,
userRoles: {
include: {
role: {
include: {
rolePermissions: {
include: {
permission: true
}
}
}
}
}
}
}
});
Étape 4 — Vérification utilisateur
Ajouter
if (!user) {
throw new NotFoundException(
'User not found'
);
}
Étape 5 — Extraction des rôles
Ajouter
const roles =
user.userRoles.map(
item => item.role.code
);
Résultat
Exemple :
[ "SUPER_ADMIN", "OWNER" ]
Étape 6 — Extraction permissions
Ajouter
const permissions =
user.userRoles.flatMap(
userRole =>
userRole.role
.rolePermissions
.map(
rp =>
rp.permission.code
)
);
Étape 7 — Déduplication
Ajouter
const uniquePermissions =
[...new Set(
permissions
)];
Résultat
users.read users.create properties.read reservations.read
Étape 8 — Construction réponse
Retour
return {
id: user.id,
tenantId: user.tenantId,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
status: user.status,
emailVerified:
user.emailVerified,
roles,
permissions:
uniquePermissions,
tenant: {
id: user.tenant.id,
code: user.tenant.code,
name: user.tenant.name
}
};
Étape 9 — Contrôleur
Endpoint
@Get('me')
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Implémentation
me(
@Req() req
) {
return this.authService.me(
req.user.sub
);
}
Étape 10 — Swagger
Documentation
@ApiOperation({
summary:
'Get current authenticated user'
})
Réponse
@ApiOkResponse({
type:
CurrentUserDto
})
Étape 11 — Exemple de réponse
Retour attendu
{
"id": "e1d1c8",
"tenantId": "tenant1",
"email":
"admin@platform.local",
"firstName":
"Super",
"lastName":
"Admin",
"status":
"ACTIVE",
"emailVerified": true,
"roles": [
"SUPER_ADMIN"
],
"permissions": [
"users.read",
"users.create",
"users.update",
"users.delete",
"roles.manage"
],
"tenant": {
"id": "tenant-id",
"code": "MAIN",
"name":
"Main Tenant"
}
}
Étape 12 — Sécurisation
Refuser
Utilisateur supprimé Utilisateur bloqué Utilisateur inactif
Ajouter
if (
user.status !== 'ACTIVE'
) {
throw new ForbiddenException(
'Account disabled'
);
}
Étape 13 — Test Swagger
Login
POST /auth/login
Copier
accessToken
Tester
GET /auth/me Authorization: Bearer xxx
Résultat
Profil utilisateur complet.
Étape 14 — Vérification SQL
Utilisateur
SELECT * FROM "User" WHERE email = 'admin@platform.local';
Rôles
SELECT * FROM "UserRole";
Permissions
SELECT * FROM "RolePermission";
Optimisation future
Sprint 1-C
Ajouter :
Redis Cache
sur :
GET /auth/me
afin d'éviter :
Chargement complet des permissions à chaque requête.
Définition de terminé
Le Sprint 1-B.6 est terminé lorsque :
✓ GET /auth/me fonctionnel ✓ JWT validé ✓ Utilisateur chargé ✓ Rôles chargés ✓ Permissions chargées ✓ Swagger documenté ✓ Test manuel réussi
Livrables
CurrentUserDto AuthService.me() Swagger Current User Chargement rôles Chargement permissions
Bilan Sprint 1-B
Les endpoints suivants sont maintenant opérationnels :
POST /auth/register POST /auth/login POST /auth/refresh POST /auth/logout GET /auth/me
Sprint 1-C.1 — Implémentation du RBAC Enterprise
Objectif
Mettre en place le moteur de contrôle d'accès central de la plateforme.
À l'issue de cette étape :
✓ @Roles() ✓ RolesGuard ✓ @Permissions() ✓ PermissionsGuard ✓ Metadata Reflector ✓ Contrôle d'accès complet ✓ Swagger compatible ✓ Réutilisable dans tous les modules
Architecture cible
Controller ↓ @Roles() @Permissions() ↓ JwtAuthGuard ↓ RolesGuard ↓ PermissionsGuard ↓ Access Granted
Structure
Créer
src/modules/auth/presentation
├── decorators
│
│ ├── roles.decorator.ts
│ └── permissions.decorator.ts
│
├── guards
│
│ ├── jwt-auth.guard.ts
│ ├── roles.guard.ts
│ └── permissions.guard.ts
│
└── constants
└── auth.constants.ts
Étape 1 — Constantes RBAC
Créer
auth.constants.ts
Contenu
export const ROLES_KEY = 'roles'; export const PERMISSIONS_KEY = 'permissions';
Étape 2 — Décorateur Roles
Créer
roles.decorator.ts
Implémentation
import { SetMetadata }
from '@nestjs/common';
import {
ROLES_KEY
}
from '../constants/auth.constants';
export const Roles = (
...roles: string[]
) => SetMetadata(
ROLES_KEY,
roles
);
Étape 3 — Décorateur Permissions
Créer
permissions.decorator.ts
Implémentation
import { SetMetadata }
from '@nestjs/common';
import {
PERMISSIONS_KEY
}
from '../constants/auth.constants';
export const Permissions = (
...permissions: string[]
) => SetMetadata(
PERMISSIONS_KEY,
permissions
);
Étape 4 — Extension JWT Payload
Fichier
jwt-payload.interface.ts
Ajouter
roles?: string[]; permissions?: string[];
Objectif
Éviter des requêtes SQL inutiles.
Le JWT transporte :
Roles Permissions
du compte connecté.
Étape 5 — Enrichir le Login
Charger permissions
Dans :
AuthService.login()
Ajouter
const permissions =
user.userRoles.flatMap(
userRole =>
userRole.role
.rolePermissions
.map(
rp =>
rp.permission.code
)
);
Dédupliquer
const uniquePermissions =
[...new Set(
permissions
)];
Payload JWT
Modifier :
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email,
sessionId: createdSession.id,
roles,
permissions:
uniquePermissions
};
Étape 6 — Enrichir Register
Ajouter
Lors du :
register()
Payload
roles: [ SYSTEM_ROLES.CUSTOMER ] permissions: []
Étape 7 — Création RolesGuard
Créer
roles.guard.ts
Injection
constructor(
private reflector:
Reflector
) {}
Implémentation
@Injectable()
export class RolesGuard
implements CanActivate {
constructor(
private reflector:
Reflector
) {}
canActivate(
context:
ExecutionContext
): boolean {
const requiredRoles =
this.reflector.getAllAndOverride<
string[]
>(
ROLES_KEY,
[
context.getHandler(),
context.getClass()
]
);
if (
!requiredRoles ||
requiredRoles.length === 0
) {
return true;
}
const request =
context
.switchToHttp()
.getRequest();
const user =
request.user;
return requiredRoles.some(
role =>
user.roles?.includes(
role
)
);
}
}
Étape 8 — Création PermissionsGuard
Créer
permissions.guard.ts
Implémentation
@Injectable()
export class PermissionsGuard
implements CanActivate {
constructor(
private reflector:
Reflector
) {}
canActivate(
context:
ExecutionContext
): boolean {
const requiredPermissions =
this.reflector.getAllAndOverride<
string[]
>(
PERMISSIONS_KEY,
[
context.getHandler(),
context.getClass()
]
);
if (
!requiredPermissions ||
requiredPermissions.length === 0
) {
return true;
}
const request =
context
.switchToHttp()
.getRequest();
const user =
request.user;
return requiredPermissions.every(
permission =>
user.permissions?.includes(
permission
)
);
}
}
Étape 9 — Mise à jour JwtStrategy
Validation
Remplacer :
return payload;
Par
return {
id: payload.sub,
tenantId:
payload.tenantId,
email:
payload.email,
sessionId:
payload.sessionId,
roles:
payload.roles ?? [],
permissions:
payload.permissions ?? []
};
Étape 10 — Export Guards
AuthModule
Ajouter :
providers: [ AuthService, PasswordService, TokenService, JwtStrategy, RolesGuard, PermissionsGuard ]
Exports
exports: [ RolesGuard, PermissionsGuard ]
Étape 11 — Utilisation Roles
Exemple
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
findAllUsers() {}
Étape 12 — Utilisation Permissions
Exemple
@Get()
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
findAllUsers() {}
Étape 13 — Combinaison RBAC
Exemple
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard,
PermissionsGuard
)
@Roles(
'ADMIN',
'SUPER_ADMIN'
)
@Permissions(
'users.read'
)
findAllUsers() {}
Étape 14 — Création d'un endpoint de test
AuthController
Ajouter :
@Get('admin')
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
adminOnly() {
return {
success: true
};
}
Étape 15 — Endpoint permissions
Ajouter
@Get('permissions')
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
permissionsOnly() {
return {
success: true
};
}
Étape 16 — Tests manuels
SUPER_ADMIN
Doit accéder :
GET /auth/admin
CUSTOMER
Doit recevoir :
{
"statusCode": 403,
"message": "Forbidden resource"
}
Étape 17 — Gestion du Super Admin
Bypass recommandé
Dans :
PermissionsGuard
ajouter :
if (
user.roles?.includes(
'SUPER_ADMIN'
)
) {
return true;
}
Résultat
Le :
SUPER_ADMIN
contourne toutes les permissions.
Étape 18 — Préparation Sprint 2
Le RBAC est maintenant réutilisable sur :
Users Properties Reservations CRM Finance Marketing Security Administration
Définition de terminé
Le Sprint 1-C.1 est terminé lorsque :
✓ @Roles créé ✓ @Permissions créé ✓ RolesGuard créé ✓ PermissionsGuard créé ✓ JWT enrichi ✓ Contrôle d'accès fonctionnel ✓ Tests manuels réussis ✓ Swagger toujours opérationnel
Livrables
RolesDecorator PermissionsDecorator RolesGuard PermissionsGuard JWT enrichi RBAC Enterprise
Sprint 1-C.1 — Implémentation du RBAC Enterprise
Objectif
Mettre en place le moteur de contrôle d'accès central de la plateforme.
À l'issue de cette étape :
✓ @Roles() ✓ RolesGuard ✓ @Permissions() ✓ PermissionsGuard ✓ Metadata Reflector ✓ Contrôle d'accès complet ✓ Swagger compatible ✓ Réutilisable dans tous les modules
Architecture cible
Controller ↓ @Roles() @Permissions() ↓ JwtAuthGuard ↓ RolesGuard ↓ PermissionsGuard ↓ Access Granted
Structure
Créer
src/modules/auth/presentation
├── decorators
│
│ ├── roles.decorator.ts
│ └── permissions.decorator.ts
│
├── guards
│
│ ├── jwt-auth.guard.ts
│ ├── roles.guard.ts
│ └── permissions.guard.ts
│
└── constants
└── auth.constants.ts
Étape 1 — Constantes RBAC
Créer
auth.constants.ts
Contenu
export const ROLES_KEY = 'roles'; export const PERMISSIONS_KEY = 'permissions';
Étape 2 — Décorateur Roles
Créer
roles.decorator.ts
Implémentation
import { SetMetadata }
from '@nestjs/common';
import {
ROLES_KEY
}
from '../constants/auth.constants';
export const Roles = (
...roles: string[]
) => SetMetadata(
ROLES_KEY,
roles
);
Étape 3 — Décorateur Permissions
Créer
permissions.decorator.ts
Implémentation
import { SetMetadata }
from '@nestjs/common';
import {
PERMISSIONS_KEY
}
from '../constants/auth.constants';
export const Permissions = (
...permissions: string[]
) => SetMetadata(
PERMISSIONS_KEY,
permissions
);
Étape 4 — Extension JWT Payload
Fichier
jwt-payload.interface.ts
Ajouter
roles?: string[]; permissions?: string[];
Objectif
Éviter des requêtes SQL inutiles.
Le JWT transporte :
Roles Permissions
du compte connecté.
Étape 5 — Enrichir le Login
Charger permissions
Dans :
AuthService.login()
Ajouter
const permissions =
user.userRoles.flatMap(
userRole =>
userRole.role
.rolePermissions
.map(
rp =>
rp.permission.code
)
);
Dédupliquer
const uniquePermissions =
[...new Set(
permissions
)];
Payload JWT
Modifier :
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email,
sessionId: createdSession.id,
roles,
permissions:
uniquePermissions
};
Étape 6 — Enrichir Register
Ajouter
Lors du :
register()
Payload
roles: [ SYSTEM_ROLES.CUSTOMER ] permissions: []
Étape 7 — Création RolesGuard
Créer
roles.guard.ts
Injection
constructor(
private reflector:
Reflector
) {}
Implémentation
@Injectable()
export class RolesGuard
implements CanActivate {
constructor(
private reflector:
Reflector
) {}
canActivate(
context:
ExecutionContext
): boolean {
const requiredRoles =
this.reflector.getAllAndOverride<
string[]
>(
ROLES_KEY,
[
context.getHandler(),
context.getClass()
]
);
if (
!requiredRoles ||
requiredRoles.length === 0
) {
return true;
}
const request =
context
.switchToHttp()
.getRequest();
const user =
request.user;
return requiredRoles.some(
role =>
user.roles?.includes(
role
)
);
}
}
Étape 8 — Création PermissionsGuard
Créer
permissions.guard.ts
Implémentation
@Injectable()
export class PermissionsGuard
implements CanActivate {
constructor(
private reflector:
Reflector
) {}
canActivate(
context:
ExecutionContext
): boolean {
const requiredPermissions =
this.reflector.getAllAndOverride<
string[]
>(
PERMISSIONS_KEY,
[
context.getHandler(),
context.getClass()
]
);
if (
!requiredPermissions ||
requiredPermissions.length === 0
) {
return true;
}
const request =
context
.switchToHttp()
.getRequest();
const user =
request.user;
return requiredPermissions.every(
permission =>
user.permissions?.includes(
permission
)
);
}
}
Étape 9 — Mise à jour JwtStrategy
Validation
Remplacer :
return payload;
Par
return {
id: payload.sub,
tenantId:
payload.tenantId,
email:
payload.email,
sessionId:
payload.sessionId,
roles:
payload.roles ?? [],
permissions:
payload.permissions ?? []
};
Étape 10 — Export Guards
AuthModule
Ajouter :
providers: [ AuthService, PasswordService, TokenService, JwtStrategy, RolesGuard, PermissionsGuard ]
Exports
exports: [ RolesGuard, PermissionsGuard ]
Étape 11 — Utilisation Roles
Exemple
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
findAllUsers() {}
Étape 12 — Utilisation Permissions
Exemple
@Get()
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
findAllUsers() {}
Étape 13 — Combinaison RBAC
Exemple
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard,
PermissionsGuard
)
@Roles(
'ADMIN',
'SUPER_ADMIN'
)
@Permissions(
'users.read'
)
findAllUsers() {}
Étape 14 — Création d'un endpoint de test
AuthController
Ajouter :
@Get('admin')
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
adminOnly() {
return {
success: true
};
}
Étape 15 — Endpoint permissions
Ajouter
@Get('permissions')
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
permissionsOnly() {
return {
success: true
};
}
Étape 16 — Tests manuels
SUPER_ADMIN
Doit accéder :
GET /auth/admin
CUSTOMER
Doit recevoir :
{
"statusCode": 403,
"message": "Forbidden resource"
}
Étape 17 — Gestion du Super Admin
Bypass recommandé
Dans :
PermissionsGuard
ajouter :
if (
user.roles?.includes(
'SUPER_ADMIN'
)
) {
return true;
}
Résultat
Le :
SUPER_ADMIN
contourne toutes les permissions.
Étape 18 — Préparation Sprint 2
Le RBAC est maintenant réutilisable sur :
Users Properties Reservations CRM Finance Marketing Security Administration
Définition de terminé
Le Sprint 1-C.1 est terminé lorsque :
✓ @Roles créé ✓ @Permissions créé ✓ RolesGuard créé ✓ PermissionsGuard créé ✓ JWT enrichi ✓ Contrôle d'accès fonctionnel ✓ Tests manuels réussis ✓ Swagger toujours opérationnel
Livrables
RolesDecorator PermissionsDecorator RolesGuard PermissionsGuard JWT enrichi RBAC Enterprise
Sprint 1-C.2 — Tests Unitaires Auth & RBAC
Objectif
Sécuriser l'ensemble du module Auth avant de poursuivre les développements métier.
À l'issue de cette étape :
✓ PasswordService testé ✓ TokenService testé ✓ AuthService testé ✓ JwtStrategy testée ✓ RolesGuard testé ✓ PermissionsGuard testé ✓ Couverture > 80% ✓ CI compatible
Architecture des tests
Répertoire
Créer :
apps/api/src/modules/auth
├── domain
│
│ └── services
│
│ ├── auth.service.spec.ts
│ ├── password.service.spec.ts
│ └── token.service.spec.ts
│
├── infrastructure
│
│ └── strategies
│
│ └── jwt.strategy.spec.ts
│
└── presentation
│
└── guards
├── roles.guard.spec.ts
└── permissions.guard.spec.ts
Étape 1 — Configuration Jest
Vérifier
npm install -D jest ts-jest @types/jest
Vérifier
apps/api/jest.config.ts
Coverage
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
Étape 2 — PasswordService
Fichier
password.service.spec.ts
Test hash
describe(
'PasswordService',
() => {
it(
'should hash password',
async () => {
const hash =
await service.hash(
'Password123!'
);
expect(hash)
.toBeDefined();
expect(hash)
.not
.toEqual(
'Password123!'
);
}
);
}
);
Test compare
it(
'should compare password',
async () => {
const hash =
await service.hash(
'Password123!'
);
const result =
await service.compare(
'Password123!',
hash
);
expect(result)
.toBe(true);
}
);
Étape 3 — TokenService
Fichier
token.service.spec.ts
Test Access Token
it(
'should generate access token',
async () => {
const token =
await service
.generateAccessToken({
sub: '1',
tenantId: '1',
email:
'test@test.com'
});
expect(token)
.toBeDefined();
}
);
Test Refresh Token
it(
'should generate refresh token',
async () => {
const token =
await service
.generateRefreshToken();
expect(token)
.toBeDefined();
}
);
Étape 4 — Mock Prisma
Créer
test/mocks/prisma.mock.ts
Contenu
export const prismaMock = {
user: {
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn()
},
role: {
findFirst: jest.fn()
},
userRole: {
create: jest.fn()
},
refreshToken: {
create: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
updateMany: jest.fn()
},
session: {
create: jest.fn(),
updateMany: jest.fn()
},
tenant: {
findUnique: jest.fn()
}
};
Étape 5 — AuthService
Fichier
auth.service.spec.ts
Setup
beforeEach(
async () => {
const module =
await Test
.createTestingModule({
providers: [
AuthService,
{
provide:
PrismaService,
useValue:
prismaMock
}
]
})
.compile();
}
);
Étape 6 — Test Register
Cas nominal
it(
'should register user',
async () => {
prismaMock.user
.findUnique
.mockResolvedValue(
null
);
const result =
await service.register({
email:
'john@test.com',
password:
'Password123!',
firstName:
'John',
lastName:
'Doe'
});
expect(result)
.toBeDefined();
}
);
Étape 7 — Email déjà utilisé
Test
it(
'should throw conflict',
async () => {
prismaMock.user
.findUnique
.mockResolvedValue({});
await expect(
service.register(
dto
)
).rejects.toThrow(
ConflictException
);
}
);
Étape 8 — Test Login
Succès
it(
'should login user',
async () => {
const result =
await service.login(
dto,
session
);
expect(result)
.toBeDefined();
}
);
Mot de passe invalide
it(
'should reject login',
async () => {
await expect(
service.login(
dto,
session
)
).rejects.toThrow(
UnauthorizedException
);
}
);
Étape 9 — Test Refresh
Succès
it(
'should refresh token',
async () => {
const result =
await service.refresh({
refreshToken:
'token'
});
expect(result)
.toBeDefined();
}
);
Échec
it(
'should reject invalid token',
async () => {
await expect(
service.refresh({
refreshToken:
'bad-token'
})
).rejects.toThrow(
UnauthorizedException
);
}
);
Étape 10 — Test Logout
Succès
it(
'should logout user',
async () => {
const result =
await service.logout(
'user-id'
);
expect(
result.success
).toBe(true);
}
);
Étape 11 — Test Current User
Succès
it(
'should return current user',
async () => {
const result =
await service.me(
'user-id'
);
expect(result)
.toBeDefined();
expect(
result.email
).toEqual(
'admin@platform.local'
);
}
);
Étape 12 — JwtStrategy
Fichier
jwt.strategy.spec.ts
Test
it(
'should validate payload',
async () => {
const result =
await strategy.validate({
sub: '1',
email:
'test@test.com',
tenantId:
'tenant'
});
expect(
result.id
).toEqual('1');
}
);
Étape 13 — RolesGuard
Fichier
roles.guard.spec.ts
Autorisé
it(
'should allow role',
() => {
expect(
guard.canActivate(
context
)
).toBe(true);
}
);
Refusé
it(
'should reject role',
() => {
expect(
guard.canActivate(
context
)
).toBe(false);
}
);
Étape 14 — PermissionsGuard
Fichier
permissions.guard.spec.ts
Autorisé
it(
'should allow permission',
() => {
expect(
guard.canActivate(
context
)
).toBe(true);
}
);
Refusé
it(
'should reject permission',
() => {
expect(
guard.canActivate(
context
)
).toBe(false);
}
);
Étape 15 — Exécution
Lancer
nx test api
Avec coverage
nx test api \ --coverage
Étape 16 — Rapport
Généré dans
coverage/apps/api
Ouvrir
coverage/apps/api/lcov-report/index.html
Étape 17 — Objectifs
PasswordService
100 %
TokenService
100 %
RolesGuard
100 %
PermissionsGuard
100 %
AuthService
> 80 %
Étape 18 — Ajout CI
Workflow
.github/workflows/ci.yml
Ajouter
- name: Run Tests run: nx test api
Coverage
- name: Coverage run: nx test api --coverage
Étape 19 — Badge Coverage
README
![Coverage]
Définition de terminé
Le Sprint 1-C.2 est terminé lorsque :
✓ PasswordService testé ✓ TokenService testé ✓ AuthService testé ✓ JwtStrategy testée ✓ RolesGuard testé ✓ PermissionsGuard testé ✓ Coverage > 80 % ✓ CI verte
Livrables
auth.service.spec.ts password.service.spec.ts token.service.spec.ts jwt.strategy.spec.ts roles.guard.spec.ts permissions.guard.spec.ts coverage report
Sprint 1-C.3 — Tests E2E Authentification
Objectif
Valider le fonctionnement réel de toute la chaîne d'authentification.
À l'issue de cette étape :
✓ Register testé ✓ Login testé ✓ Current User testé ✓ Refresh testé ✓ Logout testé ✓ RBAC testé ✓ PostgreSQL réel ✓ JWT réel ✓ Prisma réel
Ces tests constituent la première validation métier complète de la plateforme.
Architecture E2E
Environnement
NestJS ↓ Prisma ↓ PostgreSQL ↓ JWT ↓ API réelle
Pas de Mock
Contrairement aux tests unitaires :
Aucun Mock Aucun Fake Service Base PostgreSQL réelle
Étape 1 — Création du projet E2E
Vérifier
apps/api-e2e
Structure
apps
├── api
└── api-e2e
├── src
│ └── auth
│ └── auth.e2e-spec.ts
│
├── jest.config.ts
└── project.json
Étape 2 — Installation
Vérifier
npm install -D supertest npm install -D @types/supertest
Étape 3 — Base de test
Créer
.env.test
Contenu
DATABASE_URL= postgresql://postgres: postgres@localhost:5432/ rental_platform_test JWT_SECRET=test-secret JWT_EXPIRES_IN=15m REFRESH_SECRET=test-refresh REFRESH_EXPIRES_IN=30d
Étape 4 — Création DB Test
PostgreSQL
CREATE DATABASE rental_platform_test;
Étape 5 — Migration
Exécuter
DATABASE_URL=...
npx prisma migrate deploy
Seed
DATABASE_URL=...
npx prisma db seed
Étape 6 — Bootstrap E2E
Créer
apps/api-e2e/src/auth/auth.e2e-spec.ts
Base
describe(
'Auth E2E',
() => {
let app:
INestApplication;
beforeAll(
async () => {
const moduleRef =
await Test
.createTestingModule({
imports: [
AppModule
]
})
.compile();
app =
moduleRef
.createNestApplication();
await app.init();
}
);
afterAll(
async () => {
await app.close();
}
);
}
);
Étape 7 — Variables de scénario
Ajouter
let accessToken: string; let refreshToken: string; let userId: string;
Étape 8 — Test Register
Cas nominal
it(
'should register user',
async () => {
const response =
await request(
app.getHttpServer()
)
.post(
'/auth/register'
)
.send({
email:
'john@test.com',
password:
'Password123!',
firstName:
'John',
lastName:
'Doe'
})
.expect(201);
expect(
response.body
.accessToken
).toBeDefined();
userId =
response.body
.user.id;
}
);
Étape 9 — Test Login
Cas nominal
it(
'should login',
async () => {
const response =
await request(
app.getHttpServer()
)
.post(
'/auth/login'
)
.send({
email:
'john@test.com',
password:
'Password123!'
})
.expect(201);
accessToken =
response.body
.accessToken;
refreshToken =
response.body
.refreshToken;
expect(
accessToken
).toBeDefined();
expect(
refreshToken
).toBeDefined();
}
);
Étape 10 — Test Current User
Endpoint
GET /auth/me
Test
it(
'should return current user',
async () => {
const response =
await request(
app.getHttpServer()
)
.get(
'/auth/me'
)
.set(
'Authorization',
`Bearer ${accessToken}`
)
.expect(200);
expect(
response.body.email
).toEqual(
'john@test.com'
);
}
);
Étape 11 — Test RBAC
Endpoint
GET /auth/admin
CUSTOMER
it(
'should reject customer',
async () => {
await request(
app.getHttpServer()
)
.get(
'/auth/admin'
)
.set(
'Authorization',
`Bearer ${accessToken}`
)
.expect(403);
}
);
Étape 12 — Test Refresh
Endpoint
POST /auth/refresh
Test
it(
'should refresh token',
async () => {
const response =
await request(
app.getHttpServer()
)
.post(
'/auth/refresh'
)
.send({
refreshToken
})
.expect(201);
accessToken =
response.body
.accessToken;
refreshToken =
response.body
.refreshToken;
expect(
accessToken
).toBeDefined();
}
);
Étape 13 — Test Logout
Endpoint
POST /auth/logout
Test
it(
'should logout',
async () => {
await request(
app.getHttpServer()
)
.post(
'/auth/logout'
)
.set(
'Authorization',
`Bearer ${accessToken}`
)
.expect(201);
}
);
Étape 14 — Vérification sécurité
Ancien Refresh Token
Ne doit plus fonctionner.
Test
it(
'should reject old refresh token',
async () => {
await request(
app.getHttpServer()
)
.post(
'/auth/refresh'
)
.send({
refreshToken
})
.expect(401);
}
);
Étape 15 — Cas erreur Login
Mauvais mot de passe
it(
'should reject invalid password',
async () => {
await request(
app.getHttpServer()
)
.post(
'/auth/login'
)
.send({
email:
'john@test.com',
password:
'BAD_PASSWORD'
})
.expect(401);
}
);
Étape 16 — Cas erreur Register
Email existant
it(
'should reject duplicate email',
async () => {
await request(
app.getHttpServer()
)
.post(
'/auth/register'
)
.send({
email:
'john@test.com',
password:
'Password123!',
firstName:
'John',
lastName:
'Doe'
})
.expect(409);
}
);
Étape 17 — Nettoyage
Supprimer utilisateur
Dans :
afterAll()
Exemple
await prisma.user.deleteMany({
where: {
email:
'john@test.com'
}
});
Étape 18 — Configuration NX
project.json
Ajouter :
{
"targets": {
"e2e": {
"executor":
"@nx/jest:jest"
}
}
}
Étape 19 — Exécution
Lancer
nx e2e api-e2e
Résultat attendu
PASS Register PASS Login PASS Current User PASS Refresh PASS Logout PASS RBAC
Étape 20 — Intégration CI
Workflow
.github/workflows/ci.yml
Ajouter
- name: E2E Tests run: nx e2e api-e2e
Étape 21 — Pipeline qualité
Ordre
Lint ↓ Build ↓ Unit Tests ↓ E2E Tests ↓ Docker Build
Définition de terminé
Le Sprint 1-C.3 est terminé lorsque :
✓ Register testé ✓ Login testé ✓ Current User testé ✓ Refresh testé ✓ Logout testé ✓ RBAC testé ✓ PostgreSQL réel ✓ JWT réel ✓ CI verte
Livrables
auth.e2e-spec.ts .env.test Configuration E2E Pipeline CI
Bilan Sprint 1
Le Sprint 1 est considéré terminé lorsque :
✓ AuthModule opérationnel ✓ Register opérationnel ✓ Login opérationnel ✓ Refresh opérationnel ✓ Logout opérationnel ✓ Current User opérationnel ✓ RBAC opérationnel ✓ Tests unitaires verts ✓ Tests E2E verts ✓ Swagger documenté ✓ CI verte
Étape suivante
Sprint 2 — Gestion des Utilisateurs
Objectif :
Construire le premier domaine métier protégé par le RBAC :
User Profile Address Preferences NotificationSettings UserSession ConnectionHistory
avec :
CRUD Utilisateurs Gestion Profil Gestion Préférences Historique Connexions Administration Utilisateurs