Outils pour utilisateurs

Outils du site


ujusum:3-codage:2-sprints:1-sprint-1

Table des matières

Sprint 1 — Authentification

Objectif

Implémenter le premier domaine métier réellement exécutable de la plateforme.

À l'issue du Sprint 1 :

✓ Register

✓ Login

✓ JWT

✓ Refresh Token

✓ RBAC

✓ Current User

✓ Swagger

✓ Unit Tests

✓ E2E Tests

Le système devient capable :

Créer un utilisateur

S'authentifier

Gérer les rôles

Sécuriser les endpoints

Périmètre

Modules concernés

AuthModule

UsersModule

SecurityModule

Entités Prisma

User

Role

Permission

UserRole

RolePermission

RefreshToken

Session

Sprint 1-A — Modèle Prisma

Vérification User

model User {
 
  id              String @id @default(uuid())
 
  tenantId        String
 
  email           String @unique
 
  passwordHash    String
 
  firstName       String
 
  lastName        String
 
  phone           String?
 
  status          UserStatus
 
  createdAt       DateTime @default(now())
 
  updatedAt       DateTime @updatedAt
 
  deletedAt       DateTime?
 
  tenant          Tenant @relation(
                    fields:[tenantId],
                    references:[id]
                  )
 
  roles           UserRole[]
 
  refreshTokens   RefreshToken[]
 
  sessions        Session[]
}

RefreshToken

model RefreshToken {
 
  id            String @id @default(uuid())
 
  userId        String
 
  tokenHash     String
 
  expiresAt     DateTime
 
  revokedAt     DateTime?
 
  createdAt     DateTime @default(now())
 
  user          User
    @relation(
      fields:[userId],
      references:[id]
    )
}

Session

model Session {
 
  id            String @id @default(uuid())
 
  userId        String
 
  ipAddress     String?
 
  userAgent     String?
 
  createdAt     DateTime @default(now())
 
  lastSeenAt    DateTime
 
  user          User
    @relation(
      fields:[userId],
      references:[id]
    )
}

Sprint 1-B — Installation

Packages NestJS

npm install @nestjs/jwt
 
npm install @nestjs/passport
 
npm install passport
 
npm install passport-jwt
 
npm install passport-local
 
npm install bcrypt
 
npm install class-validator
 
npm install class-transformer

Types

npm install -D
 
@types/bcrypt
 
@types/passport-jwt
 
@types/passport-local

Sprint 1-C — Structure Module

Arborescence

src/modules/auth

├── application

├── domain

├── infrastructure

├── presentation

└── auth.module.ts

Application

commands

queries

handlers

dto

Sprint 1-D — DTO

RegisterDto

export class RegisterDto {
 
  @IsEmail()
  email: string;
 
  @MinLength(8)
  password: string;
 
  @IsString()
  firstName: string;
 
  @IsString()
  lastName: string;
}

LoginDto

export class LoginDto {
 
  @IsEmail()
  email: string;
 
  @IsString()
  password: string;
}

RefreshTokenDto

export class RefreshTokenDto {
 
  @IsString()
  refreshToken: string;
}

Sprint 1-E — Service Password

PasswordService

src/modules/auth/domain/services

Méthodes

hash()
 
compare()

Implémentation

const hash =
  await bcrypt.hash(password, 12);

Sprint 1-F — JWT

Variables

JWT_SECRET

JWT_EXPIRES_IN

REFRESH_SECRET

REFRESH_EXPIRES_IN

.env

JWT_SECRET=super-secret

JWT_EXPIRES_IN=15m

REFRESH_SECRET=refresh-secret

REFRESH_EXPIRES_IN=30d

Payload

export interface JwtPayload {
 
  sub: string;
 
  tenantId: string;
 
  email: string;
}

Sprint 1-G — Register

Endpoint

POST /auth/register

Workflow

Validation

↓

Email unique

↓

Hash password

↓

Create User

↓

Assign Role USER

↓

Return JWT

Response

{
  "accessToken": "...",
  "refreshToken": "...",
  "user": {}
}

Sprint 1-H — Login

Endpoint

POST /auth/login

Workflow

Find User

↓

Compare Password

↓

Generate JWT

↓

Generate Refresh Token

↓

Create Session

Sprint 1-I — Refresh Token

Endpoint

POST /auth/refresh

Workflow

Validate Refresh Token

↓

Generate New JWT

↓

Rotate Refresh Token

Sprint 1-J — Logout

Endpoint

POST /auth/logout

Action

Révocation Refresh Token

Suppression Session

Sprint 1-K — Current User

Endpoint

GET /auth/me

Response

{
  "id": "...",
  "email": "...",
  "roles": ["USER"]
}

Sprint 1-L — JWT Strategy

Fichier

jwt.strategy.ts

Validation

Bearer Token

↓

Verify JWT

↓

Load User

↓

Inject Request.user

Sprint 1-M — RBAC

Decorator

@Roles('ADMIN')

Guard

RolesGuard

Exemple

@Roles('ADMIN')
 
@Get()
findAll()

Sprint 1-N — Permissions

Decorator

@Permissions(
  'users.read'
)

Guard

PermissionsGuard

Source

RolePermission

Permission

Sprint 1-O — Swagger

Tag

@ApiTags('Auth')

Security

@ApiBearerAuth()

Endpoints

POST /auth/register

POST /auth/login

POST /auth/refresh

POST /auth/logout

GET  /auth/me

Sprint 1-P — Seed Initial

Permissions

users.read

users.create

users.update

users.delete

Roles

SUPER_ADMIN

ADMIN

MANAGER

OWNER

CUSTOMER

Super Admin

admin@platform.local

Sprint 1-Q — Tests Unitaires

Services

AuthService

PasswordService

JwtService

RolesGuard

PermissionsGuard

Commande

nx test api

Sprint 1-R — Tests E2E

Scénario

Register

↓

Login

↓

Access Protected Route

↓

Refresh Token

↓

Logout

Commande

nx e2e api-e2e

Sprint 1-S — Critères d'acceptation

Register

Utilisateur créé

Mot de passe hashé

JWT retourné

Login

JWT valide

Refresh Token valide

RBAC

ADMIN autorisé

USER refusé

Swagger

100 % documenté

Livrables

AuthModule

JwtStrategy

AuthController

AuthService

RolesGuard

PermissionsGuard

RegisterDto

LoginDto

RefreshTokenDto

Swagger

Tests

Sprint 1 — Authentification

Objectif

Implémenter le premier domaine métier réellement exécutable de la plateforme.

À l'issue du Sprint 1 :

✓ Register

✓ Login

✓ JWT

✓ Refresh Token

✓ RBAC

✓ Current User

✓ Swagger

✓ Unit Tests

✓ E2E Tests

Le système devient capable :

Créer un utilisateur

S'authentifier

Gérer les rôles

Sécuriser les endpoints

Périmètre

Modules concernés

AuthModule

UsersModule

SecurityModule

Entités Prisma

User

Role

Permission

UserRole

RolePermission

RefreshToken

Session

Sprint 1-A — Modèle Prisma

Vérification User

model User {
 
  id              String @id @default(uuid())
 
  tenantId        String
 
  email           String @unique
 
  passwordHash    String
 
  firstName       String
 
  lastName        String
 
  phone           String?
 
  status          UserStatus
 
  createdAt       DateTime @default(now())
 
  updatedAt       DateTime @updatedAt
 
  deletedAt       DateTime?
 
  tenant          Tenant @relation(
                    fields:[tenantId],
                    references:[id]
                  )
 
  roles           UserRole[]
 
  refreshTokens   RefreshToken[]
 
  sessions        Session[]
}

RefreshToken

model RefreshToken {
 
  id            String @id @default(uuid())
 
  userId        String
 
  tokenHash     String
 
  expiresAt     DateTime
 
  revokedAt     DateTime?
 
  createdAt     DateTime @default(now())
 
  user          User
    @relation(
      fields:[userId],
      references:[id]
    )
}

Session

model Session {
 
  id            String @id @default(uuid())
 
  userId        String
 
  ipAddress     String?
 
  userAgent     String?
 
  createdAt     DateTime @default(now())
 
  lastSeenAt    DateTime
 
  user          User
    @relation(
      fields:[userId],
      references:[id]
    )
}

Sprint 1-B — Installation

Packages NestJS

npm install @nestjs/jwt
 
npm install @nestjs/passport
 
npm install passport
 
npm install passport-jwt
 
npm install passport-local
 
npm install bcrypt
 
npm install class-validator
 
npm install class-transformer

Types

npm install -D
 
@types/bcrypt
 
@types/passport-jwt
 
@types/passport-local

Sprint 1-C — Structure Module

Arborescence

src/modules/auth

├── application

├── domain

├── infrastructure

├── presentation

└── auth.module.ts

Application

commands

queries

handlers

dto

Sprint 1-D — DTO

RegisterDto

export class RegisterDto {
 
  @IsEmail()
  email: string;
 
  @MinLength(8)
  password: string;
 
  @IsString()
  firstName: string;
 
  @IsString()
  lastName: string;
}

LoginDto

export class LoginDto {
 
  @IsEmail()
  email: string;
 
  @IsString()
  password: string;
}

RefreshTokenDto

export class RefreshTokenDto {
 
  @IsString()
  refreshToken: string;
}

Sprint 1-E — Service Password

PasswordService

src/modules/auth/domain/services

Méthodes

hash()
 
compare()

Implémentation

const hash =
  await bcrypt.hash(password, 12);

Sprint 1-F — JWT

Variables

JWT_SECRET

JWT_EXPIRES_IN

REFRESH_SECRET

REFRESH_EXPIRES_IN

.env

JWT_SECRET=super-secret

JWT_EXPIRES_IN=15m

REFRESH_SECRET=refresh-secret

REFRESH_EXPIRES_IN=30d

Payload

export interface JwtPayload {
 
  sub: string;
 
  tenantId: string;
 
  email: string;
}

Sprint 1-G — Register

Endpoint

POST /auth/register

Workflow

Validation

↓

Email unique

↓

Hash password

↓

Create User

↓

Assign Role USER

↓

Return JWT

Response

{
  "accessToken": "...",
  "refreshToken": "...",
  "user": {}
}

Sprint 1-H — Login

Endpoint

POST /auth/login

Workflow

Find User

↓

Compare Password

↓

Generate JWT

↓

Generate Refresh Token

↓

Create Session

Sprint 1-I — Refresh Token

Endpoint

POST /auth/refresh

Workflow

Validate Refresh Token

↓

Generate New JWT

↓

Rotate Refresh Token

Sprint 1-J — Logout

Endpoint

POST /auth/logout

Action

Révocation Refresh Token

Suppression Session

Sprint 1-K — Current User

Endpoint

GET /auth/me

Response

{
  "id": "...",
  "email": "...",
  "roles": ["USER"]
}

Sprint 1-L — JWT Strategy

Fichier

jwt.strategy.ts

Validation

Bearer Token

↓

Verify JWT

↓

Load User

↓

Inject Request.user

Sprint 1-M — RBAC

Decorator

@Roles('ADMIN')

Guard

RolesGuard

Exemple

@Roles('ADMIN')
 
@Get()
findAll()

Sprint 1-N — Permissions

Decorator

@Permissions(
  'users.read'
)

Guard

PermissionsGuard

Source

RolePermission

Permission

Sprint 1-O — Swagger

Tag

@ApiTags('Auth')

Security

@ApiBearerAuth()

Endpoints

POST /auth/register

POST /auth/login

POST /auth/refresh

POST /auth/logout

GET  /auth/me

