====== 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 {
return bcrypt.hash(
password,
12
);
}
async compare(
password: string,
hash: string
): Promise {
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
----
====== É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
----
====== É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 {
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
----
====== É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
----
====== É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
----
====== É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