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