Sprint 1-P — Seed Initial

Permissions

users.read

users.create

users.update

users.delete

Roles

SUPER_ADMIN

ADMIN

MANAGER

OWNER

CUSTOMER

Super Admin

admin@platform.local

Sprint 1-Q — Tests Unitaires

Services

AuthService

PasswordService

JwtService

RolesGuard

PermissionsGuard

Commande

nx test api

Sprint 1-R — Tests E2E

Scénario

Register

↓

Login

↓

Access Protected Route

↓

Refresh Token

↓

Logout

Commande

nx e2e api-e2e

Sprint 1-S — Critères d'acceptation

Register

Utilisateur créé

Mot de passe hashé

JWT retourné

Login

JWT valide

Refresh Token valide

RBAC

ADMIN autorisé

USER refusé

Swagger

100 % documenté

Livrables

AuthModule

JwtStrategy

AuthController

AuthService

RolesGuard

PermissionsGuard

RegisterDto

LoginDto

RefreshTokenDto

Swagger

Tests

Sprint 1-A.1 — Implémentation Prisma Auth

Objectif

Implémenter réellement les entités Prisma nécessaires à :

Register

Login

JWT

Refresh Token

RBAC

Sessions

avant toute écriture de code NestJS.

Cette étape produit :

User

Role

Permission

UserRole

RolePermission

RefreshToken

Session

dans le fichier :

apps/api/prisma/schema.prisma

Étape 1 — Vérification des Enums

Ajouter

enum UserStatus {
 
  ACTIVE
 
  INACTIVE
 
  BLOCKED
 
  PENDING
}

Étape 2 — Modèle User

Ajouter

model User {
 
  id                String @id @default(uuid())
 
  tenantId          String
 
  email             String @unique
 
  passwordHash      String
 
  firstName         String
 
  lastName          String
 
  phone             String?
 
  status            UserStatus @default(ACTIVE)
 
  emailVerified     Boolean @default(false)
 
  lastLoginAt       DateTime?
 
  createdAt         DateTime @default(now())
 
  updatedAt         DateTime @updatedAt
 
  deletedAt         DateTime?
 
  createdBy         String?
 
  updatedBy         String?
 
  tenant            Tenant
                     @relation(
                       fields:[tenantId],
                       references:[id]
                     )
 
  userRoles         UserRole[]
 
  refreshTokens     RefreshToken[]
 
  sessions          Session[]
 
  @@index([tenantId])
 
  @@index([email])
 
  @@index([status])
 
  @@index([deletedAt])
}

Étape 3 — Modèle Role

Ajouter

model Role {
 
  id                String @id @default(uuid())
 
  tenantId          String
 
  code              String
 
  name              String
 
  description       String?
 
  createdAt         DateTime @default(now())
 
  updatedAt         DateTime @updatedAt
 
  deletedAt         DateTime?
 
  tenant            Tenant
                     @relation(
                       fields:[tenantId],
                       references:[id]
                     )
 
  userRoles         UserRole[]
 
  rolePermissions   RolePermission[]
 
  @@unique([tenantId, code])
 
  @@index([tenantId])
 
  @@index([code])
}

Étape 4 — Modèle Permission

Ajouter

model Permission {
 
  id                String @id @default(uuid())
 
  code              String @unique
 
  name              String
 
  description       String?
 
  createdAt         DateTime @default(now())
 
  updatedAt         DateTime @updatedAt
 
  rolePermissions   RolePermission[]
 
  @@index([code])
}

Étape 5 — Modèle UserRole

Ajouter

model UserRole {
 
  id                String @id @default(uuid())
 
  userId            String
 
  roleId            String
 
  assignedAt        DateTime @default(now())
 
  assignedBy        String?
 
  user              User
                     @relation(
                       fields:[userId],
                       references:[id]
                     )
 
  role              Role
                     @relation(
                       fields:[roleId],
                       references:[id]
                     )
 
  @@unique([userId, roleId])
 
  @@index([userId])
 
  @@index([roleId])
}

Étape 6 — Modèle RolePermission

Ajouter

model RolePermission {
 
  id                String @id @default(uuid())
 
  roleId            String
 
  permissionId      String
 
  role              Role
                     @relation(
                       fields:[roleId],
                       references:[id]
                     )
 
  permission        Permission
                     @relation(
                       fields:[permissionId],
                       references:[id]
                     )
 
  @@unique([
    roleId,
    permissionId
  ])
 
  @@index([roleId])
 
  @@index([permissionId])
}

Étape 7 — Modèle RefreshToken

Ajouter

model RefreshToken {
 
  id                String @id @default(uuid())
 
  userId            String
 
  tokenHash         String
 
  expiresAt         DateTime
 
  revokedAt         DateTime?
 
  createdAt         DateTime @default(now())
 
  user              User
                     @relation(
                       fields:[userId],
                       references:[id]
                     )
 
  @@index([userId])
 
  @@index([expiresAt])
 
  @@index([revokedAt])
}

Étape 8 — Modèle Session

Ajouter

model Session {
 
  id                String @id @default(uuid())
 
  userId            String
 
  ipAddress         String?
 
  country           String?
 
  userAgent         String?
 
  device            String?
 
  lastSeenAt        DateTime
 
  createdAt         DateTime @default(now())
 
  revokedAt         DateTime?
 
  user              User
                     @relation(
                       fields:[userId],
                       references:[id]
                     )
 
  @@index([userId])
 
  @@index([lastSeenAt])
 
  @@index([revokedAt])
}

Étape 9 — Vérification des relations Tenant

Vérifier

Le modèle :

Tenant

doit contenir :

users User[]
 
roles Role[]

Exemple

model Tenant {
 
  id                String @id @default(uuid())
 
  code              String @unique
 
  name              String
 
  active            Boolean @default(true)
 
  users             User[]
 
  roles             Role[]
 
  createdAt         DateTime @default(now())
 
  updatedAt         DateTime @updatedAt
}

Étape 10 — Validation Prisma

Vérifier

cd apps/api
 
npx prisma validate

Résultat attendu

The schema at prisma/schema.prisma is valid

Étape 11 — Formatage

npx prisma format

Étape 12 — Création de la migration

Générer

npx prisma migrate dev \
--name auth_module

Prisma génère

prisma/migrations

└── xxxx_auth_module

    └── migration.sql

Étape 13 — Génération du Client Prisma

Exécuter

npx prisma generate

Étape 14 — Vérification SQL

Ouvrir PostgreSQL

docker exec -it postgres psql -U postgres

Base

\c rental_platform

Vérifier

\d "User"
 
\d "Role"
 
\d "Permission"
 
\d "UserRole"
 
\d "RolePermission"
 
\d "RefreshToken"
 
\d "Session"

Définition de terminé

Le Sprint 1-A.1 est terminé lorsque :

✓ User créé

✓ Role créé

✓ Permission créée

✓ UserRole créé

✓ RolePermission créé

✓ RefreshToken créé

✓ Session créée

✓ Migration exécutée

✓ Prisma Client généré

✓ Validation Prisma verte

Livrables

schema.prisma

migration auth_module

Prisma Client

Sprint 1-A.2 — Prisma Seed Auth

Objectif

Créer automatiquement les données minimales nécessaires au démarrage de la plateforme.

À l'issue de cette étape :

✓ Tenant principal créé

✓ Permissions créées

✓ Rôles créés

✓ Super Administrateur créé

✓ Affectation des permissions

✓ Affectation du rôle Super Admin

✓ Première connexion possible

Architecture du Seed

Fichier

apps/api/prisma/seed.ts

Responsabilités

Créer Tenant

Créer Permissions

Créer Rôles

Créer Super Admin

Associer les permissions

Associer les rôles

Étape 1 — Installation bcrypt

Vérifier

npm install bcrypt
 
npm install -D @types/bcrypt

Étape 2 — Configuration package.json

Ajouter

{
  "prisma": {
    "seed": "tsx prisma/seed.ts"
  }
}

Installer TSX

npm install -D tsx

Étape 3 — Initialisation du Seed

Créer

apps/api/prisma/seed.ts

Base

import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
 
const prisma = new PrismaClient();
 
async function main() {
 
  console.log('Starting seed...');
}
 
main()
  .then(() => prisma.$disconnect())
  .catch(async (error) => {
 
    console.error(error);
 
    await prisma.$disconnect();
 
    process.exit(1);
  });

Étape 4 — Création du Tenant principal

Ajouter

const tenant =
  await prisma.tenant.upsert({
 
    where: {
      code: 'MAIN'
    },
 
    update: {},
 
    create: {
 
      code: 'MAIN',
 
      name: 'Main Tenant',
 
      active: true
    }
  });

Résultat

MAIN

devient le tenant par défaut.


Étape 5 — Création des Permissions

Liste minimale

users.read
users.create
users.update
users.delete

roles.read
roles.manage

properties.read
properties.create
properties.update
properties.delete

reservations.read
reservations.create
reservations.update
reservations.delete

payments.read
payments.create

crm.read
crm.manage

security.read
security.manage

Implémentation

const permissions = [
 
  'users.read',
  'users.create',
  'users.update',
  'users.delete',
 
  'roles.read',
  'roles.manage',
 
  'properties.read',
  'properties.create',
  'properties.update',
  'properties.delete',
 
  'reservations.read',
  'reservations.create',
  'reservations.update',
  'reservations.delete',
 
  'payments.read',
  'payments.create',
 
  'crm.read',
  'crm.manage',
 
  'security.read',
  'security.manage'
];

Création

for (const code of permissions) {
 
  await prisma.permission.upsert({
 
    where: { code },
 
    update: {},
 
    create: {
 
      code,
 
      name: code
    }
  });
}

Étape 6 — Création des rôles

Rôles système

SUPER_ADMIN

ADMIN

MANAGER

OWNER

CUSTOMER

Création

const superAdminRole =
  await prisma.role.upsert({
 
    where: {
 
      tenantId_code: {
 
        tenantId: tenant.id,
 
        code: 'SUPER_ADMIN'
      }
    },
 
    update: {},
 
    create: {
 
      tenantId: tenant.id,
 
      code: 'SUPER_ADMIN',
 
      name: 'Super Administrateur'
    }
  });

Répéter pour

ADMIN

MANAGER

OWNER

CUSTOMER

Étape 7 — Attribution des permissions

Charger

const allPermissions =
  await prisma.permission.findMany();

Affectation

for (const permission of allPermissions) {
 
  await prisma.rolePermission.upsert({
 
    where: {
 
      roleId_permissionId: {
 
        roleId: superAdminRole.id,
 
        permissionId: permission.id
      }
    },
 
    update: {},
 
    create: {
 
      roleId: superAdminRole.id,
 
      permissionId: permission.id
    }
  });
}

Résultat

SUPER_ADMIN

↓

Toutes les permissions

Étape 8 — Création du compte Super Admin

Variables

admin@platform.local

Admin123!

Hash

const passwordHash =
  await bcrypt.hash(
    'Admin123!',
    12
  );

Création

const admin =
  await prisma.user.upsert({
 
    where: {
 
      email: 'admin@platform.local'
    },
 
    update: {},
 
    create: {
 
      tenantId: tenant.id,
 
      email: 'admin@platform.local',
 
      passwordHash,
 
      firstName: 'Super',
 
      lastName: 'Admin',
 
      emailVerified: true,
 
      status: 'ACTIVE'
    }
  });

Étape 9 — Affectation du rôle

Ajouter

