====== 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