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
AuthModule UsersModule SecurityModule
User Role Permission UserRole RolePermission RefreshToken Session
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[]
}
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]
)
}
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]
)
}
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
npm install -D @types/bcrypt @types/passport-jwt @types/passport-local
src/modules/auth ├── application ├── domain ├── infrastructure ├── presentation └── auth.module.ts
commands queries handlers dto
export class RegisterDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsString()
firstName: string;
@IsString()
lastName: string;
}
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
export class RefreshTokenDto {
@IsString()
refreshToken: string;
}
src/modules/auth/domain/services
hash() compare()
const hash = await bcrypt.hash(password, 12);
JWT_SECRET JWT_EXPIRES_IN REFRESH_SECRET REFRESH_EXPIRES_IN
JWT_SECRET=super-secret JWT_EXPIRES_IN=15m REFRESH_SECRET=refresh-secret REFRESH_EXPIRES_IN=30d
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
}
POST /auth/register
Validation ↓ Email unique ↓ Hash password ↓ Create User ↓ Assign Role USER ↓ Return JWT
{
"accessToken": "...",
"refreshToken": "...",
"user": {}
}
POST /auth/login
Find User ↓ Compare Password ↓ Generate JWT ↓ Generate Refresh Token ↓ Create Session
POST /auth/refresh
Validate Refresh Token ↓ Generate New JWT ↓ Rotate Refresh Token
POST /auth/logout
Révocation Refresh Token Suppression Session
GET /auth/me
{
"id": "...",
"email": "...",
"roles": ["USER"]
}
jwt.strategy.ts
Bearer Token ↓ Verify JWT ↓ Load User ↓ Inject Request.user
@Roles('ADMIN')
RolesGuard
@Roles('ADMIN')
@Get()
findAll()
@Permissions( 'users.read' )
PermissionsGuard
RolePermission Permission
@ApiTags('Auth')
@ApiBearerAuth()
POST /auth/register POST /auth/login POST /auth/refresh POST /auth/logout GET /auth/me
users.read users.create users.update users.delete
SUPER_ADMIN ADMIN MANAGER OWNER CUSTOMER
admin@platform.local
AuthService PasswordService JwtService RolesGuard PermissionsGuard
nx test api
Register ↓ Login ↓ Access Protected Route ↓ Refresh Token ↓ Logout
nx e2e api-e2e
Utilisateur créé Mot de passe hashé JWT retourné
JWT valide Refresh Token valide
ADMIN autorisé USER refusé
100 % documenté
AuthModule JwtStrategy AuthController AuthService RolesGuard PermissionsGuard RegisterDto LoginDto RefreshTokenDto Swagger Tests
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
AuthModule UsersModule SecurityModule
User Role Permission UserRole RolePermission RefreshToken Session
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[]
}
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]
)
}
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]
)
}
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
npm install -D @types/bcrypt @types/passport-jwt @types/passport-local
src/modules/auth ├── application ├── domain ├── infrastructure ├── presentation └── auth.module.ts
commands queries handlers dto
export class RegisterDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsString()
firstName: string;
@IsString()
lastName: string;
}
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
export class RefreshTokenDto {
@IsString()
refreshToken: string;
}
src/modules/auth/domain/services
hash() compare()
const hash = await bcrypt.hash(password, 12);
JWT_SECRET JWT_EXPIRES_IN REFRESH_SECRET REFRESH_EXPIRES_IN
JWT_SECRET=super-secret JWT_EXPIRES_IN=15m REFRESH_SECRET=refresh-secret REFRESH_EXPIRES_IN=30d
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
}
POST /auth/register
Validation ↓ Email unique ↓ Hash password ↓ Create User ↓ Assign Role USER ↓ Return JWT
{
"accessToken": "...",
"refreshToken": "...",
"user": {}
}
POST /auth/login
Find User ↓ Compare Password ↓ Generate JWT ↓ Generate Refresh Token ↓ Create Session
POST /auth/refresh
Validate Refresh Token ↓ Generate New JWT ↓ Rotate Refresh Token
POST /auth/logout
Révocation Refresh Token Suppression Session
GET /auth/me
{
"id": "...",
"email": "...",
"roles": ["USER"]
}
jwt.strategy.ts
Bearer Token ↓ Verify JWT ↓ Load User ↓ Inject Request.user
@Roles('ADMIN')
RolesGuard
@Roles('ADMIN')
@Get()
findAll()
@Permissions( 'users.read' )
PermissionsGuard
RolePermission Permission
@ApiTags('Auth')
@ApiBearerAuth()
POST /auth/register POST /auth/login POST /auth/refresh POST /auth/logout GET /auth/me
users.read users.create users.update users.delete
SUPER_ADMIN ADMIN MANAGER OWNER CUSTOMER
admin@platform.local
AuthService PasswordService JwtService RolesGuard PermissionsGuard
nx test api
Register ↓ Login ↓ Access Protected Route ↓ Refresh Token ↓ Logout
nx e2e api-e2e
Utilisateur créé Mot de passe hashé JWT retourné
JWT valide Refresh Token valide
ADMIN autorisé USER refusé
100 % documenté
AuthModule JwtStrategy AuthController AuthService RolesGuard PermissionsGuard RegisterDto LoginDto RefreshTokenDto Swagger Tests
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
enum UserStatus {
ACTIVE
INACTIVE
BLOCKED
PENDING
}
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])
}
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])
}
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])
}
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])
}
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])
}
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])
}
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])
}
Le modèle :
Tenant
doit contenir :
users User[] roles Role[]
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
}
cd apps/api npx prisma validate
The schema at prisma/schema.prisma is valid
npx prisma format
npx prisma migrate dev \
--name auth_module
prisma/migrations
└── xxxx_auth_module
└── migration.sql
npx prisma generate
docker exec -it postgres psql -U postgres
\c rental_platform
\d "User" \d "Role" \d "Permission" \d "UserRole" \d "RolePermission" \d "RefreshToken" \d "Session"
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
schema.prisma migration auth_module Prisma Client
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
apps/api/prisma/seed.ts
Créer Tenant Créer Permissions Créer Rôles Créer Super Admin Associer les permissions Associer les rôles
npm install bcrypt npm install -D @types/bcrypt
{
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
npm install -D tsx
apps/api/prisma/seed.ts
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);
});
const tenant =
await prisma.tenant.upsert({
where: {
code: 'MAIN'
},
update: {},
create: {
code: 'MAIN',
name: 'Main Tenant',
active: true
}
});
MAIN
devient le tenant par défaut.
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
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' ];
for (const code of permissions) {
await prisma.permission.upsert({
where: { code },
update: {},
create: {
code,
name: code
}
});
}
SUPER_ADMIN ADMIN MANAGER OWNER CUSTOMER
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'
}
});
ADMIN MANAGER OWNER CUSTOMER
const allPermissions = await prisma.permission.findMany();
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
}
});
}
SUPER_ADMIN ↓ Toutes les permissions
admin@platform.local Admin123!
const passwordHash =
await bcrypt.hash(
'Admin123!',
12
);
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'
}
});
await prisma.userRole.upsert({
where: {
userId_roleId: {
userId: admin.id,
roleId: superAdminRole.id
}
},
update: {},
create: {
userId: admin.id,
roleId: superAdminRole.id
}
});
console.log( 'Tenant created:', tenant.code ); console.log( 'Admin:', admin.email ); console.log( 'Seed completed' );
cd apps/api npx prisma db seed
Starting seed... Tenant created: MAIN Admin: admin@platform.local Seed completed
npx prisma studio
Tenant User Role Permission UserRole RolePermission
SELECT email FROM "User";
admin@platform.local
SELECT COUNT(*) FROM "Permission";
> 15
Admin123!
en production.
ADMIN_EMAIL ADMIN_PASSWORD
dans :
.env
pour les environnements réels.
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
prisma/seed.ts package.json Permissions système Rôles système Compte Super Admin
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
Créer :
apps/api/src/modules/auth
auth ├── application │ │ ├── dto │ ├── commands │ ├── queries │ └── handlers │ ├── domain │ │ └── services │ ├── infrastructure │ │ └── strategies │ ├── presentation │ │ ├── controllers │ └── guards │ └── auth.module.ts
auth.module.ts
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 {}
application/dto/register.dto.ts
import {
IsEmail,
IsString,
MinLength
} from 'class-validator';
export class RegisterDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsString()
firstName: string;
@IsString()
lastName: string;
}
application/dto/login.dto.ts
import {
IsEmail,
IsString
} from 'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
application/dto/refresh-token.dto.ts
import {
IsString
} from 'class-validator';
export class RefreshTokenDto {
@IsString()
refreshToken: string;
}
domain/services/password.service.ts
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
);
}
}
domain/services/auth.service.ts
Register Login Refresh Logout Current User
@Injectable()
export class AuthService {
async register() {}
async login() {}
async refresh() {}
async logout() {}
async me() {}
}
infrastructure/strategies/jwt-payload.interface.ts
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
}
infrastructure/strategies/jwt.strategy.ts
@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;
}
}
presentation/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard
extends AuthGuard('jwt') {}
presentation/controllers/auth.controller.ts
@ApiTags('Auth')
@Controller('auth')
export class AuthController {
}
POST /auth/register
@Post('register')
register(
@Body()
dto: RegisterDto
) {
return this.authService
.register(dto);
}
POST /auth/login
@Post('login')
login(
@Body()
dto: LoginDto
) {
return this.authService
.login(dto);
}
POST /auth/refresh
@Post('refresh')
refresh(
@Body()
dto: RefreshTokenDto
) {
return this.authService
.refresh(dto);
}
POST /auth/logout
@UseGuards( JwtAuthGuard )
@Post('logout')
logout(
@Req() req
) {
return this.authService
.logout(
req.user.sub
);
}
GET /auth/me
@UseGuards( JwtAuthGuard )
@Get('me')
me(
@Req() req
) {
return this.authService
.me(
req.user.sub
);
}
JWT_SECRET=change-me JWT_EXPIRES_IN=15m REFRESH_SECRET=change-me-too REFRESH_EXPIRES_IN=30d
Ajouter :
imports: [ AuthModule ]
nx build api
SUCCESS
/auth/register /auth/login /auth/refresh /auth/logout /auth/me
doivent apparaître dans :
http://localhost:3000/api
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
AuthModule AuthController AuthService PasswordService JwtStrategy JwtAuthGuard RegisterDto LoginDto RefreshTokenDto
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é
Register Request ↓ Validation DTO ↓ Vérification email ↓ Hash Password ↓ Create User ↓ Assign CUSTOMER Role ↓ Generate JWT ↓ Generate Refresh Token ↓ Persist Refresh Token ↓ Response
Créer :
application/dto/auth-response.dto.ts
export class AuthResponseDto {
accessToken: string;
refreshToken: string;
user: {
id: string;
email: string;
firstName: string;
lastName: string;
};
}
Le :
PrismaModule
doit exporter :
PrismaService
Ajouter :
imports: [
PrismaModule,
JwtModule.register({})
]
Modifier :
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly passwordService: PasswordService
) {}
}
Créer :
shared/constants/roles.constants.ts
export const SYSTEM_ROLES = {
SUPER_ADMIN: 'SUPER_ADMIN',
ADMIN: 'ADMIN',
MANAGER: 'MANAGER',
OWNER: 'OWNER',
CUSTOMER: 'CUSTOMER'
};
async register( dto: RegisterDto ): Promise<AuthResponseDto>
const existingUser =
await this.prisma.user.findUnique({
where: {
email: dto.email
}
});
if (existingUser) {
throw new ConflictException(
'Email already exists'
);
}
const tenant =
await this.prisma.tenant.findUnique({
where: {
code: 'MAIN'
}
});
if (!tenant) {
throw new Error(
'Main tenant not found'
);
}
const passwordHash =
await this.passwordService.hash(
dto.password
);
const user =
await this.prisma.user.create({
data: {
tenantId: tenant.id,
email: dto.email,
passwordHash,
firstName: dto.firstName,
lastName: dto.lastName,
status: 'ACTIVE'
}
});
const customerRole =
await this.prisma.role.findFirst({
where: {
tenantId: tenant.id,
code: SYSTEM_ROLES.CUSTOMER
}
});
if (!customerRole) {
throw new Error(
'Customer role missing'
);
}
await this.prisma.userRole.create({
data: {
userId: user.id,
roleId: customerRole.id
}
});
const payload = {
sub: user.id,
tenantId: tenant.id,
email: user.email
};
const accessToken =
await this.jwtService.signAsync(
payload,
{
secret:
process.env.JWT_SECRET,
expiresIn:
process.env.JWT_EXPIRES_IN
}
);
const refreshToken = randomUUID();
const refreshTokenHash =
await this.passwordService.hash(
refreshToken
);
await this.prisma.refreshToken.create({
data: {
userId: user.id,
tokenHash: refreshTokenHash,
expiresAt: addDays(
new Date(),
30
)
}
});
return {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
}
};
@Post('register')
@ApiOperation({
summary:
'Register a new user'
})
@ApiCreatedResponse({
type: AuthResponseDto
})
Vérifier :
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true
})
);
POST /auth/register
{
"email": "john@example.com",
"password": "Password123!",
"firstName": "John",
"lastName": "Doe"
}
{
"accessToken": "...",
"refreshToken": "...",
"user": {
"id": "...",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe"
}
}
SELECT email FROM "User";
SELECT * FROM "UserRole";
SELECT * FROM "RefreshToken";
{
"statusCode": 409,
"message":
"Email already exists"
}
{
"statusCode": 500,
"message":
"Main tenant not found"
}
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
AuthService.register() AuthResponseDto roles.constants.ts Swagger Register Validation complète
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é
Login Request ↓ Find User ↓ Verify Password ↓ Verify Status ↓ Create Session ↓ Generate JWT ↓ Generate Refresh Token ↓ Store Refresh Token ↓ Return Auth Response
application/dto/session-info.dto.ts
export class SessionInfoDto {
ipAddress?: string;
userAgent?: string;
}
Modifier :
@Post('login')
login(
@Body()
dto: LoginDto,
@Req()
request: Request
) {
return this.authService.login(
dto,
{
ipAddress: request.ip,
userAgent:
request.headers['user-agent']
}
);
}
Modifier :
async login( dto: LoginDto, session: SessionInfoDto ): Promise<AuthResponseDto>
const user =
await this.prisma.user.findUnique({
where: {
email: dto.email
},
include: {
userRoles: {
include: {
role: true
}
}
}
});
if (!user) {
throw new UnauthorizedException(
'Invalid credentials'
);
}
if (
user.status !== 'ACTIVE'
) {
throw new ForbiddenException(
'Account disabled'
);
}
const validPassword =
await this.passwordService.compare(
dto.password,
user.passwordHash
);
if (!validPassword) {
throw new UnauthorizedException(
'Invalid credentials'
);
}
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email
};
const accessToken =
await this.jwtService.signAsync(
payload,
{
secret:
process.env.JWT_SECRET,
expiresIn:
process.env.JWT_EXPIRES_IN
}
);
const refreshToken = randomUUID();
const refreshTokenHash =
await this.passwordService.hash(
refreshToken
);
await this.prisma.refreshToken.create({
data: {
userId: user.id,
tokenHash: refreshTokenHash,
expiresAt: addDays(
new Date(),
30
)
}
});
await this.prisma.session.create({
data: {
userId: user.id,
ipAddress:
session.ipAddress,
userAgent:
session.userAgent,
lastSeenAt:
new Date()
}
});
await this.prisma.user.update({
where: {
id: user.id
},
data: {
lastLoginAt:
new Date()
}
});
const roles =
user.userRoles.map(
item => item.role.code
);
return {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
roles
}
};
Ajouter :
@ApiOperation({
summary:
'Authenticate user'
})
@ApiOkResponse({
type: AuthResponseDto
})
{
"statusCode": 401,
"message":
"Invalid credentials"
}
{
"statusCode": 401,
"message":
"Invalid credentials"
}
{
"statusCode": 403,
"message":
"Account disabled"
}
POST /auth/login
{
"email": "admin@platform.local",
"password": "Admin123!"
}
{
"accessToken": "...",
"refreshToken": "...",
"user": {
"id": "...",
"email": "admin@platform.local",
"firstName": "Super",
"lastName": "Admin",
"roles": [
"SUPER_ADMIN"
]
}
}
SELECT * FROM "Session";
SELECT * FROM "RefreshToken";
SELECT email, "lastLoginAt" FROM "User";
À partir de maintenant, factoriser la génération des tokens.
Créer :
domain/services/token.service.ts
generateAccessToken() generateRefreshToken() verifyRefreshToken()
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
AuthService.login() SessionInfoDto Swagger Login Création Session Historisation connexion
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
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
src/modules/auth/domain/services/token.service.ts
generateAccessToken() generateRefreshToken() hashRefreshToken() verifyRefreshToken()
@Injectable()
export class TokenService {
constructor(
private readonly jwtService: JwtService,
private readonly passwordService: PasswordService
) {}
}
async generateAccessToken(
payload: JwtPayload
): Promise<string> {
return this.jwtService.signAsync(
payload,
{
secret:
process.env.JWT_SECRET,
expiresIn:
process.env.JWT_EXPIRES_IN
}
);
}
async generateRefreshToken() {
const rawToken =
randomUUID();
return rawToken;
}
async hashRefreshToken(
token: string
) {
return this.passwordService.hash(
token
);
}
async storeRefreshToken(
userId: string,
token: string
) {
const tokenHash =
await this.hashRefreshToken(
token
);
return prisma.refreshToken.create({
data: {
userId,
tokenHash,
expiresAt: addDays(
new Date(),
30
)
}
});
}
providers: [ AuthService, PasswordService, TokenService, JwtStrategy ]
constructor(
private readonly prisma:
PrismaService,
private readonly tokenService:
TokenService,
private readonly passwordService:
PasswordService
) {}
async refresh( dto: RefreshTokenDto ): Promise<AuthResponseDto>
const refreshTokens =
await this.prisma.refreshToken.findMany({
where: {
revokedAt: null,
expiresAt: {
gt: new Date()
}
},
include: {
user: {
include: {
userRoles: {
include: {
role: true
}
}
}
}
}
});
let matchedToken = null;
for (
const refreshToken
of refreshTokens
) {
const valid =
await this.passwordService.compare(
dto.refreshToken,
refreshToken.tokenHash
);
if (valid) {
matchedToken =
refreshToken;
break;
}
}
if (!matchedToken) {
throw new UnauthorizedException(
'Invalid refresh token'
);
}
const user = matchedToken.user;
if (
user.status !== 'ACTIVE'
) {
throw new UnauthorizedException(
'User inactive'
);
}
await this.prisma.refreshToken.update({
where: {
id: matchedToken.id
},
data: {
revokedAt:
new Date()
}
});
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email
};
const accessToken =
await this.tokenService
.generateAccessToken(
payload
);
const refreshToken =
await this.tokenService
.generateRefreshToken();
await this.tokenService
.storeRefreshToken(
user.id,
refreshToken
);
const roles =
user.userRoles.map(
role => role.role.code
);
return {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
firstName:
user.firstName,
lastName:
user.lastName,
roles
}
};
@Post('refresh')
@ApiOperation({
summary:
'Refresh access token'
})
@ApiOkResponse({
type: AuthResponseDto
})
{
"statusCode": 401,
"message":
"Invalid refresh token"
}
{
"statusCode": 401,
"message":
"User inactive"
}
POST /auth/refresh
{
"refreshToken":
"xxxxxxxx"
}
{
"accessToken": "...",
"refreshToken": "...",
"user": {
"id": "...",
"email": "...",
"roles": [
"CUSTOMER"
]
}
}
SELECT * FROM "RefreshToken" WHERE "revokedAt" IS NOT NULL;
SELECT * FROM "RefreshToken" WHERE "revokedAt" IS NULL;
La recherche par :
findMany()
puis comparaison BCrypt n'est pas scalable.
Ajouter :
tokenId String @unique
dans :
RefreshToken
et stocker :
tokenId.rawToken
afin de retrouver directement le token.
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
TokenService AuthService.refresh() Swagger Refresh Rotation Refresh Token
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é
Logout Request ↓ JWT Validation ↓ Load User ↓ Load Active Session ↓ Revoke Session ↓ Revoke Refresh Tokens ↓ Audit Log ↓ Success Response
application/dto/logout-response.dto.ts
export class LogoutResponseDto {
success: boolean;
message: string;
}
jwt-payload.interface.ts
export interface JwtPayload {
sub: string;
tenantId: string;
email: string;
sessionId?: string;
}
Pouvoir relier :
JWT ↓ Session
afin de révoquer précisément la session active.
Modifier :
const createdSession =
await this.prisma.session.create({
data: {
userId: user.id,
ipAddress: session.ipAddress,
userAgent: session.userAgent,
lastSeenAt: new Date()
}
});
Ajouter :
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email,
sessionId: createdSession.id
};
Chaque JWT référence désormais :
Session.id
Ajouter :
async logout( userId: string, sessionId?: string ): Promise<LogoutResponseDto>
if (sessionId) {
await this.prisma.session.updateMany({
where: {
id: sessionId,
userId
},
data: {
revokedAt:
new Date()
}
});
}
La session courante devient :
Révoquée
await this.prisma.refreshToken.updateMany({
where: {
userId,
revokedAt: null
},
data: {
revokedAt:
new Date()
}
});
Tous les refresh tokens actifs :
INVALIDES
Ajouter :
await this.prisma.user.update({
where: {
id: userId
},
data: {
updatedAt:
new Date()
}
});
return {
success: true,
message:
'Successfully logged out'
};
@Post('logout')
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
logout(
@Req() req
) {
return this.authService.logout(
req.user.sub,
req.user.sessionId
);
}
@ApiOperation({
summary:
'Logout current user'
})
@ApiOkResponse({
type:
LogoutResponseDto
})
Créer un futur endpoint :
POST /auth/logout-all
Révoquer :
Toutes les sessions Tous les refresh tokens
sur tous les appareils.
SELECT id, "revokedAt" FROM "Session";
revokedAt != null
SELECT id, "revokedAt" FROM "RefreshToken";
Tous les tokens actifs deviennent :
revokedAt != null
POST /auth/login
Récupérer :
accessToken
POST /auth/logout Authorization: Bearer xxx
{
"success": true,
"message": "Successfully logged out"
}
Réutiliser :
Refresh Token précédent
{
"statusCode": 401,
"message": "Invalid refresh token"
}
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
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
AuthService.logout() LogoutResponseDto Swagger Logout Révocation Session Révocation RefreshToken
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é
GET /auth/me ↓ JWT Validation ↓ Extract UserId ↓ Load User ↓ Load Roles ↓ Load Permissions ↓ Build Profile ↓ Return Response
application/dto/current-user.dto.ts
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;
};
}
Ajouter :
async me( userId: string ): Promise<CurrentUserDto>
const user =
await this.prisma.user.findUnique({
where: {
id: userId
},
include: {
tenant: true,
userRoles: {
include: {
role: {
include: {
rolePermissions: {
include: {
permission: true
}
}
}
}
}
}
}
});
if (!user) {
throw new NotFoundException(
'User not found'
);
}
const roles =
user.userRoles.map(
item => item.role.code
);
Exemple :
[ "SUPER_ADMIN", "OWNER" ]
const permissions =
user.userRoles.flatMap(
userRole =>
userRole.role
.rolePermissions
.map(
rp =>
rp.permission.code
)
);
const uniquePermissions =
[...new Set(
permissions
)];
users.read users.create properties.read reservations.read
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
}
};
@Get('me')
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
me(
@Req() req
) {
return this.authService.me(
req.user.sub
);
}
@ApiOperation({
summary:
'Get current authenticated user'
})
@ApiOkResponse({
type:
CurrentUserDto
})
{
"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"
}
}
Utilisateur supprimé Utilisateur bloqué Utilisateur inactif
if (
user.status !== 'ACTIVE'
) {
throw new ForbiddenException(
'Account disabled'
);
}
POST /auth/login
accessToken
GET /auth/me Authorization: Bearer xxx
Profil utilisateur complet.
SELECT * FROM "User" WHERE email = 'admin@platform.local';
SELECT * FROM "UserRole";
SELECT * FROM "RolePermission";
Ajouter :
Redis Cache
sur :
GET /auth/me
afin d'éviter :
Chargement complet des permissions à chaque requête.
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
CurrentUserDto AuthService.me() Swagger Current User Chargement rôles Chargement permissions
Les endpoints suivants sont maintenant opérationnels :
POST /auth/register POST /auth/login POST /auth/refresh POST /auth/logout GET /auth/me
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
Controller ↓ @Roles() @Permissions() ↓ JwtAuthGuard ↓ RolesGuard ↓ PermissionsGuard ↓ Access Granted
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
auth.constants.ts
export const ROLES_KEY = 'roles'; export const PERMISSIONS_KEY = 'permissions';
roles.decorator.ts
import { SetMetadata }
from '@nestjs/common';
import {
ROLES_KEY
}
from '../constants/auth.constants';
export const Roles = (
...roles: string[]
) => SetMetadata(
ROLES_KEY,
roles
);
permissions.decorator.ts
import { SetMetadata }
from '@nestjs/common';
import {
PERMISSIONS_KEY
}
from '../constants/auth.constants';
export const Permissions = (
...permissions: string[]
) => SetMetadata(
PERMISSIONS_KEY,
permissions
);
jwt-payload.interface.ts
roles?: string[]; permissions?: string[];
Éviter des requêtes SQL inutiles.
Le JWT transporte :
Roles Permissions
du compte connecté.
Dans :
AuthService.login()
const permissions =
user.userRoles.flatMap(
userRole =>
userRole.role
.rolePermissions
.map(
rp =>
rp.permission.code
)
);
const uniquePermissions =
[...new Set(
permissions
)];
Modifier :
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email,
sessionId: createdSession.id,
roles,
permissions:
uniquePermissions
};
Lors du :
register()
roles: [ SYSTEM_ROLES.CUSTOMER ] permissions: []
roles.guard.ts
constructor(
private reflector:
Reflector
) {}
@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
)
);
}
}
permissions.guard.ts
@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
)
);
}
}
Remplacer :
return payload;
return {
id: payload.sub,
tenantId:
payload.tenantId,
email:
payload.email,
sessionId:
payload.sessionId,
roles:
payload.roles ?? [],
permissions:
payload.permissions ?? []
};
Ajouter :
providers: [ AuthService, PasswordService, TokenService, JwtStrategy, RolesGuard, PermissionsGuard ]
exports: [ RolesGuard, PermissionsGuard ]
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
findAllUsers() {}
@Get()
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
findAllUsers() {}
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard,
PermissionsGuard
)
@Roles(
'ADMIN',
'SUPER_ADMIN'
)
@Permissions(
'users.read'
)
findAllUsers() {}
Ajouter :
@Get('admin')
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
adminOnly() {
return {
success: true
};
}
@Get('permissions')
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
permissionsOnly() {
return {
success: true
};
}
Doit accéder :
GET /auth/admin
Doit recevoir :
{
"statusCode": 403,
"message": "Forbidden resource"
}
Dans :
PermissionsGuard
ajouter :
if (
user.roles?.includes(
'SUPER_ADMIN'
)
) {
return true;
}
Le :
SUPER_ADMIN
contourne toutes les permissions.
Le RBAC est maintenant réutilisable sur :
Users Properties Reservations CRM Finance Marketing Security Administration
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
RolesDecorator PermissionsDecorator RolesGuard PermissionsGuard JWT enrichi RBAC Enterprise
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
Controller ↓ @Roles() @Permissions() ↓ JwtAuthGuard ↓ RolesGuard ↓ PermissionsGuard ↓ Access Granted
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
auth.constants.ts
export const ROLES_KEY = 'roles'; export const PERMISSIONS_KEY = 'permissions';
roles.decorator.ts
import { SetMetadata }
from '@nestjs/common';
import {
ROLES_KEY
}
from '../constants/auth.constants';
export const Roles = (
...roles: string[]
) => SetMetadata(
ROLES_KEY,
roles
);
permissions.decorator.ts
import { SetMetadata }
from '@nestjs/common';
import {
PERMISSIONS_KEY
}
from '../constants/auth.constants';
export const Permissions = (
...permissions: string[]
) => SetMetadata(
PERMISSIONS_KEY,
permissions
);
jwt-payload.interface.ts
roles?: string[]; permissions?: string[];
Éviter des requêtes SQL inutiles.
Le JWT transporte :
Roles Permissions
du compte connecté.
Dans :
AuthService.login()
const permissions =
user.userRoles.flatMap(
userRole =>
userRole.role
.rolePermissions
.map(
rp =>
rp.permission.code
)
);
const uniquePermissions =
[...new Set(
permissions
)];
Modifier :
const payload = {
sub: user.id,
tenantId: user.tenantId,
email: user.email,
sessionId: createdSession.id,
roles,
permissions:
uniquePermissions
};
Lors du :
register()
roles: [ SYSTEM_ROLES.CUSTOMER ] permissions: []
roles.guard.ts
constructor(
private reflector:
Reflector
) {}
@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
)
);
}
}
permissions.guard.ts
@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
)
);
}
}
Remplacer :
return payload;
return {
id: payload.sub,
tenantId:
payload.tenantId,
email:
payload.email,
sessionId:
payload.sessionId,
roles:
payload.roles ?? [],
permissions:
payload.permissions ?? []
};
Ajouter :
providers: [ AuthService, PasswordService, TokenService, JwtStrategy, RolesGuard, PermissionsGuard ]
exports: [ RolesGuard, PermissionsGuard ]
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
findAllUsers() {}
@Get()
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
findAllUsers() {}
@Get()
@UseGuards(
JwtAuthGuard,
RolesGuard,
PermissionsGuard
)
@Roles(
'ADMIN',
'SUPER_ADMIN'
)
@Permissions(
'users.read'
)
findAllUsers() {}
Ajouter :
@Get('admin')
@UseGuards(
JwtAuthGuard,
RolesGuard
)
@Roles(
'SUPER_ADMIN'
)
adminOnly() {
return {
success: true
};
}
@Get('permissions')
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
@Permissions(
'users.read'
)
permissionsOnly() {
return {
success: true
};
}
Doit accéder :
GET /auth/admin
Doit recevoir :
{
"statusCode": 403,
"message": "Forbidden resource"
}
Dans :
PermissionsGuard
ajouter :
if (
user.roles?.includes(
'SUPER_ADMIN'
)
) {
return true;
}
Le :
SUPER_ADMIN
contourne toutes les permissions.
Le RBAC est maintenant réutilisable sur :
Users Properties Reservations CRM Finance Marketing Security Administration
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
RolesDecorator PermissionsDecorator RolesGuard PermissionsGuard JWT enrichi RBAC Enterprise
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
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
npm install -D jest ts-jest @types/jest
apps/api/jest.config.ts
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
password.service.spec.ts
describe(
'PasswordService',
() => {
it(
'should hash password',
async () => {
const hash =
await service.hash(
'Password123!'
);
expect(hash)
.toBeDefined();
expect(hash)
.not
.toEqual(
'Password123!'
);
}
);
}
);
it(
'should compare password',
async () => {
const hash =
await service.hash(
'Password123!'
);
const result =
await service.compare(
'Password123!',
hash
);
expect(result)
.toBe(true);
}
);
token.service.spec.ts
it(
'should generate access token',
async () => {
const token =
await service
.generateAccessToken({
sub: '1',
tenantId: '1',
email:
'test@test.com'
});
expect(token)
.toBeDefined();
}
);
it(
'should generate refresh token',
async () => {
const token =
await service
.generateRefreshToken();
expect(token)
.toBeDefined();
}
);
test/mocks/prisma.mock.ts
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()
}
};
auth.service.spec.ts
beforeEach(
async () => {
const module =
await Test
.createTestingModule({
providers: [
AuthService,
{
provide:
PrismaService,
useValue:
prismaMock
}
]
})
.compile();
}
);
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();
}
);
it(
'should throw conflict',
async () => {
prismaMock.user
.findUnique
.mockResolvedValue({});
await expect(
service.register(
dto
)
).rejects.toThrow(
ConflictException
);
}
);
it(
'should login user',
async () => {
const result =
await service.login(
dto,
session
);
expect(result)
.toBeDefined();
}
);
it(
'should reject login',
async () => {
await expect(
service.login(
dto,
session
)
).rejects.toThrow(
UnauthorizedException
);
}
);
it(
'should refresh token',
async () => {
const result =
await service.refresh({
refreshToken:
'token'
});
expect(result)
.toBeDefined();
}
);
it(
'should reject invalid token',
async () => {
await expect(
service.refresh({
refreshToken:
'bad-token'
})
).rejects.toThrow(
UnauthorizedException
);
}
);
it(
'should logout user',
async () => {
const result =
await service.logout(
'user-id'
);
expect(
result.success
).toBe(true);
}
);
it(
'should return current user',
async () => {
const result =
await service.me(
'user-id'
);
expect(result)
.toBeDefined();
expect(
result.email
).toEqual(
'admin@platform.local'
);
}
);
jwt.strategy.spec.ts
it(
'should validate payload',
async () => {
const result =
await strategy.validate({
sub: '1',
email:
'test@test.com',
tenantId:
'tenant'
});
expect(
result.id
).toEqual('1');
}
);
roles.guard.spec.ts
it(
'should allow role',
() => {
expect(
guard.canActivate(
context
)
).toBe(true);
}
);
it(
'should reject role',
() => {
expect(
guard.canActivate(
context
)
).toBe(false);
}
);
permissions.guard.spec.ts
it(
'should allow permission',
() => {
expect(
guard.canActivate(
context
)
).toBe(true);
}
);
it(
'should reject permission',
() => {
expect(
guard.canActivate(
context
)
).toBe(false);
}
);
nx test api
nx test api \ --coverage
coverage/apps/api
coverage/apps/api/lcov-report/index.html
100 %
100 %
100 %
100 %
> 80 %
.github/workflows/ci.yml
- name: Run Tests run: nx test api
- name: Coverage run: nx test api --coverage
![Coverage]
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
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
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.
NestJS ↓ Prisma ↓ PostgreSQL ↓ JWT ↓ API réelle
Contrairement aux tests unitaires :
Aucun Mock Aucun Fake Service Base PostgreSQL réelle
apps/api-e2e
apps
├── api
└── api-e2e
├── src
│ └── auth
│ └── auth.e2e-spec.ts
│
├── jest.config.ts
└── project.json
npm install -D supertest npm install -D @types/supertest
.env.test
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
CREATE DATABASE rental_platform_test;
DATABASE_URL=...
npx prisma migrate deploy
DATABASE_URL=...
npx prisma db seed
apps/api-e2e/src/auth/auth.e2e-spec.ts
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();
}
);
}
);
let accessToken: string; let refreshToken: string; let userId: string;
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;
}
);
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();
}
);
GET /auth/me
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'
);
}
);
GET /auth/admin
it(
'should reject customer',
async () => {
await request(
app.getHttpServer()
)
.get(
'/auth/admin'
)
.set(
'Authorization',
`Bearer ${accessToken}`
)
.expect(403);
}
);
POST /auth/refresh
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();
}
);
POST /auth/logout
it(
'should logout',
async () => {
await request(
app.getHttpServer()
)
.post(
'/auth/logout'
)
.set(
'Authorization',
`Bearer ${accessToken}`
)
.expect(201);
}
);
Ne doit plus fonctionner.
it(
'should reject old refresh token',
async () => {
await request(
app.getHttpServer()
)
.post(
'/auth/refresh'
)
.send({
refreshToken
})
.expect(401);
}
);
it(
'should reject invalid password',
async () => {
await request(
app.getHttpServer()
)
.post(
'/auth/login'
)
.send({
email:
'john@test.com',
password:
'BAD_PASSWORD'
})
.expect(401);
}
);
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);
}
);
Dans :
afterAll()
await prisma.user.deleteMany({
where: {
email:
'john@test.com'
}
});
Ajouter :
{
"targets": {
"e2e": {
"executor":
"@nx/jest:jest"
}
}
}
nx e2e api-e2e
PASS Register PASS Login PASS Current User PASS Refresh PASS Logout PASS RBAC
.github/workflows/ci.yml
- name: E2E Tests run: nx e2e api-e2e
Lint ↓ Build ↓ Unit Tests ↓ E2E Tests ↓ Docker Build
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
auth.e2e-spec.ts .env.test Configuration E2E Pipeline CI
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
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