await prisma.userRole.upsert({
 
  where: {
 
    userId_roleId: {
 
      userId: admin.id,
 
      roleId: superAdminRole.id
    }
  },
 
  update: {},
 
  create: {
 
    userId: admin.id,
 
    roleId: superAdminRole.id
  }
});

Étape 10 — Logs

Ajouter

console.log(
  'Tenant created:',
  tenant.code
);
 
console.log(
  'Admin:',
  admin.email
);
 
console.log(
  'Seed completed'
);

Étape 11 — Exécution

Lancer

cd apps/api
 
npx prisma db seed

Résultat attendu

Starting seed...

Tenant created: MAIN

Admin: admin@platform.local

Seed completed

Étape 12 — Vérification Prisma Studio

Ouvrir

npx prisma studio

Vérifier

Tenant

User

Role

Permission

UserRole

RolePermission

Étape 13 — Vérification SQL

Utilisateur

SELECT email
 
FROM "User";

Résultat

admin@platform.local

Permissions

SELECT COUNT(*)
 
FROM "Permission";

Résultat

> 15

Sécurisation future

Ne jamais conserver

Admin123!

en production.


Prévoir

ADMIN_EMAIL

ADMIN_PASSWORD

dans :

.env

pour les environnements réels.


Définition de terminé

Le Sprint 1-A.2 est terminé lorsque :

✓ Tenant créé

✓ Permissions créées

✓ Rôles créés

✓ Super Admin créé

✓ Affectation des permissions

✓ Affectation du rôle

✓ Seed exécutable

✓ Première connexion possible

Livrables

prisma/seed.ts

package.json

Permissions système

Rôles système

Compte Super Admin

Sprint 1-B.1 — Création du AuthModule NestJS

Objectif

Implémenter le premier module métier réellement exécutable de la plateforme.

À l'issue de cette étape :

✓ AuthModule

✓ AuthController

✓ AuthService

✓ PasswordService

✓ JwtStrategy

✓ JwtAuthGuard

✓ Register

✓ Login

✓ Refresh

✓ Logout

✓ Current User

Étape 1 — Création de la structure

Répertoire

Créer :

apps/api/src/modules/auth

Arborescence

auth

├── application
│
│   ├── dto
│   ├── commands
│   ├── queries
│   └── handlers
│
├── domain
│
│   └── services
│
├── infrastructure
│
│   └── strategies
│
├── presentation
│
│   ├── controllers
│   └── guards
│
└── auth.module.ts

Étape 2 — Création du AuthModule

Fichier

auth.module.ts

Implémentation

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
 
import { AuthController } from './presentation/controllers/auth.controller';
import { AuthService } from './domain/services/auth.service';
import { PasswordService } from './domain/services/password.service';
import { JwtStrategy } from './infrastructure/strategies/jwt.strategy';
 
@Module({
 
  imports: [
 
    JwtModule.register({})
  ],
 
  controllers: [
 
    AuthController
  ],
 
  providers: [
 
    AuthService,
 
    PasswordService,
 
    JwtStrategy
  ],
 
  exports: [
 
    AuthService
  ]
})
export class AuthModule {}

Étape 3 — RegisterDto

Fichier

application/dto/register.dto.ts

Code

import {
  IsEmail,
  IsString,
  MinLength
} from 'class-validator';
 
export class RegisterDto {
 
  @IsEmail()
  email: string;
 
  @MinLength(8)
  password: string;
 
  @IsString()
  firstName: string;
 
  @IsString()
  lastName: string;
}

Étape 4 — LoginDto

Fichier

application/dto/login.dto.ts

Code

import {
  IsEmail,
  IsString
} from 'class-validator';
 
export class LoginDto {
 
  @IsEmail()
  email: string;
 
  @IsString()
  password: string;
}

Étape 5 — RefreshTokenDto

Fichier

application/dto/refresh-token.dto.ts

Code

import {
  IsString
} from 'class-validator';
 
export class RefreshTokenDto {
 
  @IsString()
  refreshToken: string;
}

Étape 6 — PasswordService

Fichier

domain/services/password.service.ts

Implémentation

import * as bcrypt from 'bcrypt';
 
export class PasswordService {
 
  async hash(
    password: string
  ): Promise<string> {
 
    return bcrypt.hash(
      password,
      12
    );
  }
 
  async compare(
    password: string,
    hash: string
  ): Promise<boolean> {
 
    return bcrypt.compare(
      password,
      hash
    );
  }
}

Étape 7 — AuthService

Fichier

domain/services/auth.service.ts

Responsabilités

Register

Login

Refresh

Logout

Current User

Squelette

@Injectable()
export class AuthService {
 
  async register() {}
 
  async login() {}
 
  async refresh() {}
 
  async logout() {}
 
  async me() {}
}

Étape 8 — JWT Payload

Fichier

infrastructure/strategies/jwt-payload.interface.ts

Code

export interface JwtPayload {
 
  sub: string;
 
  tenantId: string;
 
  email: string;
}

Étape 9 — JwtStrategy

Fichier

infrastructure/strategies/jwt.strategy.ts

Implémentation

@Injectable()
export class JwtStrategy
extends PassportStrategy(
  Strategy
) {
 
  constructor() {
 
    super({
 
      jwtFromRequest:
        ExtractJwt.fromAuthHeaderAsBearerToken(),
 
      ignoreExpiration: false,
 
      secretOrKey:
        process.env.JWT_SECRET
    });
  }
 
  async validate(
    payload: JwtPayload
  ) {
 
    return payload;
  }
}

Étape 10 — JwtAuthGuard

Fichier

presentation/guards/jwt-auth.guard.ts

Code

@Injectable()
export class JwtAuthGuard
extends AuthGuard('jwt') {}

Étape 11 — AuthController

Fichier

presentation/controllers/auth.controller.ts

Déclaration

@ApiTags('Auth')
 
@Controller('auth')
 
export class AuthController {
 
}

Étape 12 — Endpoint Register

Route

POST /auth/register

Code

@Post('register')
 
register(
  @Body()
  dto: RegisterDto
) {
 
  return this.authService
    .register(dto);
}

Étape 13 — Endpoint Login

Route

POST /auth/login

Code

@Post('login')
 
login(
  @Body()
  dto: LoginDto
) {
 
  return this.authService
    .login(dto);
}

Étape 14 — Endpoint Refresh

Route

POST /auth/refresh

Code

@Post('refresh')
 
refresh(
  @Body()
  dto: RefreshTokenDto
) {
 
  return this.authService
    .refresh(dto);
}

Étape 15 — Endpoint Logout

Route

POST /auth/logout

Sécurisé

@UseGuards(
  JwtAuthGuard
)

Code

@Post('logout')
 
logout(
  @Req() req
) {
 
  return this.authService
    .logout(
      req.user.sub
    );
}

Étape 16 — Endpoint Me

Route

GET /auth/me

Sécurisé

@UseGuards(
  JwtAuthGuard
)

Code

@Get('me')
 
me(
  @Req() req
) {
 
  return this.authService
    .me(
      req.user.sub
    );
}

Étape 17 — Configuration JWT

.env

JWT_SECRET=change-me

JWT_EXPIRES_IN=15m

REFRESH_SECRET=change-me-too

REFRESH_EXPIRES_IN=30d

Étape 18 — Import Module

AppModule

Ajouter :

imports: [
 
  AuthModule
]

Étape 19 — Vérification compilation

Build

nx build api

Résultat attendu

SUCCESS

Étape 20 — Swagger

Vérification

/auth/register

/auth/login

/auth/refresh

/auth/logout

/auth/me

doivent apparaître dans :

http://localhost:3000/api

Définition de terminé

Le Sprint 1-B.1 est terminé lorsque :

✓ AuthModule créé

✓ DTO créés

✓ PasswordService créé

✓ JwtStrategy créée

✓ JwtAuthGuard créé

✓ AuthController créé

✓ Endpoints exposés

✓ Swagger visible

✓ Build vert

Livrables

AuthModule

AuthController

AuthService

PasswordService

JwtStrategy

JwtAuthGuard

RegisterDto

LoginDto

RefreshTokenDto

Sprint 1-B.2 — Implémentation réelle du Register

Objectif

Rendre le endpoint :

POST /auth/register

entièrement fonctionnel.

À l'issue de cette étape :

✓ Vérification email unique

✓ Hash du mot de passe

✓ Création User

✓ Attribution rôle CUSTOMER

✓ Création RefreshToken

✓ Génération JWT

✓ Retour utilisateur connecté

Workflow complet

Register Request

↓

Validation DTO

↓

Vérification email

↓

Hash Password

↓

Create User

↓

Assign CUSTOMER Role

↓

Generate JWT

↓

Generate Refresh Token

↓

Persist Refresh Token

↓

Response

Étape 1 — Création des types de réponse

AuthResponse

Créer :

application/dto/auth-response.dto.ts

Code

export class AuthResponseDto {
 
  accessToken: string;
 
  refreshToken: string;
 
  user: {
 
    id: string;
 
    email: string;
 
    firstName: string;
 
    lastName: string;
  };
}

Étape 2 — Injection Prisma

Prérequis

Le :

PrismaModule

doit exporter :

PrismaService

AuthModule

Ajouter :

imports: [
 
  PrismaModule,
 
  JwtModule.register({})
]

Étape 3 — Injection des dépendances

AuthService

Modifier :

@Injectable()
export class AuthService {
 
  constructor(
 
    private readonly prisma: PrismaService,
 
    private readonly jwtService: JwtService,
 
    private readonly passwordService: PasswordService
  ) {}
}

Étape 4 — Recherche du rôle CUSTOMER

Constante

Créer :

shared/constants/roles.constants.ts

Code

export const SYSTEM_ROLES = {
 
  SUPER_ADMIN: 'SUPER_ADMIN',
 
  ADMIN: 'ADMIN',
 
  MANAGER: 'MANAGER',
 
  OWNER: 'OWNER',
 
  CUSTOMER: 'CUSTOMER'
};

Étape 5 — Création du Register

Signature

async register(
  dto: RegisterDto
): Promise<AuthResponseDto>

Étape 6 — Vérification email

Ajouter

const existingUser =
  await this.prisma.user.findUnique({
 
    where: {
 
      email: dto.email
    }
  });
 
if (existingUser) {
 
  throw new ConflictException(
    'Email already exists'
  );
}

Étape 7 — Charger le Tenant principal

Ajouter

const tenant =
  await this.prisma.tenant.findUnique({
 
    where: {
 
      code: 'MAIN'
    }
  });
 
if (!tenant) {
 
  throw new Error(
    'Main tenant not found'
  );
}

Étape 8 — Hash mot de passe

Ajouter

const passwordHash =
  await this.passwordService.hash(
    dto.password
  );

Étape 9 — Création User

Ajouter

const user =
  await this.prisma.user.create({
 
    data: {
 
      tenantId: tenant.id,
 
      email: dto.email,
 
      passwordHash,
 
      firstName: dto.firstName,
 
      lastName: dto.lastName,
 
      status: 'ACTIVE'
    }
  });

Étape 10 — Charger rôle CUSTOMER

Ajouter

const customerRole =
  await this.prisma.role.findFirst({
 
    where: {
 
      tenantId: tenant.id,
 
      code: SYSTEM_ROLES.CUSTOMER
    }
  });
 
if (!customerRole) {
 
  throw new Error(
    'Customer role missing'
  );
}

Étape 11 — Affecter le rôle

Ajouter

await this.prisma.userRole.create({
 
  data: {
 
    userId: user.id,
 
    roleId: customerRole.id
  }
});

Étape 12 — Génération JWT

Payload

