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