Table des matières
Sprint 2 — Gestion des Utilisateurs
Objectif du Sprint
Construire le premier domaine métier complet protégé par le système RBAC mis en place lors du Sprint 1.
À l'issue du Sprint 2 :
✓ Gestion des utilisateurs ✓ Gestion du profil ✓ Gestion des adresses ✓ Préférences utilisateur ✓ Paramètres notifications ✓ Sessions utilisateur ✓ Historique connexions ✓ Administration utilisateurs
Périmètre
Modules concernés
UsersModule ProfilesModule PreferencesModule NotificationModule SessionsModule
Entités concernées
User UserProfile Address UserPreference NotificationSetting UserSession ConnectionHistory
Sprint 2-A — Extension du modèle Prisma
Objectif
Compléter le modèle User existant avec les informations de profil et de personnalisation.
US-0201 — Profil utilisateur
Modèle UserProfile
Ajouter :
model UserProfile {
id String @id @default(uuid())
userId String @unique
avatarUrl String?
birthDate DateTime?
gender String?
language String?
timezone String?
biography String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields:[userId],
references:[id]
)
}
Relation User
Ajouter dans :
model User
profile UserProfile?
US-0202 — Adresses
Modèle Address
model Address {
id String @id @default(uuid())
userId String
label String
addressLine1 String
addressLine2 String?
postalCode String
city String
state String?
country String
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields:[userId],
references:[id]
)
@@index([userId])
}
Relation User
Ajouter :
addresses Address[]
US-0203 — Préférences
Modèle UserPreference
model UserPreference {
id String @id @default(uuid())
userId String @unique
theme String @default("light")
language String @default("fr")
timezone String @default("Europe/Paris")
dateFormat String @default("DD/MM/YYYY")
currency String @default("EUR")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields:[userId],
references:[id]
)
}
Relation User
Ajouter :
preferences UserPreference?
US-0204 — Notifications
Modèle NotificationSetting
model NotificationSetting {
id String @id @default(uuid())
userId String @unique
emailEnabled Boolean @default(true)
smsEnabled Boolean @default(false)
pushEnabled Boolean @default(true)
marketingEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields:[userId],
references:[id]
)
}
Relation User
Ajouter :
notificationSettings NotificationSetting?
US-0205 — Historique connexions
Modèle ConnectionHistory
model ConnectionHistory {
id String @id @default(uuid())
userId String
ipAddress String?
country String?
city String?
userAgent String?
connectedAt DateTime
disconnectedAt DateTime?
success Boolean
user User
@relation(
fields:[userId],
references:[id]
)
@@index([userId])
@@index([connectedAt])
}
Relation User
Ajouter :
connectionHistory ConnectionHistory[]
US-0206 — Sessions utilisateur
Évolution Session
Compléter :
model Session
Ajouter
sessionToken String? @unique deviceName String? platform String? isCurrent Boolean @default(false)
Migration
Générer
npx prisma migrate dev \
--name user_management
Générer Client
npx prisma generate
Sprint 2-B — Architecture NestJS
Modules
Créer :
src/modules/users ├── application │ ├── domain │ ├── infrastructure │ ├── presentation │ └── users.module.ts
Services
UsersService ProfileService PreferencesService SessionsService
Contrôleurs
UsersController ProfileController PreferencesController SessionsController
Sprint 2-C — API Utilisateurs
Endpoints
GET /users
GET /users/{id}
POST /users
PUT /users/{id}
DELETE /users/{id}
Permissions
users.read users.create users.update users.delete
Sprint 2-D — Gestion Profil
Endpoints
GET /profile PUT /profile
Données
Avatar Nom Prénom Téléphone Date naissance Langue Fuseau horaire
Sprint 2-E — Gestion Adresses
Endpoints
GET /profile/addresses
POST /profile/addresses
PUT /profile/addresses/{id}
DELETE /profile/addresses/{id}
Sprint 2-F — Préférences
Endpoints
GET /preferences PUT /preferences
Paramètres
Langue Devise Format date Fuseau horaire Thème
Sprint 2-G — Notifications
Endpoints
GET /notifications/settings PUT /notifications/settings
Paramètres
Email SMS Push Marketing
Sprint 2-H — Sessions
Endpoints
GET /sessions
DELETE /sessions/{id}
DELETE /sessions
Fonctionnalités
Lister sessions Déconnexion appareil Déconnexion globale
Sprint 2-I — Historique Connexions
Endpoint
GET /connection-history
Informations
Date IP Pays Navigateur Résultat
Sprint 2-J — RBAC
Protection
Tous les endpoints doivent utiliser :
@UseGuards( JwtAuthGuard, PermissionsGuard )
Exemple
@Permissions( 'users.read' )
Définition de terminé
Le Sprint 2 est terminé lorsque :
✓ CRUD utilisateurs ✓ Profil utilisateur ✓ Adresses ✓ Préférences ✓ Notifications ✓ Sessions ✓ Historique connexions ✓ Swagger documenté ✓ Tests unitaires ✓ Tests E2E
Sprint 2-A.1 — Implémentation Prisma complète
Objectif
Étendre le modèle d'authentification du Sprint 1 afin d'ajouter :
UserProfile Address UserPreference NotificationSetting ConnectionHistory
et enrichir :
User Session
pour préparer la gestion complète des utilisateurs.
Étape 1 — Modification du modèle User
Localiser
model User
Ajouter les relations
profile UserProfile? addresses Address[] preferences UserPreference? notificationSettings NotificationSetting? connectionHistory ConnectionHistory[]
Résultat complet
Les relations User deviennent :
userRoles UserRole[] refreshTokens RefreshToken[] sessions Session[] profile UserProfile? addresses Address[] preferences UserPreference? notificationSettings NotificationSetting? connectionHistory ConnectionHistory[]
Étape 2 — Création UserProfile
Ajouter
model UserProfile {
id String @id @default(uuid())
userId String @unique
avatarUrl String?
birthDate DateTime?
gender String?
language String?
timezone String?
biography String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields: [userId],
references: [id],
onDelete: Cascade
)
}
Étape 3 — Création Address
Ajouter
model Address {
id String @id @default(uuid())
userId String
label String
addressLine1 String
addressLine2 String?
postalCode String
city String
state String?
country String
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields: [userId],
references: [id],
onDelete: Cascade
)
@@index([userId])
@@index([country])
@@index([city])
}
Étape 4 — Création UserPreference
Ajouter
model UserPreference {
id String @id @default(uuid())
userId String @unique
theme String @default("light")
language String @default("fr")
timezone String @default("Europe/Paris")
dateFormat String @default("DD/MM/YYYY")
currency String @default("EUR")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields: [userId],
references: [id],
onDelete: Cascade
)
}
Étape 5 — Création NotificationSetting
Ajouter
model NotificationSetting {
id String @id @default(uuid())
userId String @unique
emailEnabled Boolean @default(true)
smsEnabled Boolean @default(false)
pushEnabled Boolean @default(true)
marketingEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields: [userId],
references: [id],
onDelete: Cascade
)
}
Étape 6 — Création ConnectionHistory
Ajouter
model ConnectionHistory {
id String @id @default(uuid())
userId String
ipAddress String?
country String?
city String?
userAgent String?
connectedAt DateTime
disconnectedAt DateTime?
success Boolean
user User
@relation(
fields: [userId],
references: [id],
onDelete: Cascade
)
@@index([userId])
@@index([connectedAt])
@@index([success])
}
Étape 7 — Évolution Session
Localiser
model Session
Ajouter
sessionToken String? @unique deviceName String? platform String? isCurrent Boolean @default(false)
Résultat
Le modèle Session devient capable de gérer :
Multi-appareils Historique appareils Déconnexion ciblée Déconnexion globale
Étape 8 — Validation Prisma
Exécuter
cd apps/api npx prisma validate
Résultat attendu
The schema at prisma/schema.prisma is valid
Étape 9 — Formatage
Exécuter
npx prisma format
Étape 10 — Génération migration
Exécuter
npx prisma migrate dev \
--name user_management
Résultat attendu
prisma/migrations
└── xxxx_user_management
└── migration.sql
Étape 11 — Génération Prisma Client
Exécuter
npx prisma generate
Étape 12 — Vérification SQL
PostgreSQL
docker exec -it postgres psql -U postgres
Base
\c rental_platform
Vérifier
\d "UserProfile" \d "Address" \d "UserPreference" \d "NotificationSetting" \d "ConnectionHistory" \d "Session"
Étape 13 — Mise à jour Seed
Objectif
Lors de la création d'un utilisateur, prévoir automatiquement :
UserProfile UserPreference NotificationSetting
afin d'éviter les valeurs nulles dans l'application.
À préparer pour Sprint 2-A.2
Créer automatiquement :
Profil vide Préférences par défaut Notifications par défaut
pour chaque utilisateur.
Définition de terminé
Le Sprint 2-A.1 est terminé lorsque :
✓ UserProfile créé ✓ Address créée ✓ UserPreference créée ✓ NotificationSetting créée ✓ ConnectionHistory créée ✓ Session enrichie ✓ Migration exécutée ✓ Prisma Client généré ✓ Validation Prisma verte
Livrables
schema.prisma migration user_management Prisma Client mis à jour
Sprint 2-A.2 — Amélioration du Seed Utilisateur
Objectif
Garantir qu'un utilisateur dispose systématiquement d'un environnement fonctionnel dès sa création.
À l'issue de cette étape :
✓ UserProfile créé automatiquement ✓ UserPreference créé automatiquement ✓ NotificationSetting créé automatiquement ✓ Seed enrichi ✓ Register enrichi ✓ Aucun compte incomplet
Problème actuel
Après le Sprint 2-A.1 :
User ✓ UserProfile ✗ UserPreference ✗ NotificationSetting ✗
sont créés séparément.
Risque
Les appels :
GET /profile GET /preferences GET /notifications/settings
peuvent retourner :
NULL
et générer des erreurs.
Solution
Créer automatiquement :
UserProfile UserPreference NotificationSetting
lors :
Seed principal Register utilisateur
Étape 1 — Création d'un UserFactoryService
Créer
src/modules/users/domain/services user-factory.service.ts
Responsabilité
Centraliser :
Création User Création Profile Création Preferences Création Notifications
Méthode
createDefaultUserEnvironment( userId: string )
Étape 2 — Implémentation
Ajouter
@Injectable()
export class UserFactoryService {
constructor(
private readonly prisma:
PrismaService
) {}
async createDefaultUserEnvironment(
userId: string
) {
await this.prisma.userProfile.create({
data: {
userId
}
});
await this.prisma.userPreference.create({
data: {
userId,
theme: 'light',
language: 'fr',
timezone:
'Europe/Paris',
dateFormat:
'DD/MM/YYYY',
currency: 'EUR'
}
});
await this.prisma.notificationSetting.create({
data: {
userId,
emailEnabled: true,
pushEnabled: true,
smsEnabled: false,
marketingEnabled: false
}
});
}
}
Étape 3 — Enregistrement dans UsersModule
Ajouter
providers: [ UserFactoryService ]
Étape 4 — Injection dans AuthModule
Ajouter
imports: [ UsersModule ]
Injection
Dans :
AuthService
Ajouter
constructor(
...
private readonly
userFactoryService:
UserFactoryService
)
Étape 5 — Enrichissement du Register
Localiser
const user = await this.prisma.user.create(...)
Ajouter
await this.userFactoryService
.createDefaultUserEnvironment(
user.id
);
Nouveau workflow
Create User ↓ Create Profile ↓ Create Preferences ↓ Create Notification Settings ↓ Assign Role ↓ Generate JWT
Étape 6 — Mise à jour du Seed
Localiser
prisma/seed.ts
Après création Admin
Ajouter :
const profileExists =
await prisma.userProfile.findUnique({
where: {
userId: admin.id
}
});
if (!profileExists) {
await prisma.userProfile.create({
data: {
userId: admin.id
}
});
}
Étape 7 — Préférences Admin
Ajouter
const preferenceExists =
await prisma.userPreference.findUnique({
where: {
userId: admin.id
}
});
if (!preferenceExists) {
await prisma.userPreference.create({
data: {
userId: admin.id,
language: 'fr',
timezone:
'Europe/Paris',
currency: 'EUR'
}
});
}
Étape 8 — Notifications Admin
Ajouter
const settingsExists =
await prisma.notificationSetting.findUnique({
where: {
userId: admin.id
}
});
if (!settingsExists) {
await prisma.notificationSetting.create({
data: {
userId: admin.id
}
});
}
Étape 9 — Rejouer le Seed
Exécuter
npx prisma db seed
Résultat attendu
Starting seed... Tenant created Permissions created Roles created Admin created Profile created Preferences created Notification settings created Seed completed
Étape 10 — Vérification Prisma Studio
Ouvrir
npx prisma studio
Vérifier
User ↓ UserProfile ↓ UserPreference ↓ NotificationSetting
Étape 11 — Vérification SQL
UserProfile
SELECT * FROM "UserProfile";
UserPreference
SELECT * FROM "UserPreference";
NotificationSetting
SELECT * FROM "NotificationSetting";
Étape 12 — Test Register
Requête
POST /auth/register
Vérifier
Après inscription :
SELECT * FROM "UserProfile" WHERE "userId" = 'new-user-id';
Résultat attendu
1 ligne
Optimisation Enterprise
Évolution recommandée
Créer un événement métier :
UserCreated
Puis
UserCreated ↓ CreateUserProfile ↓ CreateUserPreferences ↓ CreateNotificationSettings
via :
Domain Events CQRS EventBus
afin de découpler complètement Auth et Users.
Définition de terminé
Le Sprint 2-A.2 est terminé lorsque :
✓ UserProfile créé automatiquement ✓ UserPreference créé automatiquement ✓ NotificationSetting créé automatiquement ✓ Seed mis à jour ✓ Register mis à jour ✓ Aucun compte incomplet
Livrables
UserFactoryService AuthService enrichi seed.ts enrichi Création automatique du profil utilisateur
Sprint 2-B.1 — Création du UsersModule
Objectif
Implémenter le premier module métier complet de la plateforme.
À l'issue de cette étape :
✓ UsersModule ✓ UsersController ✓ UsersService ✓ DTO ✓ CRUD Utilisateurs ✓ RBAC ✓ Swagger ✓ Validation
Architecture
Créer
src/modules/users
Structure
users ├── application │ │ └── dto │ │ ├── create-user.dto.ts │ ├── update-user.dto.ts │ ├── user-response.dto.ts │ └── users-query.dto.ts │ ├── domain │ │ └── services │ │ └── users.service.ts │ ├── presentation │ │ └── controllers │ │ └── users.controller.ts │ └── users.module.ts
Étape 1 — CreateUserDto
Créer
application/dto/create-user.dto.ts
Implémentation
import {
IsEmail,
IsString,
MinLength,
IsOptional
} from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@MinLength(8)
password: string;
@IsString()
firstName: string;
@IsString()
lastName: string;
@IsOptional()
phone?: string;
}
Étape 2 — UpdateUserDto
Créer
application/dto/update-user.dto.ts
Implémentation
import {
IsOptional,
IsString
} from 'class-validator';
export class UpdateUserDto {
@IsOptional()
@IsString()
firstName?: string;
@IsOptional()
@IsString()
lastName?: string;
@IsOptional()
@IsString()
phone?: string;
@IsOptional()
@IsString()
status?: string;
}
Étape 3 — UserResponseDto
Créer
application/dto/user-response.dto.ts
Implémentation
export class UserResponseDto {
id: string;
tenantId: string;
email: string;
firstName: string;
lastName: string;
phone?: string;
status: string;
emailVerified: boolean;
createdAt: Date;
}
Étape 4 — UsersQueryDto
Créer
application/dto/users-query.dto.ts
Implémentation
import {
IsOptional,
IsNumberString
} from 'class-validator';
export class UsersQueryDto {
@IsOptional()
search?: string;
@IsOptional()
@IsNumberString()
page?: string;
@IsOptional()
@IsNumberString()
limit?: string;
}
Étape 5 — Création UsersService
Créer
domain/services/users.service.ts
Injection
@Injectable()
export class UsersService {
constructor(
private readonly prisma:
PrismaService,
private readonly passwordService:
PasswordService,
private readonly userFactoryService:
UserFactoryService
) {}
}
Étape 6 — Méthodes du service
Ajouter
findAll() findOne() create() update() remove()
Étape 7 — Implémentation findAll
Ajouter
async findAll(
query: UsersQueryDto
) {
const page =
Number(query.page ?? 1);
const limit =
Number(query.limit ?? 20);
const skip =
(page - 1) * limit;
return this.prisma.user.findMany({
skip,
take: limit,
where: {
deletedAt: null
},
orderBy: {
createdAt: 'desc'
}
});
}
Étape 8 — Implémentation findOne
Ajouter
async findOne(
id: string
) {
const user =
await this.prisma.user.findUnique({
where: { id }
});
if (!user) {
throw new NotFoundException(
'User not found'
);
}
return user;
}
Étape 9 — Implémentation create
Ajouter
async create(
dto: CreateUserDto
) {
const tenant =
await this.prisma.tenant.findUnique({
where: {
code: 'MAIN'
}
});
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,
phone: dto.phone,
status: 'ACTIVE'
}
});
await this.userFactoryService
.createDefaultUserEnvironment(
user.id
);
return user;
}
Étape 10 — Implémentation update
Ajouter
async update(
id: string,
dto: UpdateUserDto
) {
await this.findOne(id);
return this.prisma.user.update({
where: { id },
data: dto
});
}
Étape 11 — Implémentation remove
Soft Delete
async remove(
id: string
) {
await this.findOne(id);
return this.prisma.user.update({
where: { id },
data: {
deletedAt:
new Date(),
status:
'INACTIVE'
}
});
}
Étape 12 — Création UsersController
Créer
presentation/controllers/users.controller.ts
Déclaration
@ApiTags('Users')
@Controller('users')
@UseGuards(
JwtAuthGuard,
PermissionsGuard
)
export class UsersController {
constructor(
private readonly usersService:
UsersService
) {}
}
Étape 13 — GET /users
Route
@Get() @Permissions( 'users.read' )
Implémentation
findAll(
@Query()
query: UsersQueryDto
) {
return this.usersService
.findAll(query);
}
Étape 14 — GET /users/{id}
Route
@Get(':id')
@Permissions(
'users.read'
)
Implémentation
findOne(
@Param('id')
id: string
) {
return this.usersService
.findOne(id);
}
Étape 15 — POST /users
Route
@Post() @Permissions( 'users.create' )
Implémentation
create(
@Body()
dto: CreateUserDto
) {
return this.usersService
.create(dto);
}
Étape 16 — PUT /users/{id}
Route
@Put(':id')
@Permissions(
'users.update'
)
Implémentation
update(
@Param('id')
id: string,
@Body()
dto: UpdateUserDto
) {
return this.usersService
.update(
id,
dto
);
}
Étape 17 — DELETE /users/{id}
Route
@Delete(':id')
@Permissions(
'users.delete'
)
Implémentation
remove(
@Param('id')
id: string
) {
return this.usersService
.remove(id);
}
Étape 18 — UsersModule
Créer
users.module.ts
Implémentation
@Module({
imports: [
PrismaModule,
AuthModule
],
controllers: [
UsersController
],
providers: [
UsersService,
UserFactoryService
],
exports: [
UsersService
]
})
export class UsersModule {}
Étape 19 — Swagger
Vérifier
Les endpoints suivants doivent apparaître :
GET /users
GET /users/{id}
POST /users
PUT /users/{id}
DELETE /users/{id}
Étape 20 — Tests manuels
SUPER_ADMIN
Doit pouvoir :
Créer Modifier Lister Supprimer
des utilisateurs.
CUSTOMER
Doit recevoir :
{
"statusCode": 403,
"message": "Forbidden resource"
}
Définition de terminé
Le Sprint 2-B.1 est terminé lorsque :
✓ UsersModule créé ✓ UsersController créé ✓ UsersService créé ✓ CRUD fonctionnel ✓ RBAC actif ✓ Swagger documenté ✓ Soft Delete opérationnel
Livrables
UsersModule UsersController UsersService CreateUserDto UpdateUserDto UserResponseDto UsersQueryDto
Sprint 2-B.2 — Amélioration Enterprise du UsersService
Objectif
Transformer le CRUD utilisateur basique en véritable service Enterprise.
À l'issue de cette étape :
✓ Pagination avancée ✓ Recherche multi-champs ✓ Tri dynamique ✓ Filtres ✓ Isolation Multi-Tenant ✓ Audit Logs ✓ Soft Delete sécurisé ✓ Réponses paginées
Problème actuel
Le service actuel :
findAll()
ne gère que :
Pagination simple
et ne protège pas encore :
Isolation tenant Recherche Audit Tri
Étape 1 — Création UserQueryDto Enterprise
Remplacer
users-query.dto.ts
Nouveau DTO
import {
IsOptional,
IsString,
IsNumberString
} from 'class-validator';
export class UsersQueryDto {
@IsOptional()
search?: string;
@IsOptional()
status?: string;
@IsOptional()
emailVerified?: string;
@IsOptional()
role?: string;
@IsOptional()
sortBy?: string;
@IsOptional()
sortOrder?: 'asc' | 'desc';
@IsOptional()
@IsNumberString()
page?: string;
@IsOptional()
@IsNumberString()
limit?: string;
}
Étape 2 — DTO de pagination
Créer
application/dto/paginated-response.dto.ts
Implémentation
export class PaginatedResponseDto<T> {
data: T[];
page: number;
limit: number;
total: number;
totalPages: number;
}
Étape 3 — Sécurité Multi-Tenant
Principe
Aucun utilisateur ne doit voir :
Tenant A ↓ Tenant B
Récupération
Dans le contrôleur :
@Req() request
Extraire
const tenantId = request.user.tenantId;
Étape 4 — Signature findAll
Modifier
async findAll( tenantId: string, query: UsersQueryDto )
Étape 5 — Construction du filtre
Ajouter
const where: Prisma.UserWhereInput = {
tenantId,
deletedAt: null
};
Recherche
if (query.search) {
where.OR = [
{
email: {
contains: query.search,
mode: 'insensitive'
}
},
{
firstName: {
contains: query.search,
mode: 'insensitive'
}
},
{
lastName: {
contains: query.search,
mode: 'insensitive'
}
}
];
}
Étape 6 — Filtre statut
Ajouter
if (query.status) {
where.status =
query.status;
}
Étape 7 — Filtre email vérifié
Ajouter
if (
query.emailVerified !==
undefined
) {
where.emailVerified =
query.emailVerified ===
'true';
}
Étape 8 — Pagination
Ajouter
const page = Number(query.page ?? 1); const limit = Number(query.limit ?? 20); const skip = (page - 1) * limit;
Étape 9 — Tri dynamique
Ajouter
const sortBy = query.sortBy ?? 'createdAt'; const sortOrder = query.sortOrder ?? 'desc';
OrderBy
orderBy: {
[sortBy]:
sortOrder
}
Étape 10 — Count global
Ajouter
const total =
await this.prisma.user.count({
where
});
Étape 11 — Chargement utilisateurs
Ajouter
const users =
await this.prisma.user.findMany({
where,
skip,
take: limit,
orderBy: {
[sortBy]:
sortOrder
}
});
Étape 12 — Réponse paginée
Retour
return {
data: users,
page,
limit,
total,
totalPages:
Math.ceil(
total / limit
)
};
Étape 13 — Sécurisation findOne
Modifier
async findOne( tenantId: string, id: string )
Requête
const user =
await this.prisma.user.findFirst({
where: {
id,
tenantId,
deletedAt: null
}
});
Résultat
Impossible d'accéder à un utilisateur :
Autre tenant
Étape 14 — Sécurisation update
Ajouter
await this.findOne( tenantId, id );
avant toute modification.
Étape 15 — Sécurisation remove
Ajouter
await this.findOne( tenantId, id );
avant suppression.
Étape 16 — AuditLog
Préparation
Le modèle :
AuditLog
sera créé lors de la Phase 2-I.
Interface temporaire
Créer :
shared/interfaces/audit.interface.ts
export interface AuditEvent {
action: string;
entityType: string;
entityId: string;
userId: string;
timestamp: Date;
}
Étape 17 — Audit Create
Ajouter
Après :
create()
Événement
this.logger.log({
action: 'USER_CREATED',
entityType: 'User',
entityId: user.id
});
Étape 18 — Audit Update
Ajouter
USER_UPDATED
Étape 19 — Audit Delete
Ajouter
USER_DELETED
Étape 20 — Contrôleur Enterprise
Modifier
findAll(
@Req()
req,
@Query()
query
) {
return this.usersService.findAll(
req.user.tenantId,
query
);
}
Même logique
Pour :
findOne() update() remove()
Étape 21 — Swagger
Ajouter
Paramètres :
search status emailVerified sortBy sortOrder page limit
dans :
@ApiQuery()
Étape 22 — Exemples
Recherche
GET /users?search=john
Pagination
GET /users?page=2&limit=50
Tri
GET /users?sortBy=email&sortOrder=asc
Filtre
GET /users?status=ACTIVE
Étape 23 — Performance
Ajouter index Prisma
Dans :
model User
Vérifier
@@index([tenantId]) @@index([email]) @@index([status]) @@index([createdAt]) @@index([deletedAt])
Étape 24 — Tests
Vérifier
Recherche Pagination Tri Filtre Isolation tenant Soft Delete
Définition de terminé
Le Sprint 2-B.2 est terminé lorsque :
✓ Pagination Enterprise ✓ Recherche multi-champs ✓ Tri dynamique ✓ Filtres ✓ Multi-Tenant Security ✓ Audit préparé ✓ Swagger enrichi ✓ Tests verts
Livrables
UsersQueryDto PaginatedResponseDto UsersService Enterprise Recherche Filtres Pagination Isolation Multi-Tenant
Sprint 2-C.1 — Gestion du Profil Utilisateur
Objectif
Implémenter la gestion complète du profil utilisateur basée sur :
UserProfile
À l'issue de cette étape :
✓ ProfileModule ✓ ProfileController ✓ ProfileService ✓ GET /profile ✓ PUT /profile ✓ Gestion Avatar ✓ Gestion Informations personnelles ✓ Swagger ✓ RBAC
Architecture
Créer
src/modules/profile
Structure
profile ├── application │ │ └── dto │ │ ├── update-profile.dto.ts │ └── profile-response.dto.ts │ ├── domain │ │ └── services │ │ └── profile.service.ts │ ├── presentation │ │ └── controllers │ │ └── profile.controller.ts │ └── profile.module.ts
Étape 1 — UpdateProfileDto
Créer
application/dto/update-profile.dto.ts
Implémentation
import {
IsOptional,
IsString,
IsDateString,
IsUrl
} from 'class-validator';
export class UpdateProfileDto {
@IsOptional()
@IsUrl()
avatarUrl?: string;
@IsOptional()
@IsDateString()
birthDate?: string;
@IsOptional()
@IsString()
gender?: string;
@IsOptional()
@IsString()
language?: string;
@IsOptional()
@IsString()
timezone?: string;
@IsOptional()
@IsString()
biography?: string;
@IsOptional()
@IsString()
firstName?: string;
@IsOptional()
@IsString()
lastName?: string;
@IsOptional()
@IsString()
phone?: string;
}
Étape 2 — ProfileResponseDto
Créer
application/dto/profile-response.dto.ts
Implémentation
export class ProfileResponseDto {
id: string;
email: string;
firstName: string;
lastName: string;
phone?: string;
avatarUrl?: string;
birthDate?: Date;
gender?: string;
language?: string;
timezone?: string;
biography?: string;
emailVerified: boolean;
}
Étape 3 — Création ProfileService
Créer
domain/services/profile.service.ts
Injection
@Injectable()
export class ProfileService {
constructor(
private readonly prisma:
PrismaService
) {}
}
Étape 4 — Méthodes
Ajouter
getProfile() updateProfile()
Étape 5 — Implémentation getProfile
Ajouter
async getProfile(
userId: string,
tenantId: string
) {
Chargement
const user =
await this.prisma.user.findFirst({
where: {
id: userId,
tenantId,
deletedAt: null
},
include: {
profile: true
}
});
Vérification
if (!user) {
throw new NotFoundException(
'User not found'
);
}
Retour
return {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
emailVerified:
user.emailVerified,
avatarUrl:
user.profile?.avatarUrl,
birthDate:
user.profile?.birthDate,
gender:
user.profile?.gender,
language:
user.profile?.language,
timezone:
user.profile?.timezone,
biography:
user.profile?.biography
};
Étape 6 — Implémentation updateProfile
Signature
async updateProfile( userId: string, tenantId: string, dto: UpdateProfileDto )
Étape 7 — Mise à jour User
Ajouter
await this.prisma.user.update({
where: {
id: userId
},
data: {
firstName:
dto.firstName,
lastName:
dto.lastName,
phone:
dto.phone
}
});
Étape 8 — Mise à jour UserProfile
Ajouter
await this.prisma.userProfile.update({
where: {
userId
},
data: {
avatarUrl:
dto.avatarUrl,
birthDate:
dto.birthDate,
gender:
dto.gender,
language:
dto.language,
timezone:
dto.timezone,
biography:
dto.biography
}
});
Étape 9 — Retour profil
Ajouter
return this.getProfile( userId, tenantId );
Étape 10 — Création Controller
Créer
presentation/controllers/profile.controller.ts
Déclaration
@ApiTags('Profile')
@Controller('profile')
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Étape 11 — GET /profile
Route
@Get()
Implémentation
getProfile(
@Req() req
) {
return this.profileService
.getProfile(
req.user.id,
req.user.tenantId
);
}
Étape 12 — PUT /profile
Route
@Put()
Implémentation
updateProfile(
@Req() req,
@Body()
dto: UpdateProfileDto
) {
return this.profileService
.updateProfile(
req.user.id,
req.user.tenantId,
dto
);
}
Étape 13 — Swagger
GET
@ApiOperation({
summary:
'Get current profile'
})
PUT
@ApiOperation({
summary:
'Update current profile'
})
Réponse
@ApiOkResponse({
type:
ProfileResponseDto
})
Étape 14 — ProfileModule
Créer
profile.module.ts
Implémentation
@Module({
imports: [
PrismaModule,
AuthModule
],
controllers: [
ProfileController
],
providers: [
ProfileService
],
exports: [
ProfileService
]
})
export class ProfileModule {}
Étape 15 — Import AppModule
Ajouter
imports: [ ... ProfileModule ]
Étape 16 — Test GET
Requête
GET /profile Authorization: Bearer xxx
Réponse
{
"id": "...",
"email": "john@test.com",
"firstName": "John",
"lastName": "Doe",
"avatarUrl": null,
"language": "fr",
"timezone": "Europe/Paris"
}
Étape 17 — Test PUT
Requête
PUT /profile
Body
{
"avatarUrl":
"https://cdn.app/avatar.jpg",
"language":
"fr",
"timezone":
"Europe/Paris",
"biography":
"Property manager"
}
Résultat
Profil mis à jour.
Étape 18 — Sécurisation
Vérifier
Un utilisateur :
Ne peut modifier Que son propre profil
Garantie
Grâce à :
req.user.id
aucun identifiant utilisateur n'est exposé dans l'API.
Étape 19 — Audit futur
Préparer
Événement :
PROFILE_UPDATED
qui sera connecté plus tard à :
AuditLog EntityHistory
du Sprint 19.
Définition de terminé
Le Sprint 2-C.1 est terminé lorsque :
✓ ProfileModule créé ✓ ProfileController créé ✓ ProfileService créé ✓ GET /profile opérationnel ✓ PUT /profile opérationnel ✓ Swagger documenté ✓ Sécurité validée
Livrables
ProfileModule ProfileController ProfileService UpdateProfileDto ProfileResponseDto
Sprint 2-C.2 — Gestion avancée du Profil
Objectif
Transformer le profil utilisateur en profil Enterprise complet.
À l'issue de cette étape :
✓ Upload Avatar ✓ Validation image ✓ Suppression Avatar ✓ Stockage MinIO ✓ Compatibilité S3 ✓ Historique modifications ✓ Audit des changements ✓ URLs sécurisées
Architecture cible
Client ↓ Upload Avatar ↓ Validation ↓ Storage Service ↓ MinIO / S3 ↓ UserProfile ↓ AuditLog
Étape 1 — Création du StorageModule
Créer
src/modules/storage
Structure
storage ├── domain │ │ └── services │ │ └── storage.service.ts │ ├── infrastructure │ │ └── providers │ │ └── minio.provider.ts │ └── storage.module.ts
Étape 2 — Installation MinIO
Installer
npm install minio
Étape 3 — Variables d'environnement
Ajouter
MINIO_ENDPOINT=localhost MINIO_PORT=9000 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_BUCKET=avatars MINIO_SSL=false
Étape 4 — Docker Compose
Ajouter
minio: image: minio/minio container_name: minio command: server /data ports: - "9000:9000" - "9001:9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin volumes: - minio-data:/data
Étape 5 — Minio Provider
Créer
infrastructure/providers/minio.provider.ts
Implémentation
import * as Minio
from 'minio';
export const MinioProvider = {
provide: 'MINIO',
useFactory: () => {
return new Minio.Client({
endPoint:
process.env.MINIO_ENDPOINT,
port:
Number(
process.env.MINIO_PORT
),
useSSL:
process.env.MINIO_SSL ===
'true',
accessKey:
process.env.MINIO_ACCESS_KEY,
secretKey:
process.env.MINIO_SECRET_KEY
});
}
};
Étape 6 — StorageService
Créer
storage.service.ts
Méthodes
uploadAvatar() deleteAvatar() generateUrl()
Étape 7 — Upload Avatar
Ajouter
async uploadAvatar( file: Express.Multer.File, userId: string )
Nom du fichier
const fileName =
`avatars/${userId}/${
randomUUID()
}.jpg`;
Upload
await this.minioClient.putObject( process.env.MINIO_BUCKET, fileName, file.buffer );
Retour
return fileName;
Étape 8 — Validation image
Contraintes
jpg jpeg png webp
Taille
5 MB max
Validator
Créer :
shared/validators/avatar.validator.ts
Implémentation
export const ALLOWED_TYPES = [ 'image/jpeg', 'image/png', 'image/webp' ];
Contrôle
if (
!ALLOWED_TYPES.includes(
file.mimetype
)
) {
throw new BadRequestException(
'Invalid image'
);
}
Étape 9 — DTO Upload
Créer
application/dto/avatar-response.dto.ts
Contenu
export class AvatarResponseDto {
avatarUrl: string;
}
Étape 10 — Nouveau endpoint
Route
POST /profile/avatar
Controller
@Post('avatar')
@UseInterceptors(
FileInterceptor(
'file'
)
)
Swagger
@ApiConsumes( 'multipart/form-data' )
Étape 11 — Upload
Implémentation
uploadAvatar(
@Req() req,
@UploadedFile()
file
) {
return this.profileService
.uploadAvatar(
req.user.id,
file
);
}
Étape 12 — Service Profile
Ajouter
async uploadAvatar(
userId: string,
file:
Express.Multer.File
)
Upload
const avatarUrl =
await this.storageService
.uploadAvatar(
file,
userId
);
Mise à jour profil
await this.prisma.userProfile.update({
where: {
userId
},
data: {
avatarUrl
}
});
Étape 13 — Suppression Avatar
Endpoint
DELETE /profile/avatar
Service
async deleteAvatar( userId: string )
Étapes
Load Profile ↓ Delete Object MinIO ↓ avatarUrl = null
Étape 14 — Génération URL
Objectif
Ne jamais exposer :
bucket interne
Utiliser
presignedGetObject()
Exemple
const url =
await this.minioClient
.presignedGetObject(
bucket,
fileName,
3600
);
Étape 15 — Historique Profil
Créer
ProfileHistory
Modèle Prisma
model ProfileHistory {
id String @id @default(uuid())
userId String
changedField String
oldValue String?
newValue String?
changedAt DateTime @default(now())
}
Étape 16 — Relation User
Ajouter
profileHistory ProfileHistory[]
Étape 17 — Audit Modification
Lors du update
Ajouter :
PROFILE_UPDATED
Données
Champ Ancienne valeur Nouvelle valeur Date
Étape 18 — Audit Avatar
Événements
AVATAR_UPLOADED AVATAR_DELETED
Étape 19 — Swagger
Vérifier
GET /profile PUT /profile POST /profile/avatar DELETE /profile/avatar
Étape 20 — Tests
Upload
jpg valide ↓ 200
PNG
png valide ↓ 200
pdf ↓ 400
Fichier 10MB
10MB ↓ 400
Étape 21 — Préparation Enterprise
Cette architecture est compatible :
AWS S3 MinIO Azure Blob Storage Google Cloud Storage
via simple remplacement du :
StorageProvider
Définition de terminé
Le Sprint 2-C.2 est terminé lorsque :
✓ Upload Avatar ✓ Suppression Avatar ✓ Validation image ✓ MinIO opérationnel ✓ URLs sécurisées ✓ Historisation profil ✓ Audit prêt ✓ Swagger documenté
Livrables
StorageModule StorageService MinioProvider Avatar Upload Avatar Delete ProfileHistory Audit Profile
Sprint 2-D.1 — Gestion des Adresses Utilisateur
Objectif
Implémenter la gestion complète des adresses utilisateur.
À l'issue de cette étape :
✓ AddressModule ✓ AddressController ✓ AddressService ✓ CRUD Adresses ✓ Adresse par défaut ✓ Validation pays ✓ Validation code postal ✓ Swagger ✓ Sécurité utilisateur
Architecture
Créer
src/modules/address
Structure
address ├── application │ │ └── dto │ │ ├── create-address.dto.ts │ ├── update-address.dto.ts │ └── address-response.dto.ts │ ├── domain │ │ └── services │ │ └── address.service.ts │ ├── presentation │ │ └── controllers │ │ └── address.controller.ts │ └── address.module.ts
Étape 1 — CreateAddressDto
Créer
application/dto/create-address.dto.ts
Implémentation
import {
IsString,
IsOptional,
IsBoolean
} from 'class-validator';
export class CreateAddressDto {
@IsString()
label: string;
@IsString()
addressLine1: string;
@IsOptional()
@IsString()
addressLine2?: string;
@IsString()
postalCode: string;
@IsString()
city: string;
@IsOptional()
@IsString()
state?: string;
@IsString()
country: string;
@IsOptional()
@IsBoolean()
isDefault?: boolean;
}
Étape 2 — UpdateAddressDto
Créer
application/dto/update-address.dto.ts
Implémentation
import {
PartialType
} from '@nestjs/swagger';
import {
CreateAddressDto
} from './create-address.dto';
export class UpdateAddressDto
extends PartialType(
CreateAddressDto
) {}
Étape 3 — AddressResponseDto
Créer
application/dto/address-response.dto.ts
Contenu
export class AddressResponseDto {
id: string;
label: string;
addressLine1: string;
addressLine2?: string;
postalCode: string;
city: string;
state?: string;
country: string;
isDefault: boolean;
createdAt: Date;
}
Étape 4 — Création AddressService
Créer
domain/services/address.service.ts
Injection
@Injectable()
export class AddressService {
constructor(
private readonly prisma:
PrismaService
) {}
}
Étape 5 — Méthodes
Ajouter
findAll() create() update() remove() setDefault()
Étape 6 — Validation Pays
Créer
shared/constants/countries.constants.ts
Exemple
export const SUPPORTED_COUNTRIES = [ 'FR', 'BE', 'CH', 'ES', 'IT', 'DE', 'GB', 'US', 'CA' ];
Contrôle
if (
!SUPPORTED_COUNTRIES.includes(
dto.country
)
) {
throw new BadRequestException(
'Unsupported country'
);
}
Étape 7 — Validation Code Postal
Créer
shared/validators/postal-code.validator.ts
Exemple FR
const frenchPostalCode =
/^[0-9]{5}$/;
Vérification
if (
dto.country === 'FR' &&
!frenchPostalCode.test(
dto.postalCode
)
) {
throw new BadRequestException(
'Invalid postal code'
);
}
Étape 8 — Implémentation findAll
Ajouter
async findAll(
userId: string
) {
return this.prisma.address.findMany({
where: {
userId
},
orderBy: [
{
isDefault:
'desc'
},
{
createdAt:
'desc'
}
]
});
}
Étape 9 — Gestion adresse par défaut
Méthode
private async resetDefaultAddress( userId: string )
Implémentation
await this.prisma.address.updateMany({
where: {
userId
},
data: {
isDefault: false
}
});
Étape 10 — Implémentation create
Ajouter
async create( userId: string, dto: CreateAddressDto )
Adresse par défaut
if (dto.isDefault) {
await this.resetDefaultAddress(
userId
);
}
Création
return this.prisma.address.create({
data: {
userId,
...dto
}
});
Étape 11 — Implémentation update
Charger
const address =
await this.prisma.address.findFirst({
where: {
id,
userId
}
});
Vérification
if (!address) {
throw new NotFoundException(
'Address not found'
);
}
Gestion défaut
if (dto.isDefault) {
await this.resetDefaultAddress(
userId
);
}
Mise à jour
return this.prisma.address.update({
where: {
id
},
data: dto
});
Étape 12 — Implémentation remove
Ajouter
await this.prisma.address.delete({
where: {
id
}
});
Étape 13 — Création Controller
Créer
presentation/controllers/address.controller.ts
Déclaration
@ApiTags('Addresses')
@Controller(
'profile/addresses'
)
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Étape 14 — GET
Route
@Get()
Implémentation
findAll(
@Req() req
) {
return this.addressService.findAll(
req.user.id
);
}
Étape 15 — POST
Route
@Post()
Implémentation
create(
@Req() req,
@Body()
dto: CreateAddressDto
) {
return this.addressService.create(
req.user.id,
dto
);
}
Étape 16 — PUT
Route
@Put(':id')
Implémentation
update(
@Req() req,
@Param('id')
id: string,
@Body()
dto: UpdateAddressDto
) {
return this.addressService.update(
req.user.id,
id,
dto
);
}
Étape 17 — DELETE
Route
@Delete(':id')
Implémentation
remove(
@Req() req,
@Param('id')
id: string
) {
return this.addressService.remove(
req.user.id,
id
);
}
Étape 18 — AddressModule
Créer
address.module.ts
Implémentation
@Module({
imports: [
PrismaModule,
AuthModule
],
controllers: [
AddressController
],
providers: [
AddressService
],
exports: [
AddressService
]
})
export class AddressModule {}
Étape 19 — Swagger
Vérifier
GET /profile/addresses
POST /profile/addresses
PUT /profile/addresses/{id}
DELETE /profile/addresses/{id}
Étape 20 — Tests
Création
{
"label": "Domicile",
"addressLine1": "10 rue Victor Hugo",
"postalCode": "75001",
"city": "Paris",
"country": "FR",
"isDefault": true
}
Vérifier
Adresse créée Adresse par défaut appliquée Validation pays OK Validation code postal OK
Amélioration Enterprise
Sprint 2-D.2
Prévoir :
Google Places OpenStreetMap Validation géographique Latitude Longitude Géocodage inverse
afin d'améliorer :
Facturation Réservations Revenue Management CRM
Définition de terminé
Le Sprint 2-D.1 est terminé lorsque :
✓ AddressModule créé ✓ AddressController créé ✓ AddressService créé ✓ CRUD adresses opérationnel ✓ Adresse par défaut opérationnelle ✓ Validation pays opérationnelle ✓ Validation code postal opérationnelle ✓ Swagger documenté ✓ Tests verts
Livrables
AddressModule AddressController AddressService CreateAddressDto UpdateAddressDto AddressResponseDto
Sprint 2-D.2 — Géolocalisation & Validation Avancée des Adresses
Objectif
Transformer le système d'adresses en composant Enterprise géolocalisé.
À l'issue de cette étape :
✓ Latitude ✓ Longitude ✓ Géocodage ✓ Reverse Geocoding ✓ Validation géographique ✓ Normalisation adresse ✓ OpenStreetMap ✓ Google Places (optionnel) ✓ Recherche cartographique
Architecture cible
User Address ↓ Validation ↓ Geocoding Service ↓ OpenStreetMap ou Google Places ↓ Latitude / Longitude ↓ Address Storage
Sprint 2-D.2-A
Extension Prisma
Étape 1 — Évolution du modèle Address
Ajouter
Dans :
model Address
Nouveaux champs
latitude Decimal? @db.Decimal(10,7)
longitude Decimal? @db.Decimal(10,7)
formattedAddress String?
streetNumber String?
route String?
region String?
county String?
geocodedAt DateTime?
placeId String?
validationStatus String? @default("PENDING")
Statuts
PENDING VALIDATED FAILED
Étape 2 — Migration
Générer
npx prisma migrate dev \
--name address_geolocation
Générer
npx prisma generate
Sprint 2-D.2-B
GeocodingModule
Étape 3 — Création du module
src/modules/geocoding ├── application │ ├── domain │ │ └── services │ │ └── geocoding.service.ts │ ├── infrastructure │ │ └── providers │ │ ├── osm.provider.ts │ └── google.provider.ts │ └── geocoding.module.ts
Étape 4 — Installation
HTTP Client
npm install @nestjs/axios
Étape 5 — Variables
OpenStreetMap
OSM_BASE_URL= https://nominatim.openstreetmap.org
Google (optionnel)
GOOGLE_MAPS_API_KEY=
Sprint 2-D.2-C
DTO Géocodage
Étape 6 — GeocodingResult
Créer
domain/models/geocoding-result.ts
Implémentation
export interface GeocodingResult {
latitude: number;
longitude: number;
formattedAddress:
string;
streetNumber?: string;
route?: string;
city?: string;
region?: string;
postalCode?: string;
country?: string;
placeId?: string;
}
Étape 7 — GeocodingService
Signature
@Injectable()
export class GeocodingService {
async geocode(
address: string
)
async reverseGeocode(
latitude: number,
longitude: number
)
}
Étape 8 — Géocodage OpenStreetMap
URL
GET /search
Paramètres
q format=jsonv2 limit=1
Exemple
const response =
await this.httpService.axiosRef.get(
`${baseUrl}/search`,
{
params: {
q: address,
format: 'jsonv2',
limit: 1
}
}
);
Étape 9 — Conversion résultat
Mapper
return {
latitude:
Number(result.lat),
longitude:
Number(result.lon),
formattedAddress:
result.display_name
};
Sprint 2-D.2-D
Validation Adresse
Étape 10 — Création du validateur
Créer
shared/services/address-validation.service.ts
Fonction
validateAddress( dto )
Vérifications
Pays valide Ville valide Code postal cohérent Adresse trouvée Coordonnées trouvées
Étape 11 — Échec
Retour
throw new BadRequestException( 'Address validation failed' );
Sprint 2-D.2-E
Normalisation
Étape 12 — Construction adresse
Exemple
Entrée :
10 rue victor hugo 75001 paris
Résultat
10 Rue Victor Hugo, 75001 Paris, France
Étape 13 — Service
Ajouter
normalizeAddress( dto )
Retour
formattedAddress
Sprint 2-D.2-F
Intégration AddressService
Étape 14 — Injection
Ajouter
constructor(
private readonly prisma:
PrismaService,
private readonly geocodingService:
GeocodingService
)
Étape 15 — Géocodage automatique
Dans create()
Ajouter :
const addressString =
`${dto.addressLine1}
${dto.postalCode}
${dto.city}
${dto.country}`;
Géocoder
const geocoded =
await this.geocodingService
.geocode(
addressString
);
Étape 16 — Sauvegarde
Ajouter
latitude: geocoded.latitude, longitude: geocoded.longitude, formattedAddress: geocoded.formattedAddress, validationStatus: 'VALIDATED', geocodedAt: new Date()
Étape 17 — Gestion erreur
Fallback
validationStatus: 'FAILED'
Adresse conservée
Même si :
OpenStreetMap indisponible
Sprint 2-D.2-G
Reverse Geocoding
Étape 18 — Nouvelle méthode
reverseGeocode( lat, lng )
Usage futur
Properties Reservations CRM Mobile App
Sprint 2-D.2-H
Recherche géographique
Étape 19 — Endpoint
GET /profile/addresses/nearby
Paramètres
lat lng radius
Préparation
Pour :
Recherche biens Recherche clients Recherche agences
Sprint 2-D.2-I
Swagger
Étape 20 — Documentation
GET
GET /profile/addresses
retourne désormais :
{
"latitude": 48.864716,
"longitude": 2.349014,
"formattedAddress":
"10 Rue Victor Hugo, Paris"
}
Étape 21 — DTO
Enrichir
AddressResponseDto
Ajouter
latitude?: number; longitude?: number; formattedAddress?: string; validationStatus?: string;
Sprint 2-D.2-J
Préparation Phase Internationale
Compatibilité
Le modèle devient compatible :
Multi-pays Multi-régions Multi-devises Fiscalité Facturation CRM Revenue Management Channel Manager
Définition de terminé
Le Sprint 2-D.2 est terminé lorsque :
✓ Latitude stockée ✓ Longitude stockée ✓ Géocodage automatique ✓ Reverse Geocoding ✓ Validation adresse ✓ Normalisation adresse ✓ OpenStreetMap intégré ✓ Swagger mis à jour ✓ Tests verts
Livrables
GeocodingModule GeocodingService AddressValidationService Address enrichie OpenStreetMap Integration Reverse Geocoding
Sprint 2-E.1 — Gestion des Préférences Utilisateur
Objectif
Implémenter la gestion complète des préférences utilisateur basée sur :
UserPreference
À l'issue de cette étape :
✓ PreferencesModule ✓ PreferencesController ✓ PreferencesService ✓ GET /preferences ✓ PUT /preferences ✓ Langue ✓ Devise ✓ Fuseau horaire ✓ Format date ✓ Thème
Architecture
Créer
src/modules/preferences
Structure
preferences ├── application │ │ └── dto │ │ ├── update-preferences.dto.ts │ └── preferences-response.dto.ts │ ├── domain │ │ └── services │ │ └── preferences.service.ts │ ├── presentation │ │ └── controllers │ │ └── preferences.controller.ts │ └── preferences.module.ts
Étape 1 — UpdatePreferencesDto
Créer
application/dto/update-preferences.dto.ts
Implémentation
import {
IsOptional,
IsString,
IsIn
} from 'class-validator';
export class UpdatePreferencesDto {
@IsOptional()
@IsString()
language?: string;
@IsOptional()
@IsString()
currency?: string;
@IsOptional()
@IsString()
timezone?: string;
@IsOptional()
@IsString()
dateFormat?: string;
@IsOptional()
@IsIn([
'light',
'dark',
'system'
])
theme?: string;
}
Étape 2 — PreferencesResponseDto
Créer
application/dto/preferences-response.dto.ts
Implémentation
export class PreferencesResponseDto {
language: string;
currency: string;
timezone: string;
dateFormat: string;
theme: string;
createdAt: Date;
updatedAt: Date;
}
Étape 3 — Validation Langues
Créer
shared/constants/languages.constants.ts
Ajouter
export const SUPPORTED_LANGUAGES = [ 'fr', 'en', 'es', 'de', 'it', 'pt', 'nl' ];
Étape 4 — Validation Devises
Créer
shared/constants/currencies.constants.ts
Ajouter
export const SUPPORTED_CURRENCIES = [ 'EUR', 'USD', 'GBP', 'CHF', 'CAD' ];
Étape 5 — Validation Fuseaux
Créer
shared/constants/timezones.constants.ts
Ajouter
export const SUPPORTED_TIMEZONES = [ 'Europe/Paris', 'Europe/London', 'Europe/Berlin', 'America/New_York', 'America/Montreal', 'Asia/Tokyo' ];
Étape 6 — Création PreferencesService
Créer
domain/services/preferences.service.ts
Injection
@Injectable()
export class PreferencesService {
constructor(
private readonly prisma:
PrismaService
) {}
}
Étape 7 — Méthodes
Ajouter
getPreferences() updatePreferences()
Étape 8 — Implémentation getPreferences
Ajouter
async getPreferences(
userId: string
) {
const preferences =
await this.prisma
.userPreference
.findUnique({
where: {
userId
}
});
if (!preferences) {
throw new NotFoundException(
'Preferences not found'
);
}
return preferences;
}
Étape 9 — Validation métier
Ajouter
private validatePreferences(
dto:
UpdatePreferencesDto
)
Langue
if (
dto.language &&
!SUPPORTED_LANGUAGES
.includes(
dto.language
)
) {
throw new BadRequestException(
'Unsupported language'
);
}
Devise
if (
dto.currency &&
!SUPPORTED_CURRENCIES
.includes(
dto.currency
)
) {
throw new BadRequestException(
'Unsupported currency'
);
}
Fuseau
if (
dto.timezone &&
!SUPPORTED_TIMEZONES
.includes(
dto.timezone
)
) {
throw new BadRequestException(
'Unsupported timezone'
);
}
Étape 10 — Implémentation updatePreferences
Ajouter
async updatePreferences(
userId: string,
dto:
UpdatePreferencesDto
)
Validation
this.validatePreferences( dto );
Mise à jour
await this.prisma
.userPreference
.update({
where: {
userId
},
data: {
language:
dto.language,
currency:
dto.currency,
timezone:
dto.timezone,
dateFormat:
dto.dateFormat,
theme:
dto.theme
}
});
Retour
return this.getPreferences( userId );
Étape 11 — Historisation
Préparer
Ajouter plus tard :
PreferenceHistory
afin de suivre :
Changements langue Changements devise Changements fuseau
Étape 12 — Création Controller
Créer
presentation/controllers/preferences.controller.ts
Déclaration
@ApiTags('Preferences')
@Controller(
'preferences'
)
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Étape 13 — GET /preferences
Route
@Get()
Implémentation
getPreferences(
@Req() req
) {
return this.preferencesService
.getPreferences(
req.user.id
);
}
Étape 14 — PUT /preferences
Route
@Put()
Implémentation
updatePreferences(
@Req() req,
@Body()
dto:
UpdatePreferencesDto
) {
return this.preferencesService
.updatePreferences(
req.user.id,
dto
);
}
Étape 15 — Swagger
GET
@ApiOperation({
summary:
'Get current user preferences'
})
PUT
@ApiOperation({
summary:
'Update current user preferences'
})
Réponse
@ApiOkResponse({
type:
PreferencesResponseDto
})
Étape 16 — PreferencesModule
Créer
preferences.module.ts
Implémentation
@Module({
imports: [
PrismaModule,
AuthModule
],
controllers: [
PreferencesController
],
providers: [
PreferencesService
],
exports: [
PreferencesService
]
})
export class PreferencesModule {}
Étape 17 — AppModule
Ajouter
imports: [ ... PreferencesModule ]
Étape 18 — Tests
GET
GET /preferences
Réponse
{
"language": "fr",
"currency": "EUR",
"timezone": "Europe/Paris",
"dateFormat": "DD/MM/YYYY",
"theme": "light"
}
PUT
PUT /preferences
Body
{
"language": "en",
"currency": "USD",
"timezone": "America/New_York",
"theme": "dark"
}
Résultat
Préférences mises à jour
Étape 19 — Préparation Internationalisation
Ces préférences seront utilisées plus tard par :
InternationalizationModule LocalizationModule MultiCurrencyModule NotificationModule ReportingModule
des Sprints :
15 20
Définition de terminé
Le Sprint 2-E.1 est terminé lorsque :
✓ PreferencesModule créé ✓ PreferencesController créé ✓ PreferencesService créé ✓ GET /preferences opérationnel ✓ PUT /preferences opérationnel ✓ Validation langue ✓ Validation devise ✓ Validation fuseau ✓ Swagger documenté ✓ Tests verts
Livrables
PreferencesModule PreferencesController PreferencesService UpdatePreferencesDto PreferencesResponseDto Validation Langues Validation Devises Validation Fuseaux
Sprint 2-E.2 — Préférences Enterprise & Internationalisation
Objectif
Faire évoluer le système de préférences utilisateur vers un moteur complet d'internationalisation.
À l'issue de cette étape :
✓ Locales ✓ Formats régionaux ✓ Formats monétaires ✓ Formats numériques ✓ Formats horaires ✓ Détection navigateur ✓ Préférences automatiques ✓ Compatibilité Sprint 15 ✓ Compatibilité Sprint 20
Architecture cible
Browser ↓ Locale Detection ↓ Preferences Service ↓ User Preferences ↓ Localization Engine ↓ UI Rendering
Sprint 2-E.2-A
Extension Prisma
Étape 1 — Évolution UserPreference
Ajouter
Dans :
model UserPreference
Nouveaux champs
locale String? @default("fr-FR")
numberFormat String? @default("fr-FR")
currencyFormat String? @default("fr-FR")
timeFormat String? @default("24H")
weekStartsOn Int? @default(1)
measurementSystem String? @default("METRIC")
browserLanguage String?
browserTimezone String?
autoDetectLocale Boolean @default(true)
autoDetectTimezone Boolean @default(true)
Étape 2 — Migration
Générer
npx prisma migrate dev \
--name preferences_internationalization
Générer
npx prisma generate
Sprint 2-E.2-B
DTO Enterprise
Étape 3 — UpdatePreferencesDto
Ajouter
locale?: string; numberFormat?: string; currencyFormat?: string; timeFormat?: string; weekStartsOn?: number; measurementSystem?: string; autoDetectLocale?: boolean; autoDetectTimezone?: boolean;
Étape 4 — Response DTO
Ajouter
locale: string; numberFormat: string; currencyFormat: string; timeFormat: string; weekStartsOn: number; measurementSystem: string; browserLanguage?: string; browserTimezone?: string; autoDetectLocale: boolean; autoDetectTimezone: boolean;
Sprint 2-E.2-C
Constantes Internationales
Étape 5 — Locales supportées
Créer
shared/constants/locales.constants.ts
Ajouter
export const SUPPORTED_LOCALES = [ 'fr-FR', 'en-US', 'en-GB', 'de-DE', 'es-ES', 'it-IT', 'pt-PT', 'nl-NL' ];
Étape 6 — Formats horaires
Ajouter
export const TIME_FORMATS = [ '12H', '24H' ];
Étape 7 — Systèmes de mesure
Ajouter
export const MEASUREMENT_SYSTEMS = [ 'METRIC', 'IMPERIAL' ];
Sprint 2-E.2-D
Détection Automatique
Étape 8 — Création LocaleDetectionService
Créer
shared/services locale-detection.service.ts
Méthodes
detectLanguage() detectTimezone() detectLocale()
Étape 9 — Détection Langue
Header
Accept-Language
Exemple
detectLanguage(
request: Request
) {
return request.headers[
'accept-language'
];
}
Étape 10 — Détection Fuseau
Header Front
X-Timezone
Exemple
Europe/Paris America/New_York Asia/Tokyo
Étape 11 — Détection Locale
Exemple
fr-FR ↓ language = fr currency = EUR timezone = Europe/Paris numberFormat = fr-FR
Sprint 2-E.2-E
Validation Enterprise
Étape 12 — Validation Locale
Ajouter
if (
!SUPPORTED_LOCALES.includes(
dto.locale
)
) {
throw new BadRequestException(
'Unsupported locale'
);
}
Étape 13 — Validation Time Format
Ajouter
if (
!TIME_FORMATS.includes(
dto.timeFormat
)
) {
throw new BadRequestException(
'Invalid time format'
);
}
Étape 14 — Validation Measurement
Ajouter
if (
!MEASUREMENT_SYSTEMS.includes(
dto.measurementSystem
)
) {
throw new BadRequestException(
'Invalid measurement system'
);
}
Sprint 2-E.2-F
Initialisation Automatique
Étape 15 — Premier Login
Workflow
User Login ↓ No Preferences ↓ Detect Browser ↓ Apply Defaults ↓ Persist Preferences
Étape 16 — Méthode
Ajouter
initializePreferencesFromBrowser( request )
Sauvegarder
browserLanguage browserTimezone locale
Sprint 2-E.2-G
Formatters
Étape 17 — Date Formatter
Créer
shared/services/date-format.service.ts
Méthode
formatDate( date, preferences )
Exemple
fr-FR ↓ 31/12/2026
Exemple
en-US ↓ 12/31/2026
Étape 18 — Currency Formatter
Créer
shared/services/currency-format.service.ts
Exemple
1500.50 ↓ 1 500,50 €
Exemple
1500.50 ↓ $1,500.50
Étape 19 — Number Formatter
Créer
shared/services/number-format.service.ts
Exemple
1000000 ↓ 1 000 000
Exemple
1000000 ↓ 1,000,000
Sprint 2-E.2-H
Préparation Multi-Currency
Étape 20 — Mapping devises
Créer
shared/constants currency-locales.ts
Exemple
export const DEFAULT_CURRENCY_BY_LOCALE = {
'fr-FR': 'EUR',
'en-US': 'USD',
'en-GB': 'GBP',
'de-DE': 'EUR'
};
Étape 21 — Auto-provisionnement
Lors du Register
Créer :
locale currency timezone
à partir :
Browser Locale
si disponible.
Sprint 2-E.2-I
Swagger
Étape 22 — Documentation
GET
GET /preferences
Réponse
{
"language": "fr",
"locale": "fr-FR",
"currency": "EUR",
"timezone": "Europe/Paris",
"numberFormat": "fr-FR",
"currencyFormat": "fr-FR",
"timeFormat": "24H",
"measurementSystem": "METRIC"
}
Étape 23 — Mise à jour
Exemple
PUT /preferences
Body
{
"locale": "en-US",
"currency": "USD",
"timezone": "America/New_York",
"timeFormat": "12H",
"measurementSystem": "IMPERIAL"
}
Sprint 2-E.2-J
Préparation Sprint 15
Ces préférences alimenteront
Country Currency Language Timezone Translation Localization Multi-Site Multi-Régions
Préparation Sprint 20
Elles seront utilisées par :
InternationalizationModule LocalizationModule MultiCurrencyModule EnterpriseReleaseModule
Définition de terminé
Le Sprint 2-E.2 est terminé lorsque :
✓ Locales supportées ✓ Détection navigateur ✓ Formats régionaux ✓ Formats monétaires ✓ Formats numériques ✓ Formats horaires ✓ Préférences automatiques ✓ Swagger documenté ✓ Compatible Sprint 15 ✓ Compatible Sprint 20
Livrables
LocaleDetectionService DateFormatService CurrencyFormatService NumberFormatService UserPreference Enterprise Détection automatique navigateur Support internationalisation
Sprint 2-F.1 — Gestion des Notifications Utilisateur
Objectif
Implémenter la gestion complète des préférences de notifications utilisateur basée sur :
NotificationSetting
À l'issue de cette étape :
✓ NotificationSettingsModule ✓ NotificationSettingsController ✓ NotificationSettingsService ✓ GET /notifications/settings ✓ PUT /notifications/settings ✓ Notifications Email ✓ Notifications SMS ✓ Notifications Push ✓ Préférences Marketing ✓ Swagger
Architecture
Créer
src/modules/notification-settings
Structure
notification-settings ├── application │ │ └── dto │ │ ├── update-notification-settings.dto.ts │ └── notification-settings-response.dto.ts │ ├── domain │ │ └── services │ │ └── notification-settings.service.ts │ ├── presentation │ │ └── controllers │ │ └── notification-settings.controller.ts │ └── notification-settings.module.ts
Étape 1 — Vérification du modèle Prisma
Vérifier
model NotificationSetting
Version minimale
model NotificationSetting {
id String @id @default(uuid())
userId String @unique
emailEnabled Boolean @default(true)
smsEnabled Boolean @default(false)
pushEnabled Boolean @default(true)
marketingEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
@relation(
fields:[userId],
references:[id],
onDelete: Cascade
)
}
Étape 2 — Extension Enterprise
Ajouter
reservationEmails Boolean @default(true) paymentEmails Boolean @default(true) marketingEmails Boolean @default(false) systemEmails Boolean @default(true) reservationSms Boolean @default(false) paymentSms Boolean @default(false) securitySms Boolean @default(true) pushReservations Boolean @default(true) pushPayments Boolean @default(true) pushMarketing Boolean @default(false)
Étape 3 — Migration
Générer
npx prisma migrate dev \
--name notification_settings_enterprise
Générer
npx prisma generate
Étape 4 — DTO de mise à jour
Créer
application/dto/update-notification-settings.dto.ts
Implémentation
import {
IsOptional,
IsBoolean
} from 'class-validator';
export class UpdateNotificationSettingsDto {
@IsOptional()
@IsBoolean()
emailEnabled?: boolean;
@IsOptional()
@IsBoolean()
smsEnabled?: boolean;
@IsOptional()
@IsBoolean()
pushEnabled?: boolean;
@IsOptional()
@IsBoolean()
marketingEnabled?: boolean;
@IsOptional()
@IsBoolean()
reservationEmails?: boolean;
@IsOptional()
@IsBoolean()
paymentEmails?: boolean;
@IsOptional()
@IsBoolean()
marketingEmails?: boolean;
@IsOptional()
@IsBoolean()
systemEmails?: boolean;
@IsOptional()
@IsBoolean()
reservationSms?: boolean;
@IsOptional()
@IsBoolean()
paymentSms?: boolean;
@IsOptional()
@IsBoolean()
securitySms?: boolean;
@IsOptional()
@IsBoolean()
pushReservations?: boolean;
@IsOptional()
@IsBoolean()
pushPayments?: boolean;
@IsOptional()
@IsBoolean()
pushMarketing?: boolean;
}
Étape 5 — DTO de réponse
Créer
application/dto/notification-settings-response.dto.ts
Implémentation
export class NotificationSettingsResponseDto {
emailEnabled: boolean;
smsEnabled: boolean;
pushEnabled: boolean;
marketingEnabled: boolean;
reservationEmails: boolean;
paymentEmails: boolean;
marketingEmails: boolean;
systemEmails: boolean;
reservationSms: boolean;
paymentSms: boolean;
securitySms: boolean;
pushReservations: boolean;
pushPayments: boolean;
pushMarketing: boolean;
createdAt: Date;
updatedAt: Date;
}
Étape 6 — Création du service
Créer
domain/services/notification-settings.service.ts
Injection
@Injectable()
export class NotificationSettingsService {
constructor(
private readonly prisma:
PrismaService
) {}
}
Étape 7 — Méthodes
Ajouter
getSettings() updateSettings()
Étape 8 — Implémentation getSettings
Ajouter
async getSettings(
userId: string
) {
const settings =
await this.prisma
.notificationSetting
.findUnique({
where: {
userId
}
});
if (!settings) {
throw new NotFoundException(
'Notification settings not found'
);
}
return settings;
}
Étape 9 — Implémentation updateSettings
Ajouter
async updateSettings(
userId: string,
dto:
UpdateNotificationSettingsDto
) {
await this.prisma
.notificationSetting
.update({
where: {
userId
},
data: dto
});
return this.getSettings(
userId
);
}
Étape 10 — Contrôleur
Créer
presentation/controllers notification-settings.controller.ts
Déclaration
@ApiTags('Notification Settings')
@Controller(
'notifications/settings'
)
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Étape 11 — GET Endpoint
Route
@Get()
Implémentation
getSettings(
@Req() req
) {
return this
.notificationSettingsService
.getSettings(
req.user.id
);
}
Étape 12 — PUT Endpoint
Route
@Put()
Implémentation
updateSettings(
@Req() req,
@Body()
dto:
UpdateNotificationSettingsDto
) {
return this
.notificationSettingsService
.updateSettings(
req.user.id,
dto
);
}
Étape 13 — Module
Créer
notification-settings.module.ts
Implémentation
@Module({
imports: [
PrismaModule,
AuthModule
],
controllers: [
NotificationSettingsController
],
providers: [
NotificationSettingsService
],
exports: [
NotificationSettingsService
]
})
export class NotificationSettingsModule {}
Étape 14 — AppModule
Ajouter
imports: [ ... NotificationSettingsModule ]
Étape 15 — Swagger
Vérifier
Les endpoints suivants apparaissent :
GET /notifications/settings PUT /notifications/settings
Étape 16 — Test GET
Requête
GET /notifications/settings Authorization: Bearer xxx
Réponse
{
"emailEnabled": true,
"smsEnabled": false,
"pushEnabled": true,
"marketingEnabled": false,
"reservationEmails": true,
"paymentEmails": true,
"systemEmails": true
}
Étape 17 — Test PUT
Requête
PUT /notifications/settings
Body
{
"marketingEnabled": true,
"marketingEmails": true,
"pushMarketing": true
}
Résultat
Préférences enregistrées
Étape 18 — Préparation RGPD
Prévoir
Les champs :
marketingEnabled marketingEmails pushMarketing
seront utilisés par :
Consent MarketingCampaign CommunicationModule
afin de respecter :
RGPD Opt-in Opt-out
Étape 19 — Préparation Sprint 9
Ces paramètres seront exploités par :
EmailModule SmsModule PushModule CampaignModule
du Sprint 9.
Définition de terminé
Le Sprint 2-F.1 est terminé lorsque :
✓ NotificationSettingsModule créé ✓ NotificationSettingsController créé ✓ NotificationSettingsService créé ✓ GET opérationnel ✓ PUT opérationnel ✓ Préférences Email ✓ Préférences SMS ✓ Préférences Push ✓ Préférences Marketing ✓ Swagger documenté
Livrables
NotificationSettingsModule NotificationSettingsController NotificationSettingsService UpdateNotificationSettingsDto NotificationSettingsResponseDto
Sprint 2-F.2 — Notifications Enterprise & Centre de Préférences
Objectif
Transformer le système de notifications en centre de préférences Enterprise complet.
À l'issue de cette étape :
✓ Multi-canaux ✓ Fréquence configurable ✓ Digest quotidien ✓ Digest hebdomadaire ✓ Silence Hours ✓ Catégories ✓ Consentements RGPD ✓ Préférences avancées ✓ Compatible Sprint 9 ✓ Compatible Sprint 19
Architecture cible
Notification Event ↓ Preference Center ↓ Consent Validation ↓ Channel Selection ↓ Delivery Rules ↓ Email SMS Push Webhook
Sprint 2-F.2-A
Extension Prisma
Étape 1 — Évolution NotificationSetting
Ajouter
Dans :
model NotificationSetting
Fréquence
notificationFrequency String
@default("REALTIME")
Digest
dailyDigestEnabled Boolean
@default(false)
weeklyDigestEnabled Boolean
@default(false)
Silence Hours
quietHoursEnabled Boolean
@default(false)
quietHoursStart String?
quietHoursEnd String?
Préférences Mobile
mobilePushEnabled Boolean
@default(true)
desktopPushEnabled Boolean
@default(true)
Consentements
gdprConsentGiven Boolean
@default(false)
gdprConsentAt DateTime?
marketingConsentGiven Boolean
@default(false)
marketingConsentAt DateTime?
Étape 2 — Création NotificationCategory
Ajouter
model NotificationCategory {
id String
@id
@default(uuid())
code String
@unique
name String
description String?
createdAt DateTime
@default(now())
updatedAt DateTime
@updatedAt
preferences NotificationCategoryPreference[]
}
Étape 3 — Préférences par catégorie
Ajouter
model NotificationCategoryPreference {
id String
@id
@default(uuid())
notificationSettingId String
categoryId String
emailEnabled Boolean
@default(true)
smsEnabled Boolean
@default(false)
pushEnabled Boolean
@default(true)
notificationSetting NotificationSetting
@relation(
fields:[notificationSettingId],
references:[id],
onDelete:Cascade
)
category NotificationCategory
@relation(
fields:[categoryId],
references:[id],
onDelete:Cascade
)
@@unique([
notificationSettingId,
categoryId
])
}
Étape 4 — Relations
Ajouter
Dans :
model NotificationSetting
categoryPreferences NotificationCategoryPreference[]
Étape 5 — Migration
Générer
npx prisma migrate dev \
--name notification_preferences_enterprise
Générer
npx prisma generate
Sprint 2-F.2-B
Constantes
Étape 6 — Fréquences
Créer
shared/constants notification-frequency.constants.ts
Ajouter
export const NOTIFICATION_FREQUENCIES = [ 'REALTIME', 'HOURLY', 'DAILY', 'WEEKLY' ];
Étape 7 — Catégories
Créer
shared/constants notification-categories.constants.ts
Ajouter
export const NOTIFICATION_CATEGORIES = [ 'RESERVATION', 'PAYMENT', 'SECURITY', 'SYSTEM', 'MARKETING', 'CRM', 'PROPERTY', 'OWNER', 'CHANNEL_MANAGER' ];
Sprint 2-F.2-C
DTO Enterprise
Étape 8 — Extension DTO
Ajouter
Dans :
UpdateNotificationSettingsDto
Nouveaux champs
notificationFrequency?: string; dailyDigestEnabled?: boolean; weeklyDigestEnabled?: boolean; quietHoursEnabled?: boolean; quietHoursStart?: string; quietHoursEnd?: string; mobilePushEnabled?: boolean; desktopPushEnabled?: boolean; gdprConsentGiven?: boolean; marketingConsentGiven?: boolean;
Étape 9 — DTO Catégories
Créer
update-notification-category.dto.ts
Implémentation
export class UpdateNotificationCategoryDto {
categoryCode: string;
emailEnabled: boolean;
smsEnabled: boolean;
pushEnabled: boolean;
}
Sprint 2-F.2-D
Service Enterprise
Étape 10 — Validation fréquence
Ajouter
if (
!NOTIFICATION_FREQUENCIES.includes(
dto.notificationFrequency
)
) {
throw new BadRequestException(
'Invalid notification frequency'
);
}
Étape 11 — Validation Silence Hours
Ajouter
if (
dto.quietHoursEnabled &&
(!dto.quietHoursStart ||
!dto.quietHoursEnd)
) {
throw new BadRequestException(
'Quiet hours invalid'
);
}
Étape 12 — Consentement RGPD
Ajouter
if (
dto.gdprConsentGiven ===
true
) {
data.gdprConsentAt =
new Date();
}
Marketing
if (
dto.marketingConsentGiven ===
true
) {
data.marketingConsentAt =
new Date();
}
Sprint 2-F.2-E
Centre de Préférences
Étape 13 — Endpoint Catégories
Ajouter
GET /notifications/categories
Retour
[
{
"code":"PAYMENT",
"emailEnabled":true,
"smsEnabled":false,
"pushEnabled":true
}
]
Étape 14 — Mise à jour catégorie
Ajouter
PUT /notifications/categories
Body
{
"categoryCode":"PAYMENT",
"emailEnabled":true,
"smsEnabled":true,
"pushEnabled":true
}
Étape 15 — Endpoint Consentement
Ajouter
POST /notifications/consents
Usage
RGPD Marketing Partenaires Newsletter
Sprint 2-F.2-F
Seed
Étape 16 — Création catégories
Ajouter
Dans :
prisma/seed.ts
Créer
RESERVATION PAYMENT SECURITY SYSTEM MARKETING CRM PROPERTY
Étape 17 — Préférences par défaut
Lors du Register
Créer automatiquement :
NotificationCategoryPreference
pour chaque catégorie.
Sprint 2-F.2-G
Moteur de Livraison
Étape 18 — Service futur
Préparer
NotificationPreferenceResolver
Rôle
Déterminer :
Canal ↓ Autorisé ? ↓ Consentement ? ↓ Silence Hours ? ↓ Digest ? ↓ Envoi
Étape 19 — Exemple
Paiement reçu
PAYMENT ↓ Email ↓ Push ↓ Pas SMS
selon les préférences.
Sprint 2-F.2-H
Swagger
Étape 20 — Nouveaux endpoints
GET /notifications/settings PUT /notifications/settings GET /notifications/categories PUT /notifications/categories POST /notifications/consents
Sprint 2-F.2-I
Préparation Sprint 9
Compatible
EmailModule SmsModule PushModule CampaignModule MarketingModule
Préparation Sprint 19
Compatible :
Consent Audit Compliance RGPD RetentionPolicy
Définition de terminé
Le Sprint 2-F.2 est terminé lorsque :
✓ Fréquences configurables ✓ Daily Digest ✓ Weekly Digest ✓ Quiet Hours ✓ Notification Categories ✓ Consentements RGPD ✓ Centre de préférences ✓ Seed enrichi ✓ Swagger documenté
Livrables
NotificationCategory NotificationCategoryPreference Notification Preferences Center GDPR Consent Management Digest Configuration Quiet Hours NotificationPreferenceResolver
Sprint 2-G.1 — Gestion des Sessions Utilisateur
Objectif
Implémenter la gestion complète des sessions utilisateur basée sur :
Session ConnectionHistory
À l'issue de cette étape :
✓ UserSessionModule ✓ UserSessionController ✓ UserSessionService ✓ Sessions actives ✓ Déconnexion appareil ✓ Déconnexion globale ✓ Historique sessions ✓ Sécurité multi-appareils ✓ Swagger
Architecture
Créer
src/modules/user-sessions
Structure
user-sessions ├── application │ │ └── dto │ │ ├── session-response.dto.ts │ └── revoke-session.dto.ts │ ├── domain │ │ └── services │ │ └── user-session.service.ts │ ├── presentation │ │ └── controllers │ │ └── user-session.controller.ts │ └── user-session.module.ts
Étape 1 — Vérification du modèle Session
Vérifier
model Session
Champs requis
id String userId String sessionToken String? ipAddress String? userAgent String? deviceName String? platform String? isCurrent Boolean lastSeenAt DateTime? revokedAt DateTime? createdAt DateTime
Étape 2 — DTO de réponse
Créer
application/dto/session-response.dto.ts
Implémentation
export class SessionResponseDto {
id: string;
deviceName?: string;
platform?: string;
ipAddress?: string;
userAgent?: string;
isCurrent: boolean;
lastSeenAt?: Date;
createdAt: Date;
revokedAt?: Date;
}
Étape 3 — DTO de révocation
Créer
application/dto/revoke-session.dto.ts
Implémentation
export class RevokeSessionDto {
sessionId: string;
}
Étape 4 — Création du service
Créer
domain/services/user-session.service.ts
Injection
@Injectable()
export class UserSessionService {
constructor(
private readonly prisma:
PrismaService
) {}
}
Étape 5 — Méthodes
Ajouter
getSessions() revokeSession() revokeAllSessions() getConnectionHistory()
Étape 6 — Sessions actives
Implémentation
async getSessions(
userId: string
) {
return this.prisma.session.findMany({
where: {
userId,
revokedAt: null
},
orderBy: {
lastSeenAt: 'desc'
}
});
}
Étape 7 — Révocation d'une session
Signature
async revokeSession( userId: string, sessionId: string )
Vérification
const session =
await this.prisma.session.findFirst({
where: {
id: sessionId,
userId
}
});
Sécurité
if (!session) {
throw new NotFoundException(
'Session not found'
);
}
Révocation
await this.prisma.session.update({
where: {
id: sessionId
},
data: {
revokedAt:
new Date(),
isCurrent:
false
}
});
Étape 8 — Révocation Refresh Tokens
Ajouter
await this.prisma.refreshToken.updateMany({
where: {
userId,
revokedAt: null
},
data: {
revokedAt:
new Date()
}
});
Remarque
Dans une future évolution, les refresh tokens devront être liés à :
Session
pour une révocation ciblée.
Étape 9 — Déconnexion globale
Signature
async revokeAllSessions( userId: string )
Implémentation
await this.prisma.session.updateMany({
where: {
userId,
revokedAt: null
},
data: {
revokedAt:
new Date(),
isCurrent:
false
}
});
Révocation tokens
await this.prisma.refreshToken.updateMany({
where: {
userId,
revokedAt: null
},
data: {
revokedAt:
new Date()
}
});
Étape 10 — Historique connexions
Implémentation
async getConnectionHistory(
userId: string
) {
return this.prisma
.connectionHistory
.findMany({
where: {
userId
},
orderBy: {
connectedAt:
'desc'
},
take: 100
});
}
Étape 11 — Controller
Créer
presentation/controllers user-session.controller.ts
Déclaration
@ApiTags('Sessions')
@Controller('sessions')
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Étape 12 — GET /sessions
Route
@Get()
Implémentation
getSessions(
@Req() req
) {
return this
.userSessionService
.getSessions(
req.user.id
);
}
Étape 13 — DELETE /sessions/{id}
Route
@Delete(':id')
Implémentation
revokeSession(
@Req() req,
@Param('id')
id: string
) {
return this
.userSessionService
.revokeSession(
req.user.id,
id
);
}
Étape 14 — DELETE /sessions
Route
@Delete()
Implémentation
revokeAllSessions(
@Req() req
) {
return this
.userSessionService
.revokeAllSessions(
req.user.id
);
}
Étape 15 — Historique
Ajouter
GET /sessions/history
Implémentation
@Get('history')
getHistory(
@Req() req
) {
return this
.userSessionService
.getConnectionHistory(
req.user.id
);
}
Étape 16 — Mise à jour Login
Lors du login
Créer :
ConnectionHistory
Ajouter
await prisma.connectionHistory.create({
data: {
userId: user.id,
ipAddress:
session.ipAddress,
userAgent:
session.userAgent,
connectedAt:
new Date(),
success: true
}
});
Étape 17 — Mise à jour Logout
Lors du logout
Ajouter :
disconnectedAt: new Date()
dans :
ConnectionHistory
Étape 18 — Détection Appareil
Créer
shared/services device-detection.service.ts
Extraire
Depuis :
User-Agent
Retour
Chrome Windows Safari MacOS Chrome Android Safari iPhone
Étape 19 — UserSessionModule
Créer
user-session.module.ts
Implémentation
@Module({
imports: [
PrismaModule,
AuthModule
],
controllers: [
UserSessionController
],
providers: [
UserSessionService
],
exports: [
UserSessionService
]
})
export class UserSessionModule {}
Étape 20 — Swagger
Vérifier
GET /sessions
GET /sessions/history
DELETE /sessions/{id}
DELETE /sessions
Étape 21 — Tests
Cas 1
Connexion sur :
Chrome
et :
Mobile
Résultat
2 sessions visibles
Cas 2
DELETE :
/sessions/{id}
Résultat
Session révoquée
Cas 3
DELETE :
/sessions
Résultat
Toutes les sessions révoquées
Étape 22 — Préparation Sprint 19
Les informations collectées seront utilisées par :
SecurityIncident Risk AuditLog ComplianceAudit
pour :
Détection fraude Analyse risques Historique sécurité
Définition de terminé
Le Sprint 2-G.1 est terminé lorsque :
✓ Sessions actives ✓ Historique connexions ✓ Déconnexion appareil ✓ Déconnexion globale ✓ Device detection ✓ Swagger documenté ✓ Tests verts
Livrables
UserSessionModule UserSessionController UserSessionService Session Management API Connection History API Device Detection Service
Sprint 2-G.2 — Sessions Enterprise & Sécurité Avancée
Objectif
Transformer le système de sessions en plateforme de sécurité Enterprise.
À l'issue de cette étape :
✓ Session Risk Score ✓ Trusted Devices ✓ Validation 2FA ✓ Détection anomalies ✓ Détection géographique ✓ Alertes sécurité ✓ Session Policies ✓ Préparation Sprint 19
Architecture cible
Login ↓ Security Engine ↓ Risk Analysis ↓ Geo Analysis ↓ Device Analysis ↓ Policy Evaluation ↓ Allow / Challenge / Block
Sprint 2-G.2-A
Extension Prisma
Étape 1 — Création TrustedDevice
Ajouter
model TrustedDevice {
id String
@id
@default(uuid())
userId String
deviceFingerprint String
deviceName String?
platform String?
browser String?
lastIpAddress String?
lastCountry String?
trustedAt DateTime
@default(now())
lastSeenAt DateTime?
isActive Boolean
@default(true)
createdAt DateTime
@default(now())
updatedAt DateTime
@updatedAt
user User
@relation(
fields:[userId],
references:[id],
onDelete:Cascade
)
@@index([userId])
@@unique([
userId,
deviceFingerprint
])
}
Étape 2 — Création SecurityAlert
Ajouter
model SecurityAlert {
id String
@id
@default(uuid())
userId String
sessionId String?
severity String
alertType String
title String
description String?
resolved Boolean
@default(false)
resolvedAt DateTime?
metadata Json?
createdAt DateTime
@default(now())
user User
@relation(
fields:[userId],
references:[id],
onDelete:Cascade
)
@@index([userId])
@@index([severity])
@@index([alertType])
@@index([resolved])
}
Étape 3 — Extension Session
Ajouter
Dans :
model Session
Nouveaux champs
deviceFingerprint String?
riskScore Int
@default(0)
riskLevel String?
@default("LOW")
country String?
city String?
latitude Decimal?
@db.Decimal(10,7)
longitude Decimal?
@db.Decimal(10,7)
isTrustedDevice Boolean
@default(false)
requires2fa Boolean
@default(false)
securityFlags Json?
lastActivityAt DateTime?
Étape 4 — Relations User
Ajouter
Dans :
model User
trustedDevices TrustedDevice[] securityAlerts SecurityAlert[]
Étape 5 — Migration
Générer
npx prisma migrate dev \
--name session_security_enterprise
Générer
npx prisma generate
Sprint 2-G.2-B
Session Risk Engine
Étape 6 — Création SecurityModule
src/modules/security
Structure
security ├── domain │ │ └── services │ │ ├── risk-engine.service.ts │ ├── geo-security.service.ts │ ├── trusted-device.service.ts │ └── security-alert.service.ts │ └── security.module.ts
Étape 7 — RiskEngineService
Créer
risk-engine.service.ts
Méthode
calculateRiskScore( context )
Étape 8 — Critères
Nouveau pays
+30 points
Nouveau device
+20 points
Nouvelle IP
+15 points
Heure inhabituelle
+10 points
VPN détecté
+25 points
TOR détecté
+50 points
Étape 9 — Classification
0 - 29 LOW
30 - 59 MEDIUM
60+ HIGH
Sprint 2-G.2-C
Trusted Devices
Étape 10 — TrustedDeviceService
Méthodes
isTrusted() registerTrustedDevice() revokeTrustedDevice() getTrustedDevices()
Étape 11 — Fingerprint
Construire
à partir de :
User-Agent Platform Browser Screen Resolution Timezone
Hash
sha256( fingerprint )
Étape 12 — Login Workflow
Login ↓ Trusted Device ? ↓ Yes ↓ Risk Reduced
Sinon
New Device ↓ Risk Increased
Sprint 2-G.2-D
Validation 2FA
Étape 13 — Extension Session
Règle
Risk HIGH ↓ 2FA obligatoire
Exemple
if (
riskScore >= 60
) {
requires2fa = true;
}
Étape 14 — Préparation future
Compatible avec :
TOTP SMS OTP Email OTP Authenticator App
Sprint 2-G.2-E
Détection Géographique
Étape 15 — GeoSecurityService
Méthodes
resolveLocation() detectImpossibleTravel() detectCountryChange()
Étape 16 — Impossible Travel
Exemple
Paris ↓ 5 minutes ↓ New York
Résultat
HIGH RISK
Étape 17 — Country Change
Exemple
France ↓ Russie
Action
Security Alert
Sprint 2-G.2-F
Alertes Sécurité
Étape 18 — SecurityAlertService
Méthodes
createAlert() resolveAlert() listAlerts()
Étape 19 — Types
NEW_DEVICE IMPOSSIBLE_TRAVEL HIGH_RISK_LOGIN COUNTRY_CHANGE MULTIPLE_FAILED_LOGINS SESSION_HIJACK_ATTEMPT
Étape 20 — Sévérités
LOW MEDIUM HIGH CRITICAL
Sprint 2-G.2-G
Session Policies
Étape 21 — Création SessionPolicy
Ajouter
model SessionPolicy {
id String
@id
@default(uuid())
tenantId String
name String
maxSessionsPerUser Int
@default(10)
sessionDurationMin Int
@default(1440)
require2fa Boolean
@default(false)
allowNewDevices Boolean
@default(true)
allowForeignCountries Boolean
@default(true)
createdAt DateTime
@default(now())
updatedAt DateTime
@updatedAt
tenant Tenant
@relation(
fields:[tenantId],
references:[id]
)
}
Étape 22 — Application Policy
Workflow
Login ↓ Load Policy ↓ Validate Rules ↓ Create Session
Étape 23 — Exemples
Enterprise
Max Sessions = 3 Require 2FA = true Foreign Countries = false
PME
Max Sessions = 10 Require 2FA = false
Sprint 2-G.2-H
API Sécurité
Étape 24 — Endpoints
GET /sessions/trusted-devices
DELETE /sessions/trusted-devices/{id}
GET /security/alerts
PUT /security/alerts/{id}/resolve
Étape 25 — Swagger
Ajouter
Trusted Devices Security Alerts Session Policies
Sprint 2-G.2-I
Préparation Sprint 19
Ces données alimenteront
SecurityIncident Risk ComplianceAudit SecurityPolicy AuditLog
Préparation Sprint 20
Compatible :
Enterprise Security SOC2 ISO27001 Zero Trust Risk Management
Définition de terminé
Le Sprint 2-G.2 est terminé lorsque :
✓ Risk Score ✓ Trusted Devices ✓ Détection géographique ✓ Alertes sécurité ✓ Validation 2FA ✓ Session Policies ✓ Swagger documenté ✓ Compatible Sprint 19
Livrables
TrustedDevice SecurityAlert SessionPolicy RiskEngineService GeoSecurityService TrustedDeviceService SecurityAlertService
Sprint 2-H.1 — Historique de Connexion & Audit Utilisateur
Objectif
Mettre en place un système complet d'analyse des connexions et de traçabilité utilisateur.
À l'issue de cette étape :
✓ Historique des connexions ✓ Historique des échecs ✓ Analyse sécurité ✓ Audit utilisateur ✓ Statistiques ✓ Détection comportements anormaux ✓ Préparation Sprint 19
Architecture cible
Authentication ↓ ConnectionHistory ↓ Security Analysis ↓ Audit Engine ↓ Statistics Engine ↓ Monitoring Dashboard
Sprint 2-H.1-A
Évolution Prisma
Étape 1 — Extension ConnectionHistory
Vérifier
model ConnectionHistory
Ajouter
sessionId String?
tenantId String?
failureReason String?
authenticationType String?
riskScore Int?
riskLevel String?
country String?
region String?
city String?
latitude Decimal?
@db.Decimal(10,7)
longitude Decimal?
@db.Decimal(10,7)
deviceFingerprint String?
browser String?
platform String?
connectionDuration Int?
metadata Json?
Étape 2 — Relations
Ajouter
Dans :
model ConnectionHistory
session Session?
@relation(
fields:[sessionId],
references:[id]
)
Étape 3 — Index Performance
Ajouter
@@index([userId]) @@index([connectedAt]) @@index([success]) @@index([country]) @@index([riskLevel]) @@index([tenantId])
Étape 4 — Migration
Générer
npx prisma migrate dev \
--name connection_history_enterprise
Générer
npx prisma generate
Sprint 2-H.1-B
Module
Étape 5 — Création
src/modules/connection-history ├── application │ │ └── dto │ │ ├── connection-query.dto.ts │ ├── security-analysis.dto.ts │ └── connection-statistics.dto.ts │ ├── domain │ │ └── services │ │ └── connection-history.service.ts │ ├── presentation │ │ └── controllers │ │ └── connection-history.controller.ts │ └── connection-history.module.ts
Étape 6 — DTO Recherche
Créer
connection-query.dto.ts
Ajouter
export class ConnectionQueryDto {
page?: number;
limit?: number;
success?: boolean;
country?: string;
riskLevel?: string;
from?: Date;
to?: Date;
}
Sprint 2-H.1-C
Service
Étape 7 — Création
connection-history.service.ts
Injection
@Injectable()
export class ConnectionHistoryService {
constructor(
private readonly prisma:
PrismaService
) {}
}
Étape 8 — Méthodes
Ajouter
getConnections() getSecurityAnalysis() getStatistics() getFailedConnections()
Étape 9 — Historique
Implémentation
async getConnections(
userId: string,
query:
ConnectionQueryDto
)
Requête
return this.prisma
.connectionHistory
.findMany({
where: {
userId
},
orderBy: {
connectedAt:
'desc'
},
take:
query.limit ?? 50
});
Étape 10 — Historique échecs
Ajouter
async getFailedConnections( userId: string )
Requête
return this.prisma
.connectionHistory
.findMany({
where: {
userId,
success: false
},
orderBy: {
connectedAt:
'desc'
}
});
Sprint 2-H.1-D
Analyse Sécurité
Étape 11 — DTO
Créer
security-analysis.dto.ts
Structure
export class SecurityAnalysisDto {
totalConnections: number;
failedConnections: number;
uniqueCountries: number;
uniqueDevices: number;
highRiskConnections: number;
lastLoginAt?: Date;
}
Étape 12 — Analyse
Implémentation
async getSecurityAnalysis( userId: string )
Calcul
const totalConnections =
await this.prisma
.connectionHistory
.count({
where: {
userId
}
});
Échecs
const failedConnections =
await this.prisma
.connectionHistory
.count({
where: {
userId,
success: false
}
});
Risque élevé
const highRiskConnections =
await this.prisma
.connectionHistory
.count({
where: {
userId,
riskLevel:
'HIGH'
}
});
Étape 13 — Appareils
Calcul
const devices =
await this.prisma
.connectionHistory
.findMany({
where: {
userId
},
distinct: [
'deviceFingerprint'
]
});
Retour
uniqueDevices: devices.length
Sprint 2-H.1-E
Statistiques
Étape 14 — DTO
Créer
connection-statistics.dto.ts
Ajouter
export class ConnectionStatisticsDto {
dailyConnections: number;
weeklyConnections: number;
monthlyConnections: number;
successRate: number;
averageRiskScore: number;
}
Étape 15 — Statistiques
Méthode
async getStatistics( userId: string )
Calculs
Connexions jour Connexions semaine Connexions mois Taux succès Score moyen
Sprint 2-H.1-F
Audit Utilisateur
Étape 16 — AuditLog
Préparer
Le modèle :
AuditLog
a été défini lors de :
Phase 2-I
Événements
LOGIN_SUCCESS LOGIN_FAILED LOGOUT SESSION_REVOKED PASSWORD_CHANGED 2FA_ENABLED 2FA_DISABLED
Étape 17 — AuditService
Créer interface
shared/interfaces audit-event.interface.ts
Structure
export interface AuditEvent {
userId: string;
action: string;
entityType: string;
entityId?: string;
metadata?: Record<
string,
unknown
>;
}
Sprint 2-H.1-G
Controller
Étape 18 — Création
connection-history.controller.ts
Déclaration
@ApiTags('Connections')
@Controller(
'connections'
)
@UseGuards(
JwtAuthGuard
)
@ApiBearerAuth()
Étape 19 — GET /connections
Route
@Get()
Retour
Historique complet
Étape 20 — GET /connections/security
Route
@Get('security')
Retour
Analyse sécurité
Étape 21 — GET /connections/statistics
Route
@Get('statistics')
Retour
Statistiques
Étape 22 — GET /connections/failed
Route
@Get('failed')
Retour
Historique échecs
Sprint 2-H.1-H
Module
Étape 23 — Création
connection-history.module.ts
Implémentation
@Module({
imports: [
PrismaModule,
AuthModule
],
controllers: [
ConnectionHistoryController
],
providers: [
ConnectionHistoryService
],
exports: [
ConnectionHistoryService
]
})
export class ConnectionHistoryModule {}
Étape 24 — Swagger
Vérifier
GET /connections GET /connections/failed GET /connections/security GET /connections/statistics
Étape 25 — Exemples
Security Analysis
{
"totalConnections": 342,
"failedConnections": 17,
"uniqueCountries": 4,
"uniqueDevices": 6,
"highRiskConnections": 2,
"lastLoginAt":
"2026-06-10T09:12:00Z"
}
Statistics
{
"dailyConnections": 8,
"weeklyConnections": 41,
"monthlyConnections": 132,
"successRate": 95.2,
"averageRiskScore": 14.8
}
Préparation Sprint 19
Les données collectées alimenteront :
Risk SecurityIncident ComplianceAudit SecurityPolicy AuditLog
pour :
SOC2 ISO27001 RGPD Enterprise Security
Définition de terminé
Le Sprint 2-H.1 est terminé lorsque :
✓ ConnectionHistoryModule créé ✓ Historique connexions ✓ Historique échecs ✓ Analyse sécurité ✓ Statistiques ✓ Audit préparé ✓ Swagger documenté ✓ Tests verts
Livrables
ConnectionHistoryModule ConnectionHistoryController ConnectionHistoryService SecurityAnalysisDto ConnectionStatisticsDto AuditEvent Interface
Sprint 2-H.2 — Analyse Comportementale & Détection de Menaces
Objectif
Finaliser la couche sécurité utilisateur Enterprise en ajoutant :
ThreatDetectionService BehaviorAnalysisService FraudDetection Impossible Travel Brute Force Detection Account Takeover Detection Risk Dashboard
À l'issue de cette étape :
✓ Détection comportementale ✓ Détection fraude ✓ Impossible Travel ✓ Brute Force Detection ✓ Account Takeover Detection ✓ Risk Dashboard ✓ Score de menace ✓ Préparation SOC2 ✓ Préparation ISO27001
Architecture cible
Login Event ↓ Threat Engine ↓ Behavior Analysis ↓ Fraud Detection ↓ Risk Scoring ↓ Security Alert ↓ Security Dashboard
Sprint 2-H.2-A
Évolution Prisma
Étape 1 — Création ThreatEvent
Ajouter
model ThreatEvent {
id String
@id
@default(uuid())
userId String?
sessionId String?
threatType String
severity String
riskScore Int
title String
description String?
detectedAt DateTime
@default(now())
resolved Boolean
@default(false)
resolvedAt DateTime?
metadata Json?
user User?
@relation(
fields:[userId],
references:[id]
)
@@index([userId])
@@index([threatType])
@@index([severity])
@@index([detectedAt])
}
Étape 2 — Création UserBehaviorProfile
Ajouter
model UserBehaviorProfile {
id String
@id
@default(uuid())
userId String
@unique
usualCountries Json?
usualDevices Json?
usualLoginHours Json?
averageRiskScore Float
@default(0)
lastAnalyzedAt DateTime?
createdAt DateTime
@default(now())
updatedAt DateTime
@updatedAt
user User
@relation(
fields:[userId],
references:[id],
onDelete:Cascade
)
}
Étape 3 — Relations User
Ajouter
Dans :
model User
behaviorProfile UserBehaviorProfile? threatEvents ThreatEvent[]
Étape 4 — Migration
Générer
npx prisma migrate dev \
--name threat_detection
Générer
npx prisma generate
Sprint 2-H.2-B
Security Module
Étape 5 — Structure
security ├── domain │ │ └── services │ │ ├── threat-detection.service.ts │ ├── behavior-analysis.service.ts │ ├── fraud-detection.service.ts │ ├── account-takeover.service.ts │ └── risk-dashboard.service.ts
Étape 6 — Types de menaces
Créer
shared/constants threat-types.constants.ts
Ajouter
export const THREAT_TYPES = [ 'IMPOSSIBLE_TRAVEL', 'BRUTE_FORCE', 'ACCOUNT_TAKEOVER', 'SUSPICIOUS_DEVICE', 'SUSPICIOUS_COUNTRY', 'TOKEN_ABUSE', 'SESSION_HIJACK', 'PASSWORD_SPRAY' ];
Sprint 2-H.2-C
BehaviorAnalysisService
Étape 7 — Création
behavior-analysis.service.ts
Méthodes
buildBehaviorProfile() analyzeLoginBehavior() detectAnomaly()
Étape 8 — Construction profil
Analyse
Pays habituels Appareils habituels Fuseaux horaires Horaires habituels IPs habituelles
Source
ConnectionHistory
des :
90 derniers jours
Étape 9 — Détection anomalie
Exemple
Connexion habituelle : France Chrome 08h - 19h
Nouvelle connexion
Vietnam Tor Browser 03h12
Résultat
Anomalie détectée
Sprint 2-H.2-D
FraudDetectionService
Étape 10 — Création
fraud-detection.service.ts
Méthodes
detectFraud() detectImpossibleTravel() detectBruteForce()
Étape 11 — Impossible Travel
Algorithme
Connexion 1 Paris ↓ 15 minutes ↓ Connexion 2 New York
Résultat
IMPOSSIBLE_TRAVEL
Calcul
distanceKm / elapsedHours
Seuil
> 900 km/h
Étape 12 — Brute Force
Détection
10 échecs ↓ 15 minutes
Requête
success = false
sur :
ConnectionHistory
Résultat
BRUTE_FORCE
Sprint 2-H.2-E
Account Takeover
Étape 13 — Création
account-takeover.service.ts
Critères
Nouveau pays + Nouvel appareil + Risque élevé + Modification mot de passe
Résultat
ACCOUNT_TAKEOVER
Étape 14 — Score
Exemple
Pays inconnu +30 Nouvel appareil +25 IP inconnue +15 Mot de passe changé +20 Total = 90
Niveau
90 = CRITICAL
Sprint 2-H.2-F
ThreatDetectionService
Étape 15 — Création
threat-detection.service.ts
Méthode
analyzeLogin( context )
Workflow
Login ↓ Behavior Analysis ↓ Fraud Detection ↓ Risk Engine ↓ Threat Events ↓ Security Alerts
Étape 16 — Création ThreatEvent
Ajouter
await prisma.threatEvent.create({
data: {
userId,
threatType:
'IMPOSSIBLE_TRAVEL',
severity:
'HIGH',
riskScore:
85
}
});
Sprint 2-H.2-G
Dashboard Risque
Étape 17 — DTO
Créer
risk-dashboard.dto.ts
Ajouter
export class RiskDashboardDto {
activeThreats: number;
criticalThreats: number;
blockedLogins: number;
averageRiskScore: number;
suspiciousDevices: number;
suspiciousCountries: number;
}
Étape 18 — RiskDashboardService
Méthodes
getUserRiskDashboard() getTenantRiskDashboard()
Étape 19 — Calcul
Agrégations
ThreatEvent SecurityAlert ConnectionHistory
Retour
{
"activeThreats": 4,
"criticalThreats": 1,
"blockedLogins": 12,
"averageRiskScore": 37.2,
"suspiciousDevices": 2,
"suspiciousCountries": 3
}
Sprint 2-H.2-H
API Sécurité
Étape 20 — Controller
Ajouter
SecurityController
Endpoints
GET /security/threats GET /security/dashboard GET /security/behavior GET /security/fraud
Étape 21 — RBAC
Permissions
security.read security.manage
Accès
SUPER_ADMIN SECURITY_ADMIN
Sprint 2-H.2-I
Alertes Automatiques
Étape 22 — Déclencheurs
Générer SecurityAlert
si :
ThreatEvent.severity = HIGH
ou :
CRITICAL
Notification
Email Push SMS
(selon préférences utilisateur)
Étape 23 — Blocage automatique
Politique
RiskScore >= 90 ↓ Bloquer session ↓ Forcer 2FA ↓ Créer SecurityAlert
Sprint 2-H.2-J
Préparation Sprint 19
Compatible
Risk SecurityIncident ComplianceAudit SecurityPolicy AuditLog
Compatible normes
SOC2 ISO27001 RGPD NIS2 Zero Trust
Définition de terminé
Le Sprint 2-H.2 est terminé lorsque :
✓ ThreatEvent créé ✓ UserBehaviorProfile créé ✓ ThreatDetectionService ✓ BehaviorAnalysisService ✓ FraudDetectionService ✓ AccountTakeoverService ✓ RiskDashboardService ✓ Dashboard sécurité ✓ Détection menaces ✓ Swagger documenté
Livrables
ThreatEvent UserBehaviorProfile ThreatDetectionService BehaviorAnalysisService FraudDetectionService AccountTakeoverService RiskDashboardService SecurityController
Bilan Sprint 2
Le domaine :
Users Profiles Addresses Preferences Notifications Sessions Security Audit
est désormais entièrement couvert au niveau Enterprise.