const payload = {
 
  sub: user.id,
 
  tenantId: tenant.id,
 
  email: user.email
};

Token

const accessToken =
  await this.jwtService.signAsync(
    payload,
    {
      secret:
        process.env.JWT_SECRET,
 
      expiresIn:
        process.env.JWT_EXPIRES_IN
    }
  );

Étape 13 — Génération Refresh Token

Token brut

const refreshToken =
  randomUUID();

Hash

const refreshTokenHash =
  await this.passwordService.hash(
    refreshToken
  );

Sauvegarde

await this.prisma.refreshToken.create({
 
  data: {
 
    userId: user.id,
 
    tokenHash: refreshTokenHash,
 
    expiresAt: addDays(
      new Date(),
      30
    )
  }
});

Étape 14 — Réponse

Retour

return {
 
  accessToken,
 
  refreshToken,
 
  user: {
 
    id: user.id,
 
    email: user.email,
 
    firstName: user.firstName,
 
    lastName: user.lastName
  }
};

Étape 15 — Controller

Route

@Post('register')
 
@ApiOperation({
 
  summary:
    'Register a new user'
})

Response

@ApiCreatedResponse({
 
  type: AuthResponseDto
})

Étape 16 — Validation globale

main.ts

Vérifier :

app.useGlobalPipes(
 
  new ValidationPipe({
 
    whitelist: true,
 
    forbidNonWhitelisted: true,
 
    transform: true
  })
);

Étape 17 — Test manuel

Requête

POST /auth/register

Body

{
  "email": "john@example.com",
  "password": "Password123!",
  "firstName": "John",
  "lastName": "Doe"
}

Réponse attendue

{
  "accessToken": "...",
 
  "refreshToken": "...",
 
  "user": {
 
    "id": "...",
 
    "email": "john@example.com",
 
    "firstName": "John",
 
    "lastName": "Doe"
  }
}

Étape 18 — Vérification SQL

User

SELECT email
 
FROM "User";

UserRole

SELECT *
 
FROM "UserRole";

RefreshToken

SELECT *
 
FROM "RefreshToken";

Cas d'erreur

Email déjà utilisé

{
  "statusCode": 409,
 
  "message":
    "Email already exists"
}

Tenant absent

{
  "statusCode": 500,
 
  "message":
    "Main tenant not found"
}

Définition de terminé

Le Sprint 1-B.2 est terminé lorsque :

✓ Register fonctionnel

✓ User créé

✓ Password hashé

✓ CUSTOMER assigné

✓ JWT généré

✓ Refresh Token généré

✓ Swagger documenté

✓ Test manuel réussi

Livrables

AuthService.register()

AuthResponseDto

roles.constants.ts

Swagger Register

Validation complète

Sprint 1-B.3 — Implémentation réelle du Login

Objectif

Rendre le endpoint :

POST /auth/login

entièrement fonctionnel.

À l'issue de cette étape :

✓ Recherche utilisateur

✓ Vérification mot de passe

✓ Vérification statut utilisateur

✓ Création Session

✓ Génération JWT

✓ Génération Refresh Token

✓ Historisation connexion

✓ Retour utilisateur authentifié

Workflow complet

Login Request

↓

Find User

↓

Verify Password

↓

Verify Status

↓

Create Session

↓

Generate JWT

↓

Generate Refresh Token

↓

Store Refresh Token

↓

Return Auth Response

Étape 1 — Création de SessionDto

Créer

application/dto/session-info.dto.ts

Contenu

export class SessionInfoDto {
 
  ipAddress?: string;
 
  userAgent?: string;
}

Étape 2 — Mise à jour AuthController

Login

Modifier :

@Post('login')
 
login(
 
  @Body()
  dto: LoginDto,
 
  @Req()
  request: Request
) {
 
  return this.authService.login(
 
    dto,
 
    {
      ipAddress: request.ip,
 
      userAgent:
        request.headers['user-agent']
    }
  );
}

Étape 3 — Signature Login

AuthService

Modifier :

async login(
 
  dto: LoginDto,
 
  session: SessionInfoDto
): Promise<AuthResponseDto>

Étape 4 — Recherche utilisateur

Ajouter

const user =
  await this.prisma.user.findUnique({
 
    where: {
 
      email: dto.email
    },
 
    include: {
 
      userRoles: {
 
        include: {
 
          role: true
        }
      }
    }
  });

Vérification

if (!user) {
 
  throw new UnauthorizedException(
    'Invalid credentials'
  );
}

Étape 5 — Vérification statut

Ajouter

if (
 
  user.status !== 'ACTIVE'
 
) {
 
  throw new ForbiddenException(
    'Account disabled'
  );
}

Étape 6 — Vérification mot de passe

Ajouter

const validPassword =
  await this.passwordService.compare(
 
    dto.password,
 
    user.passwordHash
  );

Contrôle

if (!validPassword) {
 
  throw new UnauthorizedException(
    'Invalid credentials'
  );
}

Étape 7 — Génération du JWT

Payload

const payload = {
 
  sub: user.id,
 
  tenantId: user.tenantId,
 
  email: user.email
};

Génération

const accessToken =
  await this.jwtService.signAsync(
 
    payload,
 
    {
 
      secret:
        process.env.JWT_SECRET,
 
      expiresIn:
        process.env.JWT_EXPIRES_IN
    }
  );

Étape 8 — Génération Refresh Token

Token

const refreshToken =
  randomUUID();

Hash

const refreshTokenHash =
  await this.passwordService.hash(
 
    refreshToken
  );

Sauvegarde

await this.prisma.refreshToken.create({
 
  data: {
 
    userId: user.id,
 
    tokenHash: refreshTokenHash,
 
    expiresAt: addDays(
      new Date(),
      30
    )
  }
});

Étape 9 — Création Session

Ajouter

await this.prisma.session.create({
 
  data: {
 
    userId: user.id,
 
    ipAddress:
      session.ipAddress,
 
    userAgent:
      session.userAgent,
 
    lastSeenAt:
      new Date()
  }
});

Étape 10 — Historisation connexion

Mise à jour User

await this.prisma.user.update({
 
  where: {
 
    id: user.id
  },
 
  data: {
 
    lastLoginAt:
      new Date()
  }
});

Étape 11 — Chargement des rôles

Extraire

const roles =
  user.userRoles.map(
 
    item => item.role.code
  );

Étape 12 — Retour réponse

Réponse

return {
 
  accessToken,
 
  refreshToken,
 
  user: {
 
    id: user.id,
 
    email: user.email,
 
    firstName: user.firstName,
 
    lastName: user.lastName,
 
    roles
  }
};

Étape 13 — Swagger

Login

Ajouter :

@ApiOperation({
 
  summary:
    'Authenticate user'
})

Response

@ApiOkResponse({
 
  type: AuthResponseDto
})

Étape 14 — Cas d'erreurs

Mauvais mot de passe

{
  "statusCode": 401,
 
  "message":
    "Invalid credentials"
}

Utilisateur absent

{
  "statusCode": 401,
 
  "message":
    "Invalid credentials"
}

Compte bloqué

{
  "statusCode": 403,
 
  "message":
    "Account disabled"
}

Étape 15 — Test Swagger

Requête

POST /auth/login

Body

{
  "email": "admin@platform.local",
 
  "password": "Admin123!"
}

Réponse

{
  "accessToken": "...",
 
  "refreshToken": "...",
 
  "user": {
 
    "id": "...",
 
    "email": "admin@platform.local",
 
    "firstName": "Super",
 
    "lastName": "Admin",
 
    "roles": [
 
      "SUPER_ADMIN"
    ]
  }
}

Étape 16 — Vérification SQL

Session

SELECT *
 
FROM "Session";

RefreshToken

SELECT *
 
FROM "RefreshToken";

User

SELECT
  email,
  "lastLoginAt"
 
FROM "User";

Refactoring recommandé

À partir de maintenant, factoriser la génération des tokens.

Créer :

domain/services/token.service.ts

Responsabilités

generateAccessToken()

generateRefreshToken()

verifyRefreshToken()

Définition de terminé

Le Sprint 1-B.3 est terminé lorsque :

✓ Login fonctionnel

✓ Password vérifié

✓ Statut vérifié

✓ JWT généré

✓ Refresh Token généré

✓ Session créée

✓ Historique connexion mis à jour

✓ Swagger documenté

✓ Test manuel réussi

Livrables

AuthService.login()

SessionInfoDto

Swagger Login

Création Session

Historisation connexion

Sprint 1-B.4 — Implémentation réelle du Refresh Token

Objectif

Rendre le endpoint :

POST /auth/refresh

entièrement fonctionnel.

À l'issue de cette étape :

✓ Validation Refresh Token

✓ Recherche utilisateur

✓ Vérification expiration

✓ Rotation Refresh Token

✓ Révocation ancien token

✓ Nouveau JWT

✓ Nouveau Refresh Token

✓ Réponse authentifiée

Workflow complet

Refresh Request

↓

Find Valid Refresh Token

↓

Verify Token

↓

Load User

↓

Revoke Old Token

↓

Generate New JWT

↓

Generate New Refresh Token

↓

Persist New Refresh Token

↓

Return Tokens

Étape 1 — Création du TokenService

Créer

src/modules/auth/domain/services/token.service.ts

Responsabilités

generateAccessToken()

generateRefreshToken()

hashRefreshToken()

verifyRefreshToken()

Squelette

@Injectable()
export class TokenService {
 
  constructor(
    private readonly jwtService: JwtService,
    private readonly passwordService: PasswordService
  ) {}
}

Étape 2 — Génération Access Token

Ajouter

async generateAccessToken(
  payload: JwtPayload
): Promise<string> {
 
  return this.jwtService.signAsync(
    payload,
    {
      secret:
        process.env.JWT_SECRET,
 
      expiresIn:
        process.env.JWT_EXPIRES_IN
    }
  );
}

Étape 3 — Génération Refresh Token

Ajouter

async generateRefreshToken() {
 
  const rawToken =
    randomUUID();
 
  return rawToken;
}

Étape 4 — Hash Refresh Token

Ajouter

async hashRefreshToken(
  token: string
) {
 
  return this.passwordService.hash(
    token
  );
}

Étape 5 — Enregistrement Refresh Token

Ajouter

async storeRefreshToken(
 
  userId: string,
 
  token: string
) {
 
  const tokenHash =
    await this.hashRefreshToken(
      token
    );
 
  return prisma.refreshToken.create({
 
    data: {
 
      userId,
 
      tokenHash,
 
      expiresAt: addDays(
        new Date(),
        30
      )
    }
  });
}

Étape 6 — Injection dans AuthModule

Ajouter

providers: [
 
  AuthService,
 
  PasswordService,
 
  TokenService,
 
  JwtStrategy
]

Étape 7 — Injection dans AuthService

Constructeur

constructor(
 
  private readonly prisma:
    PrismaService,
 
  private readonly tokenService:
    TokenService,
 
  private readonly passwordService:
    PasswordService
) {}

Étape 8 — Signature Refresh

AuthService

async refresh(
 
  dto: RefreshTokenDto
): Promise<AuthResponseDto>

Étape 9 — Charger les Refresh Tokens

Ajouter

const refreshTokens =
  await this.prisma.refreshToken.findMany({
 
    where: {
 
      revokedAt: null,
 
      expiresAt: {
 
        gt: new Date()
      }
    },
 
    include: {
 
      user: {
 
        include: {
 
          userRoles: {
 
            include: {
 
              role: true
            }
          }
        }
      }
    }
  });

Étape 10 — Vérification du token

Recherche

let matchedToken = null;
 
for (
  const refreshToken
  of refreshTokens
) {
 
  const valid =
    await this.passwordService.compare(
 
      dto.refreshToken,
 
      refreshToken.tokenHash
    );
 
  if (valid) {
 
    matchedToken =
      refreshToken;
 
    break;
  }
}

Contrôle

if (!matchedToken) {
 
  throw new UnauthorizedException(
    'Invalid refresh token'
  );
}

Étape 11 — Vérifier utilisateur

Ajouter

const user =
  matchedToken.user;

Contrôle

if (
 
  user.status !== 'ACTIVE'
 
) {
 
  throw new UnauthorizedException(
    'User inactive'
  );
}

Étape 12 — Révocation ancien Refresh Token

Ajouter

await this.prisma.refreshToken.update({
 
  where: {
 
    id: matchedToken.id
  },
 
  data: {
 
    revokedAt:
      new Date()
  }
});

Étape 13 — Génération nouveau JWT

Payload

const payload = {
 
  sub: user.id,
 
  tenantId: user.tenantId,
 
  email: user.email
};

Génération

const accessToken =
  await this.tokenService
    .generateAccessToken(
      payload
    );

Étape 14 — Génération nouveau Refresh Token

Ajouter

const refreshToken =
  await this.tokenService
    .generateRefreshToken();

Persistance

await this.tokenService
  .storeRefreshToken(
 
    user.id,
 
    refreshToken
  );

Étape 15 — Chargement des rôles

Ajouter

const roles =
  user.userRoles.map(
 
    role => role.role.code
  );

Étape 16 — Retour

Réponse

return {
 
  accessToken,
 
  refreshToken,
 
  user: {
 
    id: user.id,
 
    email: user.email,
 
    firstName:
      user.firstName,
 
    lastName:
      user.lastName,
 
    roles
  }
};

Étape 17 — Swagger

Route

@Post('refresh')

Documentation

@ApiOperation({
 
  summary:
    'Refresh access token'
})
 
@ApiOkResponse({
 
  type: AuthResponseDto
})

Étape 18 — Cas d'erreurs

Token invalide

{
  "statusCode": 401,
 
  "message":
    "Invalid refresh token"
}

Utilisateur inactif

{
  "statusCode": 401,
 
  "message":
    "User inactive"
}

Étape 19 — Test Swagger

Requête

POST /auth/refresh

Body

{
  "refreshToken":
    "xxxxxxxx"
}

Réponse

{
  "accessToken": "...",
 
  "refreshToken": "...",
 
  "user": {
 
    "id": "...",
 
    "email": "...",
 
    "roles": [
      "CUSTOMER"
    ]
  }
}

Étape 20 — Vérification SQL

Ancien token

SELECT *
 
FROM "RefreshToken"
 
WHERE "revokedAt"
IS NOT NULL;

Nouveau token

SELECT *
 
FROM "RefreshToken"
 
WHERE "revokedAt"
IS NULL;

Optimisation recommandée

Problème

La recherche par :

findMany()

puis comparaison BCrypt n'est pas scalable.


Amélioration Sprint 1.5

Ajouter :

tokenId String @unique

dans :

RefreshToken

et stocker :

tokenId.rawToken

afin de retrouver directement le token.


Définition de terminé

Le Sprint 1-B.4 est terminé lorsque :

✓ Refresh fonctionnel

✓ Ancien token révoqué

✓ Nouveau JWT généré

✓ Nouveau Refresh Token généré

✓ Rotation opérationnelle

✓ Swagger documenté

✓ Tests manuels réussis

Livrables

TokenService

AuthService.refresh()

Swagger Refresh

Rotation Refresh Token

Sprint 1-B.5 — Implémentation réelle du Logout

Objectif

Rendre le endpoint :

POST /auth/logout

entièrement fonctionnel.

À l'issue de cette étape :

✓ JWT requis

✓ Session révoquée

✓ Refresh Tokens révoqués

✓ Déconnexion complète

✓ Audit de déconnexion

✓ Swagger documenté

Workflow complet

Logout Request

↓

JWT Validation

↓

Load User

↓

Load Active Session

↓

Revoke Session

↓

Revoke Refresh Tokens

↓

Audit Log

↓

Success Response

Étape 1 — Création du DTO de réponse

Créer

application/dto/logout-response.dto.ts

Code

export class LogoutResponseDto {
 
  success: boolean;
 
  message: string;
}

Étape 2 — Amélioration du JWT Payload

Fichier

jwt-payload.interface.ts

Ajouter

export interface JwtPayload {
 
  sub: string;
 
  tenantId: string;
 
  email: string;
 
  sessionId?: string;
}

Objectif

Pouvoir relier :

JWT

↓

Session

afin de révoquer précisément la session active.


Étape 3 — Adapter le Login

Lors de la création Session

Modifier :

const createdSession =
  await this.prisma.session.create({
    data: {
      userId: user.id,
      ipAddress: session.ipAddress,
      userAgent: session.userAgent,
      lastSeenAt: new Date()
    }
  });

JWT Payload

Ajouter :

const payload = {
 
  sub: user.id,
 
  tenantId: user.tenantId,
 
  email: user.email,
 
  sessionId: createdSession.id
};

Résultat

Chaque JWT référence désormais :

Session.id

Étape 4 — Signature Logout

AuthService

Ajouter :

async logout(
 
  userId: string,
 
  sessionId?: string
): Promise<LogoutResponseDto>

Étape 5 — Révocation Session

Ajouter

if (sessionId) {
 
  await this.prisma.session.updateMany({
 
    where: {
 
      id: sessionId,
 
      userId
    },
 
    data: {
 
      revokedAt:
        new Date()
    }
  });
}

Résultat

La session courante devient :

Révoquée

Étape 6 — Révocation Refresh Tokens

Ajouter

await this.prisma.refreshToken.updateMany({
 
  where: {
 
    userId,
 
    revokedAt: null
  },
 
  data: {
 
    revokedAt:
      new Date()
  }
});

Résultat

Tous les refresh tokens actifs :

INVALIDES

Étape 7 — Mise à jour dernière activité

Optionnel

Ajouter :

await this.prisma.user.update({
 
  where: {
 
    id: userId
  },
 
  data: {
 
    updatedAt:
      new Date()
  }
});

Étape 8 — Réponse

Retour

return {
 
  success: true,
 
  message:
    'Successfully logged out'
};

Étape 9 — Controller

Modifier

@Post('logout')
 
@UseGuards(
  JwtAuthGuard
)
 
@ApiBearerAuth()

Implémentation

logout(
  @Req() req
) {
 
  return this.authService.logout(
 
    req.user.sub,
 
    req.user.sessionId
  );
}

Étape 10 — Swagger

Documentation

@ApiOperation({
 
  summary:
    'Logout current user'
})

Réponse

@ApiOkResponse({
 
  type:
    LogoutResponseDto
})

Étape 11 — Cas particulier

Déconnexion globale

Créer un futur endpoint :

POST /auth/logout-all

Fonction

Révoquer :

Toutes les sessions

Tous les refresh tokens

sur tous les appareils.


Étape 12 — Vérification Session

SQL

SELECT
 
  id,
 
  "revokedAt"
 
FROM "Session";

Résultat attendu

revokedAt != null

Étape 13 — Vérification Refresh Tokens

SQL

SELECT
 
  id,
 
  "revokedAt"
 
FROM "RefreshToken";

Résultat attendu

Tous les tokens actifs deviennent :

revokedAt != null

Étape 14 — Test Swagger

Login

POST /auth/login

Récupérer :

accessToken

Logout

POST /auth/logout
Authorization: Bearer xxx

Réponse

{
  "success": true,
  "message": "Successfully logged out"
}

Étape 15 — Vérification sécurité

Tester

Réutiliser :

Refresh Token précédent

Résultat attendu

{
  "statusCode": 401,
  "message": "Invalid refresh token"
}

Amélioration Sprint 1-C

Pour renforcer la sécurité Enterprise :

Ajouter dans :

model Session

le champ :

sessionToken String? @unique

afin de permettre :

Gestion multi-appareils

Révocation ciblée

Historique complet

Définition de terminé

Le Sprint 1-B.5 est terminé lorsque :

✓ Logout fonctionnel

✓ Session révoquée

✓ Refresh Tokens révoqués

✓ Swagger documenté

✓ Test manuel réussi

✓ Sécurité validée

Livrables

AuthService.logout()

LogoutResponseDto

Swagger Logout

Révocation Session

Révocation RefreshToken

Sprint 1-B.6 — Implémentation réelle du Current User

Objectif

Rendre le endpoint :

GET /auth/me

entièrement fonctionnel.

À l'issue de cette étape :

✓ JWT validé

✓ Utilisateur chargé

✓ Rôles chargés

✓ Permissions chargées

✓ Tenant chargé

✓ Profil utilisateur complet

✓ Swagger documenté

Workflow complet

GET /auth/me

↓

JWT Validation

↓

Extract UserId

↓

Load User

↓

Load Roles

↓

Load Permissions

↓

Build Profile

↓

Return Response

Étape 1 — DTO de réponse

Créer

application/dto/current-user.dto.ts

Contenu

export class CurrentUserDto {
 
  id: string;
 
  tenantId: string;
 
  email: string;
 
  firstName: string;
 
  lastName: string;
 
  phone?: string;
 
  status: string;
 
  emailVerified: boolean;
 
  roles: string[];
 
  permissions: string[];
 
  tenant: {
 
    id: string;
 
    code: string;
 
    name: string;
  };
}

Étape 2 — Signature du service

AuthService

Ajouter :

async me(
  userId: string
): Promise<CurrentUserDto>

Étape 3 — Chargement utilisateur

Ajouter

const user =
  await this.prisma.user.findUnique({
 
    where: {
 
      id: userId
    },
 
    include: {
 
      tenant: true,
 
      userRoles: {
 
        include: {
 
          role: {
 
            include: {
 
              rolePermissions: {
 
                include: {
 
                  permission: true
                }
              }
            }
          }
        }
      }
    }
  });

Étape 4 — Vérification utilisateur

Ajouter

if (!user) {
 
  throw new NotFoundException(
    'User not found'
  );
}

Étape 5 — Extraction des rôles

Ajouter

const roles =
  user.userRoles.map(
 
    item => item.role.code
  );

Résultat

Exemple :

[
  "SUPER_ADMIN",
  "OWNER"
]

Étape 6 — Extraction permissions

Ajouter

const permissions =
  user.userRoles.flatMap(
 
    userRole =>
 
      userRole.role
        .rolePermissions
        .map(
 
          rp =>
            rp.permission.code
        )
  );

Étape 7 — Déduplication

Ajouter

const uniquePermissions =
 
  [...new Set(
    permissions
  )];

Résultat

users.read

users.create

properties.read

reservations.read

Étape 8 — Construction réponse

Retour

return {
 
  id: user.id,
 
  tenantId: user.tenantId,
 
  email: user.email,
 
  firstName: user.firstName,
 
  lastName: user.lastName,
 
  phone: user.phone,
 
  status: user.status,
 
  emailVerified:
    user.emailVerified,
 
  roles,
 
  permissions:
    uniquePermissions,
 
  tenant: {
 
    id: user.tenant.id,
 
    code: user.tenant.code,
 
    name: user.tenant.name
  }
};

Étape 9 — Contrôleur

Endpoint

@Get('me')
 
@UseGuards(
  JwtAuthGuard
)
 
@ApiBearerAuth()

Implémentation

me(
  @Req() req
) {
 
  return this.authService.me(
    req.user.sub
  );
}

Étape 10 — Swagger

Documentation

@ApiOperation({
 
  summary:
    'Get current authenticated user'
})

Réponse

@ApiOkResponse({
 
  type:
    CurrentUserDto
})

Étape 11 — Exemple de réponse

Retour attendu

{
  "id": "e1d1c8",
 
  "tenantId": "tenant1",
 
  "email":
    "admin@platform.local",
 
  "firstName":
    "Super",
 
  "lastName":
    "Admin",
 
  "status":
    "ACTIVE",
 
  "emailVerified": true,
 
  "roles": [
 
    "SUPER_ADMIN"
  ],
 
  "permissions": [
 
    "users.read",
 
    "users.create",
 
    "users.update",
 
    "users.delete",
 
    "roles.manage"
  ],
 
  "tenant": {
 
    "id": "tenant-id",
 
    "code": "MAIN",
 
    "name":
      "Main Tenant"
  }
}

Étape 12 — Sécurisation

Refuser

Utilisateur supprimé

Utilisateur bloqué

Utilisateur inactif

Ajouter

if (
 
  user.status !== 'ACTIVE'
 
) {
 
  throw new ForbiddenException(
    'Account disabled'
  );
}

Étape 13 — Test Swagger

Login

POST /auth/login

Copier

accessToken

Tester

GET /auth/me
 
Authorization:
Bearer xxx

Résultat

Profil utilisateur complet.


Étape 14 — Vérification SQL

Utilisateur

SELECT *
 
FROM "User"
 
WHERE email =
'admin@platform.local';

Rôles

SELECT *
 
FROM "UserRole";

Permissions

SELECT *
 
FROM "RolePermission";

Optimisation future

Sprint 1-C

Ajouter :

Redis Cache

sur :

GET /auth/me

afin d'éviter :

Chargement complet
des permissions
à chaque requête.

Définition de terminé

Le Sprint 1-B.6 est terminé lorsque :

✓ GET /auth/me fonctionnel

✓ JWT validé

✓ Utilisateur chargé

✓ Rôles chargés

✓ Permissions chargées

✓ Swagger documenté

✓ Test manuel réussi

Livrables

CurrentUserDto

AuthService.me()

Swagger Current User

Chargement rôles

Chargement permissions

Bilan Sprint 1-B

Les endpoints suivants sont maintenant opérationnels :

POST /auth/register

POST /auth/login

POST /auth/refresh

POST /auth/logout

GET /auth/me

Sprint 1-C.1 — Implémentation du RBAC Enterprise

Objectif

Mettre en place le moteur de contrôle d'accès central de la plateforme.

À l'issue de cette étape :

✓ @Roles()

✓ RolesGuard

✓ @Permissions()

✓ PermissionsGuard

✓ Metadata Reflector

✓ Contrôle d'accès complet

✓ Swagger compatible

✓ Réutilisable dans tous les modules

Architecture cible

Controller

↓

@Roles()

@Permissions()

↓

JwtAuthGuard

↓

RolesGuard

↓

PermissionsGuard

↓

Access Granted

Structure

Créer

src/modules/auth/presentation

├── decorators
│
│   ├── roles.decorator.ts
│   └── permissions.decorator.ts
│
├── guards
│
│   ├── jwt-auth.guard.ts
│   ├── roles.guard.ts
│   └── permissions.guard.ts
│
└── constants
    └── auth.constants.ts

Étape 1 — Constantes RBAC

Créer

auth.constants.ts

Contenu

export const ROLES_KEY =
  'roles';
 
export const PERMISSIONS_KEY =
  'permissions';

Étape 2 — Décorateur Roles

Créer

roles.decorator.ts

Implémentation

import { SetMetadata }
from '@nestjs/common';
 
import {
  ROLES_KEY
}
from '../constants/auth.constants';
 
export const Roles = (
 
  ...roles: string[]
 
) => SetMetadata(
  ROLES_KEY,
  roles
);

Étape 3 — Décorateur Permissions

Créer

permissions.decorator.ts

Implémentation

import { SetMetadata }
from '@nestjs/common';
 
import {
  PERMISSIONS_KEY
}
from '../constants/auth.constants';
 
export const Permissions = (
 
  ...permissions: string[]
 
) => SetMetadata(
  PERMISSIONS_KEY,
  permissions
);

Étape 4 — Extension JWT Payload

Fichier

jwt-payload.interface.ts

Ajouter

roles?: string[];
 
permissions?: string[];

Objectif

Éviter des requêtes SQL inutiles.

Le JWT transporte :

Roles

Permissions

du compte connecté.


Étape 5 — Enrichir le Login

Charger permissions

Dans :

AuthService.login()

Ajouter

const permissions =
 
  user.userRoles.flatMap(
 
    userRole =>
 
      userRole.role
        .rolePermissions
        .map(
 
          rp =>
            rp.permission.code
        )
  );

Dédupliquer

const uniquePermissions =
 
  [...new Set(
    permissions
  )];

Payload JWT

Modifier :

const payload = {
 
  sub: user.id,
 
  tenantId: user.tenantId,
 
  email: user.email,
 
  sessionId: createdSession.id,
 
  roles,
 
  permissions:
    uniquePermissions
};

Étape 6 — Enrichir Register

Ajouter

Lors du :

register()

Payload

roles: [
 
  SYSTEM_ROLES.CUSTOMER
]
 
permissions: []

Étape 7 — Création RolesGuard

Créer

roles.guard.ts

Injection

constructor(
 
  private reflector:
    Reflector
) {}

Implémentation

@Injectable()
export class RolesGuard
implements CanActivate {
 
  constructor(
    private reflector:
      Reflector
  ) {}
 
  canActivate(
    context:
      ExecutionContext
  ): boolean {
 
    const requiredRoles =
 
      this.reflector.getAllAndOverride<
        string[]
      >(
        ROLES_KEY,
        [
          context.getHandler(),
          context.getClass()
        ]
      );
 
    if (
      !requiredRoles ||
      requiredRoles.length === 0
    ) {
 
      return true;
    }
 
    const request =
      context
        .switchToHttp()
        .getRequest();
 
    const user =
      request.user;
 
    return requiredRoles.some(
 
      role =>
 
        user.roles?.includes(
          role
        )
    );
  }
}

Étape 8 — Création PermissionsGuard

Créer

permissions.guard.ts

Implémentation

@Injectable()
export class PermissionsGuard
implements CanActivate {
 
  constructor(
    private reflector:
      Reflector
  ) {}
 
  canActivate(
    context:
      ExecutionContext
  ): boolean {
 
    const requiredPermissions =
 
      this.reflector.getAllAndOverride<
        string[]
      >(
        PERMISSIONS_KEY,
        [
          context.getHandler(),
          context.getClass()
        ]
      );
 
    if (
      !requiredPermissions ||
      requiredPermissions.length === 0
    ) {
 
      return true;
    }
 
    const request =
      context
        .switchToHttp()
        .getRequest();
 
    const user =
      request.user;
 
    return requiredPermissions.every(
 
      permission =>
 
        user.permissions?.includes(
          permission
        )
    );
  }
}

Étape 9 — Mise à jour JwtStrategy

Validation

Remplacer :

return payload;

Par

return {
 
  id: payload.sub,
 
  tenantId:
    payload.tenantId,
 
  email:
    payload.email,
 
  sessionId:
    payload.sessionId,
 
  roles:
    payload.roles ?? [],
 
  permissions:
    payload.permissions ?? []
};

Étape 10 — Export Guards

AuthModule

Ajouter :

providers: [
 
  AuthService,
 
  PasswordService,
 
  TokenService,
 
  JwtStrategy,
 
  RolesGuard,
 
  PermissionsGuard
]

Exports

exports: [
 
  RolesGuard,
 
  PermissionsGuard
]

Étape 11 — Utilisation Roles

Exemple

@Get()
 
@UseGuards(
 
  JwtAuthGuard,
 
  RolesGuard
)
 
@Roles(
  'SUPER_ADMIN'
)
 
findAllUsers() {}

Étape 12 — Utilisation Permissions

Exemple

@Get()
 
@UseGuards(
 
  JwtAuthGuard,
 
  PermissionsGuard
)
 
@Permissions(
  'users.read'
)
 
findAllUsers() {}

Étape 13 — Combinaison RBAC

Exemple

@Get()
 
@UseGuards(
 
  JwtAuthGuard,
 
  RolesGuard,
 
  PermissionsGuard
)
 
@Roles(
  'ADMIN',
  'SUPER_ADMIN'
)
 
@Permissions(
  'users.read'
)
 
findAllUsers() {}

Étape 14 — Création d'un endpoint de test

AuthController

Ajouter :

@Get('admin')
 
@UseGuards(
 
  JwtAuthGuard,
 
  RolesGuard
)
 
@Roles(
  'SUPER_ADMIN'
)
 
adminOnly() {
 
  return {
 
    success: true
  };
}

Étape 15 — Endpoint permissions

Ajouter

@Get('permissions')
 
@UseGuards(
 
  JwtAuthGuard,
 
  PermissionsGuard
)
 
@Permissions(
  'users.read'
)
 
permissionsOnly() {
 
  return {
 
    success: true
  };
}

Étape 16 — Tests manuels

SUPER_ADMIN

Doit accéder :

GET /auth/admin

CUSTOMER

Doit recevoir :

{
  "statusCode": 403,
  "message": "Forbidden resource"
}

Étape 17 — Gestion du Super Admin

Bypass recommandé

Dans :

PermissionsGuard

ajouter :

if (
 
  user.roles?.includes(
    'SUPER_ADMIN'
  )
) {
 
  return true;
}

Résultat

Le :

SUPER_ADMIN

contourne toutes les permissions.


Étape 18 — Préparation Sprint 2

Le RBAC est maintenant réutilisable sur :

Users

Properties

Reservations

CRM

Finance

Marketing

Security

Administration

Définition de terminé

Le Sprint 1-C.1 est terminé lorsque :

✓ @Roles créé

✓ @Permissions créé

✓ RolesGuard créé

✓ PermissionsGuard créé

✓ JWT enrichi

✓ Contrôle d'accès fonctionnel

✓ Tests manuels réussis

✓ Swagger toujours opérationnel

Livrables

RolesDecorator

PermissionsDecorator

RolesGuard

PermissionsGuard

JWT enrichi

RBAC Enterprise

Sprint 1-C.1 — Implémentation du RBAC Enterprise

Objectif

Mettre en place le moteur de contrôle d'accès central de la plateforme.

À l'issue de cette étape :

✓ @Roles()

✓ RolesGuard

✓ @Permissions()

✓ PermissionsGuard

✓ Metadata Reflector

✓ Contrôle d'accès complet

✓ Swagger compatible

✓ Réutilisable dans tous les modules

Architecture cible

Controller

↓

@Roles()

@Permissions()

↓

JwtAuthGuard

↓

RolesGuard

↓

PermissionsGuard

↓

Access Granted

Structure

Créer

src/modules/auth/presentation

├── decorators
│
│   ├── roles.decorator.ts
│   └── permissions.decorator.ts
│
├── guards
│
│   ├── jwt-auth.guard.ts
│   ├── roles.guard.ts
│   └── permissions.guard.ts
│
└── constants
    └── auth.constants.ts

Étape 1 — Constantes RBAC

Créer

auth.constants.ts

Contenu

export const ROLES_KEY =
  'roles';
 
export const PERMISSIONS_KEY =
  'permissions';

Étape 2 — Décorateur Roles

Créer

roles.decorator.ts

Implémentation

import { SetMetadata }
from '@nestjs/common';
 
import {
  ROLES_KEY
}
from '../constants/auth.constants';
 
export const Roles = (
 
  ...roles: string[]
 
) => SetMetadata(
  ROLES_KEY,
  roles
);

Étape 3 — Décorateur Permissions

Créer

permissions.decorator.ts

Implémentation

import { SetMetadata }
from '@nestjs/common';
 
import {
  PERMISSIONS_KEY
}
from '../constants/auth.constants';
 
export const Permissions = (
 
  ...permissions: string[]
 
) => SetMetadata(
  PERMISSIONS_KEY,
  permissions
);

Étape 4 — Extension JWT Payload

Fichier

jwt-payload.interface.ts

Ajouter

roles?: string[];
 
permissions?: string[];

Objectif

Éviter des requêtes SQL inutiles.

Le JWT transporte :

Roles

Permissions

du compte connecté.


Étape 5 — Enrichir le Login

Charger permissions

Dans :

AuthService.login()

Ajouter

const permissions =
 
  user.userRoles.flatMap(
 
    userRole =>
 
      userRole.role
        .rolePermissions
        .map(
 
          rp =>
            rp.permission.code
        )
  );

Dédupliquer

const uniquePermissions =
 
  [...new Set(
    permissions
  )];

Payload JWT

Modifier :

const payload = {
 
  sub: user.id,
 
  tenantId: user.tenantId,
 
  email: user.email,
 
  sessionId: createdSession.id,
 
  roles,
 
  permissions:
    uniquePermissions
};

Étape 6 — Enrichir Register

Ajouter

Lors du :

register()

Payload

roles: [
 
  SYSTEM_ROLES.CUSTOMER
]
 
permissions: []

Étape 7 — Création RolesGuard

Créer

roles.guard.ts

Injection

constructor(
 
  private reflector:
    Reflector
) {}

Implémentation

@Injectable()
export class RolesGuard
implements CanActivate {
 
  constructor(
    private reflector:
      Reflector
  ) {}
 
  canActivate(
    context:
      ExecutionContext
  ): boolean {
 
    const requiredRoles =
 
      this.reflector.getAllAndOverride<
        string[]
      >(
        ROLES_KEY,
        [
          context.getHandler(),
          context.getClass()
        ]
      );
 
    if (
      !requiredRoles ||
      requiredRoles.length === 0
    ) {
 
      return true;
    }
 
    const request =
      context
        .switchToHttp()
        .getRequest();
 
    const user =
      request.user;
 
    return requiredRoles.some(
 
      role =>
 
        user.roles?.includes(
          role
        )
    );
  }
}

Étape 8 — Création PermissionsGuard

Créer

permissions.guard.ts

Implémentation

@Injectable()
export class PermissionsGuard
implements CanActivate {
 
  constructor(
    private reflector:
      Reflector
  ) {}
 
  canActivate(
    context:
      ExecutionContext
  ): boolean {
 
    const requiredPermissions =
 
      this.reflector.getAllAndOverride<
        string[]
      >(
        PERMISSIONS_KEY,
        [
          context.getHandler(),
          context.getClass()
        ]
      );
 
    if (
      !requiredPermissions ||
      requiredPermissions.length === 0
    ) {
 
      return true;
    }
 
    const request =
      context
        .switchToHttp()
        .getRequest();
 
    const user =
      request.user;
 
    return requiredPermissions.every(
 
      permission =>
 
        user.permissions?.includes(
          permission
        )
    );
  }
}

Étape 9 — Mise à jour JwtStrategy

Validation

Remplacer :

return payload;

Par

return {
 
  id: payload.sub,
 
  tenantId:
    payload.tenantId,
 
  email:
    payload.email,
 
  sessionId:
    payload.sessionId,
 
  roles:
    payload.roles ?? [],
 
  permissions:
    payload.permissions ?? []
};

Étape 10 — Export Guards

AuthModule

Ajouter :

providers: [
 
  AuthService,
 
  PasswordService,
 
  TokenService,
 
  JwtStrategy,
 
  RolesGuard,
 
  PermissionsGuard
]

Exports

exports: [
 
  RolesGuard,
 
  PermissionsGuard
]

Étape 11 — Utilisation Roles

Exemple

@Get()
 
@UseGuards(
 
  JwtAuthGuard,
 
  RolesGuard
)
 
@Roles(
  'SUPER_ADMIN'
)
 
findAllUsers() {}

Étape 12 — Utilisation Permissions

Exemple

@Get()
 
@UseGuards(
 
  JwtAuthGuard,
 
  PermissionsGuard
)
 
@Permissions(
  'users.read'
)
 
findAllUsers() {}

Étape 13 — Combinaison RBAC

Exemple

@Get()
 
@UseGuards(
 
  JwtAuthGuard,
 
  RolesGuard,
 
  PermissionsGuard
)
 
@Roles(
  'ADMIN',
  'SUPER_ADMIN'
)
 
@Permissions(
  'users.read'
)
 
findAllUsers() {}

Étape 14 — Création d'un endpoint de test

AuthController

Ajouter :

@Get('admin')
 
@UseGuards(
 
  JwtAuthGuard,
 
  RolesGuard
)
 
@Roles(
  'SUPER_ADMIN'
)
 
adminOnly() {
 
  return {
 
    success: true
  };
}

Étape 15 — Endpoint permissions

Ajouter

@Get('permissions')
 
@UseGuards(
 
  JwtAuthGuard,
 
  PermissionsGuard
)
 
@Permissions(
  'users.read'
)
 
permissionsOnly() {
 
  return {
 
    success: true
  };
}

Étape 16 — Tests manuels

SUPER_ADMIN

Doit accéder :

GET /auth/admin

CUSTOMER

Doit recevoir :

{
  "statusCode": 403,
  "message": "Forbidden resource"
}

Étape 17 — Gestion du Super Admin

Bypass recommandé

Dans :

PermissionsGuard

ajouter :

if (
 
  user.roles?.includes(
    'SUPER_ADMIN'
  )
) {
 
  return true;
}

Résultat

Le :

SUPER_ADMIN

contourne toutes les permissions.


Étape 18 — Préparation Sprint 2

Le RBAC est maintenant réutilisable sur :

Users

Properties

Reservations

CRM

Finance

Marketing

Security

Administration

Définition de terminé

Le Sprint 1-C.1 est terminé lorsque :

✓ @Roles créé

✓ @Permissions créé

✓ RolesGuard créé

✓ PermissionsGuard créé

✓ JWT enrichi

✓ Contrôle d'accès fonctionnel

✓ Tests manuels réussis

✓ Swagger toujours opérationnel

Livrables

RolesDecorator

PermissionsDecorator

RolesGuard

PermissionsGuard

JWT enrichi

RBAC Enterprise

Sprint 1-C.2 — Tests Unitaires Auth & RBAC

Objectif

Sécuriser l'ensemble du module Auth avant de poursuivre les développements métier.

À l'issue de cette étape :

✓ PasswordService testé

✓ TokenService testé

✓ AuthService testé

✓ JwtStrategy testée

✓ RolesGuard testé

✓ PermissionsGuard testé

✓ Couverture > 80%

✓ CI compatible

Architecture des tests

Répertoire

Créer :

apps/api/src/modules/auth

├── domain
│
│   └── services
│
│       ├── auth.service.spec.ts
│       ├── password.service.spec.ts
│       └── token.service.spec.ts
│
├── infrastructure
│
│   └── strategies
│
│       └── jwt.strategy.spec.ts
│
└── presentation
    │
    └── guards

        ├── roles.guard.spec.ts
        └── permissions.guard.spec.ts

Étape 1 — Configuration Jest

Vérifier

npm install -D
 
jest
 
ts-jest
 
@types/jest

Vérifier

apps/api/jest.config.ts

Coverage

coverageThreshold: {
 
  global: {
 
    branches: 80,
 
    functions: 80,
 
    lines: 80,
 
    statements: 80
  }
}

Étape 2 — PasswordService

Fichier

password.service.spec.ts

Test hash

describe(
  'PasswordService',
  () => {
 
    it(
      'should hash password',
      async () => {
 
        const hash =
 
          await service.hash(
            'Password123!'
          );
 
        expect(hash)
          .toBeDefined();
 
        expect(hash)
          .not
          .toEqual(
            'Password123!'
          );
      }
    );
  }
);

Test compare

it(
  'should compare password',
  async () => {
 
    const hash =
 
      await service.hash(
        'Password123!'
      );
 
    const result =
 
      await service.compare(
 
        'Password123!',
 
        hash
      );
 
    expect(result)
      .toBe(true);
  }
);

Étape 3 — TokenService

Fichier

token.service.spec.ts

Test Access Token

it(
  'should generate access token',
  async () => {
 
    const token =
 
      await service
        .generateAccessToken({
 
          sub: '1',
 
          tenantId: '1',
 
          email:
            'test@test.com'
        });
 
    expect(token)
      .toBeDefined();
  }
);

Test Refresh Token

it(
  'should generate refresh token',
  async () => {
 
    const token =
 
      await service
        .generateRefreshToken();
 
    expect(token)
      .toBeDefined();
  }
);

Étape 4 — Mock Prisma

Créer

test/mocks/prisma.mock.ts

Contenu

export const prismaMock = {
 
  user: {
 
    findUnique: jest.fn(),
 
    create: jest.fn(),
 
    update: jest.fn()
  },
 
  role: {
 
    findFirst: jest.fn()
  },
 
  userRole: {
 
    create: jest.fn()
  },
 
  refreshToken: {
 
    create: jest.fn(),
 
    findMany: jest.fn(),
 
    update: jest.fn(),
 
    updateMany: jest.fn()
  },
 
  session: {
 
    create: jest.fn(),
 
    updateMany: jest.fn()
  },
 
  tenant: {
 
    findUnique: jest.fn()
  }
};

Étape 5 — AuthService

Fichier

auth.service.spec.ts

Setup

beforeEach(
  async () => {
 
    const module =
 
      await Test
        .createTestingModule({
 
          providers: [
 
            AuthService,
 
            {
              provide:
                PrismaService,
 
              useValue:
                prismaMock
            }
          ]
        })
        .compile();
  }
);

Étape 6 — Test Register

Cas nominal

it(
  'should register user',
  async () => {
 
    prismaMock.user
      .findUnique
      .mockResolvedValue(
        null
      );
 
    const result =
 
      await service.register({
 
        email:
          'john@test.com',
 
        password:
          'Password123!',
 
        firstName:
          'John',
 
        lastName:
          'Doe'
      });
 
    expect(result)
      .toBeDefined();
  }
);

Étape 7 — Email déjà utilisé

Test

it(
  'should throw conflict',
  async () => {
 
    prismaMock.user
      .findUnique
      .mockResolvedValue({});
 
    await expect(
 
      service.register(
        dto
      )
 
    ).rejects.toThrow(
      ConflictException
    );
  }
);

Étape 8 — Test Login

Succès

it(
  'should login user',
  async () => {
 
    const result =
 
      await service.login(
        dto,
        session
      );
 
    expect(result)
      .toBeDefined();
  }
);

Mot de passe invalide

it(
  'should reject login',
  async () => {
 
    await expect(
 
      service.login(
        dto,
        session
      )
 
    ).rejects.toThrow(
      UnauthorizedException
    );
  }
);

Étape 9 — Test Refresh

Succès

it(
  'should refresh token',
  async () => {
 
    const result =
 
      await service.refresh({
 
        refreshToken:
          'token'
      });
 
    expect(result)
      .toBeDefined();
  }
);

Échec

it(
  'should reject invalid token',
  async () => {
 
    await expect(
 
      service.refresh({
 
        refreshToken:
          'bad-token'
      })
 
    ).rejects.toThrow(
      UnauthorizedException
    );
  }
);

Étape 10 — Test Logout

Succès

it(
  'should logout user',
  async () => {
 
    const result =
 
      await service.logout(
        'user-id'
      );
 
    expect(
      result.success
    ).toBe(true);
  }
);

Étape 11 — Test Current User

Succès

it(
  'should return current user',
  async () => {
 
    const result =
 
      await service.me(
        'user-id'
      );
 
    expect(result)
      .toBeDefined();
 
    expect(
      result.email
    ).toEqual(
      'admin@platform.local'
    );
  }
);

Étape 12 — JwtStrategy

Fichier

jwt.strategy.spec.ts

Test

it(
  'should validate payload',
  async () => {
 
    const result =
 
      await strategy.validate({
 
        sub: '1',
 
        email:
          'test@test.com',
 
        tenantId:
          'tenant'
      });
 
    expect(
      result.id
    ).toEqual('1');
  }
);

Étape 13 — RolesGuard

Fichier

roles.guard.spec.ts

Autorisé

it(
  'should allow role',
  () => {
 
    expect(
      guard.canActivate(
        context
      )
    ).toBe(true);
  }
);

Refusé

it(
  'should reject role',
  () => {
 
    expect(
      guard.canActivate(
        context
      )
    ).toBe(false);
  }
);

Étape 14 — PermissionsGuard

Fichier

permissions.guard.spec.ts

Autorisé

it(
  'should allow permission',
  () => {
 
    expect(
      guard.canActivate(
        context
      )
    ).toBe(true);
  }
);

Refusé

it(
  'should reject permission',
  () => {
 
    expect(
      guard.canActivate(
        context
      )
    ).toBe(false);
  }
);

Étape 15 — Exécution

Lancer

nx test api

Avec coverage

nx test api \
--coverage

Étape 16 — Rapport

Généré dans

coverage/apps/api

Ouvrir

coverage/apps/api/lcov-report/index.html

Étape 17 — Objectifs

PasswordService

100 %

TokenService

100 %

RolesGuard

100 %

PermissionsGuard

100 %

AuthService

> 80 %

Étape 18 — Ajout CI

Workflow

.github/workflows/ci.yml

Ajouter

- name: Run Tests

  run: nx test api

Coverage

- name: Coverage

  run: nx test api --coverage

Étape 19 — Badge Coverage

README

![Coverage]

Définition de terminé

Le Sprint 1-C.2 est terminé lorsque :

✓ PasswordService testé

✓ TokenService testé

✓ AuthService testé

✓ JwtStrategy testée

✓ RolesGuard testé

✓ PermissionsGuard testé

✓ Coverage > 80 %

✓ CI verte

Livrables

auth.service.spec.ts

password.service.spec.ts

token.service.spec.ts

jwt.strategy.spec.ts

roles.guard.spec.ts

permissions.guard.spec.ts

coverage report

Sprint 1-C.3 — Tests E2E Authentification

Objectif

Valider le fonctionnement réel de toute la chaîne d'authentification.

À l'issue de cette étape :

✓ Register testé

✓ Login testé

✓ Current User testé

✓ Refresh testé

✓ Logout testé

✓ RBAC testé

✓ PostgreSQL réel

✓ JWT réel

✓ Prisma réel

Ces tests constituent la première validation métier complète de la plateforme.


Architecture E2E

Environnement

NestJS

↓

Prisma

↓

PostgreSQL

↓

JWT

↓

API réelle

Pas de Mock

Contrairement aux tests unitaires :

Aucun Mock

Aucun Fake Service

Base PostgreSQL réelle

Étape 1 — Création du projet E2E

Vérifier

apps/api-e2e

Structure

apps

├── api

└── api-e2e

    ├── src

    │   └── auth

    │       └── auth.e2e-spec.ts

    │
    ├── jest.config.ts

    └── project.json

Étape 2 — Installation

Vérifier

npm install -D supertest
 
npm install -D @types/supertest

Étape 3 — Base de test

Créer

.env.test

Contenu

DATABASE_URL=

postgresql://postgres:
postgres@localhost:5432/
rental_platform_test

JWT_SECRET=test-secret

JWT_EXPIRES_IN=15m

REFRESH_SECRET=test-refresh

REFRESH_EXPIRES_IN=30d

Étape 4 — Création DB Test

PostgreSQL

CREATE DATABASE
rental_platform_test;

Étape 5 — Migration

Exécuter

DATABASE_URL=...
 
npx prisma migrate deploy

Seed

DATABASE_URL=...
 
npx prisma db seed

Étape 6 — Bootstrap E2E

Créer

apps/api-e2e/src/auth/auth.e2e-spec.ts

Base

describe(
  'Auth E2E',
  () => {
 
    let app:
      INestApplication;
 
    beforeAll(
      async () => {
 
        const moduleRef =
 
          await Test
            .createTestingModule({
 
              imports: [
 
                AppModule
              ]
            })
            .compile();
 
        app =
          moduleRef
            .createNestApplication();
 
        await app.init();
      }
    );
 
    afterAll(
      async () => {
 
        await app.close();
      }
    );
  }
);

Étape 7 — Variables de scénario

Ajouter

let accessToken:
  string;
 
let refreshToken:
  string;
 
let userId:
  string;

Étape 8 — Test Register

Cas nominal

it(
  'should register user',
  async () => {
 
    const response =
 
      await request(
 
        app.getHttpServer()
      )
 
      .post(
        '/auth/register'
      )
 
      .send({
 
        email:
          'john@test.com',
 
        password:
          'Password123!',
 
        firstName:
          'John',
 
        lastName:
          'Doe'
      })
 
      .expect(201);
 
    expect(
 
      response.body
        .accessToken
 
    ).toBeDefined();
 
    userId =
 
      response.body
        .user.id;
  }
);

Étape 9 — Test Login

Cas nominal

it(
  'should login',
  async () => {
 
    const response =
 
      await request(
 
        app.getHttpServer()
      )
 
      .post(
        '/auth/login'
      )
 
      .send({
 
        email:
          'john@test.com',
 
        password:
          'Password123!'
      })
 
      .expect(201);
 
    accessToken =
 
      response.body
        .accessToken;
 
    refreshToken =
 
      response.body
        .refreshToken;
 
    expect(
      accessToken
    ).toBeDefined();
 
    expect(
      refreshToken
    ).toBeDefined();
  }
);

Étape 10 — Test Current User

Endpoint

GET /auth/me

Test

it(
  'should return current user',
  async () => {
 
    const response =
 
      await request(
 
        app.getHttpServer()
      )
 
      .get(
        '/auth/me'
      )
 
      .set(
 
        'Authorization',
 
        `Bearer ${accessToken}`
      )
 
      .expect(200);
 
    expect(
 
      response.body.email
 
    ).toEqual(
      'john@test.com'
    );
  }
);

Étape 11 — Test RBAC

Endpoint

GET /auth/admin

CUSTOMER

it(
  'should reject customer',
  async () => {
 
    await request(
 
      app.getHttpServer()
    )
 
    .get(
      '/auth/admin'
    )
 
    .set(
 
      'Authorization',
 
      `Bearer ${accessToken}`
    )
 
    .expect(403);
  }
);

Étape 12 — Test Refresh

Endpoint

POST /auth/refresh

Test

it(
  'should refresh token',
  async () => {
 
    const response =
 
      await request(
 
        app.getHttpServer()
      )
 
      .post(
        '/auth/refresh'
      )
 
      .send({
 
        refreshToken
      })
 
      .expect(201);
 
    accessToken =
 
      response.body
        .accessToken;
 
    refreshToken =
 
      response.body
        .refreshToken;
 
    expect(
      accessToken
    ).toBeDefined();
  }
);

Étape 13 — Test Logout

Endpoint

POST /auth/logout

Test

it(
  'should logout',
  async () => {
 
    await request(
 
      app.getHttpServer()
    )
 
    .post(
      '/auth/logout'
    )
 
    .set(
 
      'Authorization',
 
      `Bearer ${accessToken}`
    )
 
    .expect(201);
  }
);

Étape 14 — Vérification sécurité

Ancien Refresh Token

Ne doit plus fonctionner.


Test

it(
  'should reject old refresh token',
  async () => {
 
    await request(
 
      app.getHttpServer()
    )
 
    .post(
      '/auth/refresh'
    )
 
    .send({
 
      refreshToken
    })
 
    .expect(401);
  }
);

Étape 15 — Cas erreur Login

Mauvais mot de passe

it(
  'should reject invalid password',
  async () => {
 
    await request(
 
      app.getHttpServer()
    )
 
    .post(
      '/auth/login'
    )
 
    .send({
 
      email:
        'john@test.com',
 
      password:
        'BAD_PASSWORD'
    })
 
    .expect(401);
  }
);

Étape 16 — Cas erreur Register

Email existant

it(
  'should reject duplicate email',
  async () => {
 
    await request(
 
      app.getHttpServer()
    )
 
    .post(
      '/auth/register'
    )
 
    .send({
 
      email:
        'john@test.com',
 
      password:
        'Password123!',
 
      firstName:
        'John',
 
      lastName:
        'Doe'
    })
 
    .expect(409);
  }
);

Étape 17 — Nettoyage

Supprimer utilisateur

Dans :

afterAll()

Exemple

await prisma.user.deleteMany({
 
  where: {
 
    email:
      'john@test.com'
  }
});

Étape 18 — Configuration NX

project.json

Ajouter :

{
  "targets": {
 
    "e2e": {
 
      "executor":
 
      "@nx/jest:jest"
    }
  }
}

Étape 19 — Exécution

Lancer

nx e2e api-e2e

Résultat attendu

PASS

Register

PASS

Login

PASS

Current User

PASS

Refresh

PASS

Logout

PASS

RBAC

Étape 20 — Intégration CI

Workflow

.github/workflows/ci.yml

Ajouter

- name: E2E Tests

  run:

    nx e2e api-e2e

Étape 21 — Pipeline qualité

Ordre

Lint

↓

Build

↓

Unit Tests

↓

E2E Tests

↓

Docker Build

Définition de terminé

Le Sprint 1-C.3 est terminé lorsque :

✓ Register testé

✓ Login testé

✓ Current User testé

✓ Refresh testé

✓ Logout testé

✓ RBAC testé

✓ PostgreSQL réel

✓ JWT réel

✓ CI verte

Livrables

auth.e2e-spec.ts

.env.test

Configuration E2E

Pipeline CI

Bilan Sprint 1

Le Sprint 1 est considéré terminé lorsque :

✓ AuthModule opérationnel

✓ Register opérationnel

✓ Login opérationnel

✓ Refresh opérationnel

✓ Logout opérationnel

✓ Current User opérationnel

✓ RBAC opérationnel

✓ Tests unitaires verts

✓ Tests E2E verts

✓ Swagger documenté

✓ CI verte

Étape suivante

Sprint 2 — Gestion des Utilisateurs

Objectif :

Construire le premier domaine métier protégé par le RBAC :

User

Profile

Address

Preferences

NotificationSettings

UserSession

ConnectionHistory

avec :

CRUD Utilisateurs

Gestion Profil

Gestion Préférences

Historique Connexions

Administration Utilisateurs
ujusum/3-codage/2-sprints/1-sprint-1.txt · Dernière modification : 2026/06/08 02:01 de admin

DokuWiki Appliance - Powered by TurnKey Linux