Outils pour utilisateurs

Outils du site


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

Table des matières

Sprint 2 — Gestion des Utilisateurs

Objectif du Sprint

Construire le premier domaine métier complet protégé par le système RBAC mis en place lors du Sprint 1.

À l'issue du Sprint 2 :

✓ Gestion des utilisateurs

✓ Gestion du profil

✓ Gestion des adresses

✓ Préférences utilisateur

✓ Paramètres notifications

✓ Sessions utilisateur

✓ Historique connexions

✓ Administration utilisateurs

Périmètre

Modules concernés

UsersModule

ProfilesModule

PreferencesModule

NotificationModule

SessionsModule

Entités concernées

User

UserProfile

Address

UserPreference

NotificationSetting

UserSession

ConnectionHistory

Sprint 2-A — Extension du modèle Prisma

Objectif

Compléter le modèle User existant avec les informations de profil et de personnalisation.


US-0201 — Profil utilisateur

Modèle UserProfile

Ajouter :

model UserProfile {
 
  id                    String @id @default(uuid())
 
  userId                String @unique
 
  avatarUrl             String?
 
  birthDate             DateTime?
 
  gender                String?
 
  language              String?
 
  timezone              String?
 
  biography             String?
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields:[userId],
                           references:[id]
                         )
}

Relation User

Ajouter dans :

model User
profile UserProfile?

US-0202 — Adresses

Modèle Address

model Address {
 
  id                    String @id @default(uuid())
 
  userId                String
 
  label                 String
 
  addressLine1          String
 
  addressLine2          String?
 
  postalCode            String
 
  city                  String
 
  state                 String?
 
  country               String
 
  isDefault             Boolean @default(false)
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields:[userId],
                           references:[id]
                         )
 
  @@index([userId])
}

Relation User

Ajouter :

addresses Address[]

US-0203 — Préférences

Modèle UserPreference

model UserPreference {
 
  id                    String @id @default(uuid())
 
  userId                String @unique
 
  theme                 String @default("light")
 
  language              String @default("fr")
 
  timezone              String @default("Europe/Paris")
 
  dateFormat            String @default("DD/MM/YYYY")
 
  currency              String @default("EUR")
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields:[userId],
                           references:[id]
                         )
}

Relation User

Ajouter :

preferences UserPreference?

US-0204 — Notifications

Modèle NotificationSetting

model NotificationSetting {
 
  id                    String @id @default(uuid())
 
  userId                String @unique
 
  emailEnabled          Boolean @default(true)
 
  smsEnabled            Boolean @default(false)
 
  pushEnabled           Boolean @default(true)
 
  marketingEnabled      Boolean @default(false)
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields:[userId],
                           references:[id]
                         )
}

Relation User

Ajouter :

notificationSettings NotificationSetting?

US-0205 — Historique connexions

Modèle ConnectionHistory

model ConnectionHistory {
 
  id                    String @id @default(uuid())
 
  userId                String
 
  ipAddress             String?
 
  country               String?
 
  city                  String?
 
  userAgent             String?
 
  connectedAt           DateTime
 
  disconnectedAt        DateTime?
 
  success               Boolean
 
  user                  User
                         @relation(
                           fields:[userId],
                           references:[id]
                         )
 
  @@index([userId])
 
  @@index([connectedAt])
}

Relation User

Ajouter :

connectionHistory ConnectionHistory[]

US-0206 — Sessions utilisateur

Évolution Session

Compléter :

model Session

Ajouter

sessionToken      String? @unique
 
deviceName        String?
 
platform          String?
 
isCurrent         Boolean @default(false)

Migration

Générer

npx prisma migrate dev \
--name user_management

Générer Client

npx prisma generate

Sprint 2-B — Architecture NestJS

Modules

Créer :

src/modules/users

├── application
│
├── domain
│
├── infrastructure
│
├── presentation
│
└── users.module.ts

Services

UsersService

ProfileService

PreferencesService

SessionsService

Contrôleurs

UsersController

ProfileController

PreferencesController

SessionsController

Sprint 2-C — API Utilisateurs

Endpoints

GET    /users
 
GET    /users/{id}
 
POST   /users
 
PUT    /users/{id}
 
DELETE /users/{id}

Permissions

users.read

users.create

users.update

users.delete

Sprint 2-D — Gestion Profil

Endpoints

GET /profile
 
PUT /profile

Données

Avatar

Nom

Prénom

Téléphone

Date naissance

Langue

Fuseau horaire

Sprint 2-E — Gestion Adresses

Endpoints

GET    /profile/addresses
 
POST   /profile/addresses
 
PUT    /profile/addresses/{id}
 
DELETE /profile/addresses/{id}

Sprint 2-F — Préférences

Endpoints

GET /preferences
 
PUT /preferences

Paramètres

Langue

Devise

Format date

Fuseau horaire

Thème

Sprint 2-G — Notifications

Endpoints

GET /notifications/settings
 
PUT /notifications/settings

Paramètres

Email

SMS

Push

Marketing

Sprint 2-H — Sessions

Endpoints

GET /sessions
 
DELETE /sessions/{id}
 
DELETE /sessions

Fonctionnalités

Lister sessions

Déconnexion appareil

Déconnexion globale

Sprint 2-I — Historique Connexions

Endpoint

GET /connection-history

Informations

Date

IP

Pays

Navigateur

Résultat

Sprint 2-J — RBAC

Protection

Tous les endpoints doivent utiliser :

@UseGuards(
 
  JwtAuthGuard,
 
  PermissionsGuard
)

Exemple

@Permissions(
  'users.read'
)

Définition de terminé

Le Sprint 2 est terminé lorsque :

✓ CRUD utilisateurs

✓ Profil utilisateur

✓ Adresses

✓ Préférences

✓ Notifications

✓ Sessions

✓ Historique connexions

✓ Swagger documenté

✓ Tests unitaires

✓ Tests E2E

Sprint 2-A.1 — Implémentation Prisma complète

Objectif

Étendre le modèle d'authentification du Sprint 1 afin d'ajouter :

UserProfile

Address

UserPreference

NotificationSetting

ConnectionHistory

et enrichir :

User

Session

pour préparer la gestion complète des utilisateurs.


Étape 1 — Modification du modèle User

Localiser

model User

Ajouter les relations

profile                 UserProfile?
 
addresses               Address[]
 
preferences             UserPreference?
 
notificationSettings    NotificationSetting?
 
connectionHistory       ConnectionHistory[]

Résultat complet

Les relations User deviennent :

userRoles               UserRole[]
 
refreshTokens           RefreshToken[]
 
sessions                Session[]
 
profile                 UserProfile?
 
addresses               Address[]
 
preferences             UserPreference?
 
notificationSettings    NotificationSetting?
 
connectionHistory       ConnectionHistory[]

Étape 2 — Création UserProfile

Ajouter

model UserProfile {
 
  id                    String @id @default(uuid())
 
  userId                String @unique
 
  avatarUrl             String?
 
  birthDate             DateTime?
 
  gender                String?
 
  language              String?
 
  timezone              String?
 
  biography             String?
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields: [userId],
                           references: [id],
                           onDelete: Cascade
                         )
}

Étape 3 — Création Address

Ajouter

model Address {
 
  id                    String @id @default(uuid())
 
  userId                String
 
  label                 String
 
  addressLine1          String
 
  addressLine2          String?
 
  postalCode            String
 
  city                  String
 
  state                 String?
 
  country               String
 
  isDefault             Boolean @default(false)
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields: [userId],
                           references: [id],
                           onDelete: Cascade
                         )
 
  @@index([userId])
 
  @@index([country])
 
  @@index([city])
}

Étape 4 — Création UserPreference

Ajouter

model UserPreference {
 
  id                    String @id @default(uuid())
 
  userId                String @unique
 
  theme                 String @default("light")
 
  language              String @default("fr")
 
  timezone              String @default("Europe/Paris")
 
  dateFormat            String @default("DD/MM/YYYY")
 
  currency              String @default("EUR")
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields: [userId],
                           references: [id],
                           onDelete: Cascade
                         )
}

Étape 5 — Création NotificationSetting

Ajouter

model NotificationSetting {
 
  id                    String @id @default(uuid())
 
  userId                String @unique
 
  emailEnabled          Boolean @default(true)
 
  smsEnabled            Boolean @default(false)
 
  pushEnabled           Boolean @default(true)
 
  marketingEnabled      Boolean @default(false)
 
  createdAt             DateTime @default(now())
 
  updatedAt             DateTime @updatedAt
 
  user                  User
                         @relation(
                           fields: [userId],
                           references: [id],
                           onDelete: Cascade
                         )
}

Étape 6 — Création ConnectionHistory

Ajouter

model ConnectionHistory {
 
  id                    String @id @default(uuid())
 
  userId                String
 
  ipAddress             String?
 
  country               String?
 
  city                  String?
 
  userAgent             String?
 
  connectedAt           DateTime
 
  disconnectedAt        DateTime?
 
  success               Boolean
 
  user                  User
                         @relation(
                           fields: [userId],
                           references: [id],
                           onDelete: Cascade
                         )
 
  @@index([userId])
 
  @@index([connectedAt])
 
  @@index([success])
}

Étape 7 — Évolution Session

Localiser

model Session

Ajouter

sessionToken       String? @unique
 
deviceName         String?
 
platform           String?
 
isCurrent          Boolean @default(false)

Résultat

Le modèle Session devient capable de gérer :

Multi-appareils

Historique appareils

Déconnexion ciblée

Déconnexion globale

Étape 8 — Validation Prisma

Exécuter

cd apps/api
 
npx prisma validate

Résultat attendu

The schema at prisma/schema.prisma is valid

Étape 9 — Formatage

Exécuter

npx prisma format

Étape 10 — Génération migration

Exécuter

npx prisma migrate dev \
--name user_management

Résultat attendu

prisma/migrations

└── xxxx_user_management

    └── migration.sql

Étape 11 — Génération Prisma Client

Exécuter

npx prisma generate

Étape 12 — Vérification SQL

PostgreSQL

docker exec -it postgres psql -U postgres

Base

\c rental_platform

Vérifier

\d "UserProfile"
 
\d "Address"
 
\d "UserPreference"
 
\d "NotificationSetting"
 
\d "ConnectionHistory"
 
\d "Session"

Étape 13 — Mise à jour Seed

Objectif

Lors de la création d'un utilisateur, prévoir automatiquement :

UserProfile

UserPreference

NotificationSetting

afin d'éviter les valeurs nulles dans l'application.


À préparer pour Sprint 2-A.2

Créer automatiquement :

Profil vide

Préférences par défaut

Notifications par défaut

pour chaque utilisateur.


Définition de terminé

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

✓ UserProfile créé

✓ Address créée

✓ UserPreference créée

✓ NotificationSetting créée

✓ ConnectionHistory créée

✓ Session enrichie

✓ Migration exécutée

✓ Prisma Client généré

✓ Validation Prisma verte

Livrables

schema.prisma

migration user_management

Prisma Client mis à jour

Sprint 2-A.2 — Amélioration du Seed Utilisateur

Objectif

Garantir qu'un utilisateur dispose systématiquement d'un environnement fonctionnel dès sa création.

À l'issue de cette étape :

✓ UserProfile créé automatiquement

✓ UserPreference créé automatiquement

✓ NotificationSetting créé automatiquement

✓ Seed enrichi

✓ Register enrichi

✓ Aucun compte incomplet

Problème actuel

Après le Sprint 2-A.1 :

User
✓

UserProfile
✗

UserPreference
✗

NotificationSetting
✗

sont créés séparément.


Risque

Les appels :

GET /profile

GET /preferences

GET /notifications/settings

peuvent retourner :

NULL

et générer des erreurs.


Solution

Créer automatiquement :

UserProfile

UserPreference

NotificationSetting

lors :

Seed principal

Register utilisateur

Étape 1 — Création d'un UserFactoryService

Créer

src/modules/users/domain/services

user-factory.service.ts

Responsabilité

Centraliser :

Création User

Création Profile

Création Preferences

Création Notifications

Méthode

createDefaultUserEnvironment(
  userId: string
)

Étape 2 — Implémentation

Ajouter

@Injectable()
export class UserFactoryService {
 
  constructor(
 
    private readonly prisma:
      PrismaService
  ) {}
 
  async createDefaultUserEnvironment(
 
    userId: string
  ) {
 
    await this.prisma.userProfile.create({
 
      data: {
 
        userId
      }
    });
 
    await this.prisma.userPreference.create({
 
      data: {
 
        userId,
 
        theme: 'light',
 
        language: 'fr',
 
        timezone:
          'Europe/Paris',
 
        dateFormat:
          'DD/MM/YYYY',
 
        currency: 'EUR'
      }
    });
 
    await this.prisma.notificationSetting.create({
 
      data: {
 
        userId,
 
        emailEnabled: true,
 
        pushEnabled: true,
 
        smsEnabled: false,
 
        marketingEnabled: false
      }
    });
  }
}

Étape 3 — Enregistrement dans UsersModule

Ajouter

providers: [
 
  UserFactoryService
]

Étape 4 — Injection dans AuthModule

Ajouter

imports: [
 
  UsersModule
]

Injection

Dans :

AuthService

Ajouter

constructor(
 
  ...
 
  private readonly
    userFactoryService:
      UserFactoryService
)

Étape 5 — Enrichissement du Register

Localiser

const user =
  await this.prisma.user.create(...)

Ajouter

await this.userFactoryService
  .createDefaultUserEnvironment(
 
    user.id
  );

Nouveau workflow

Create User

↓

Create Profile

↓

Create Preferences

↓

Create Notification Settings

↓

Assign Role

↓

Generate JWT

Étape 6 — Mise à jour du Seed

Localiser

prisma/seed.ts

Après création Admin

Ajouter :

const profileExists =
  await prisma.userProfile.findUnique({
 
    where: {
 
      userId: admin.id
    }
  });
 
if (!profileExists) {
 
  await prisma.userProfile.create({
 
    data: {
 
      userId: admin.id
    }
  });
}

Étape 7 — Préférences Admin

Ajouter

const preferenceExists =
  await prisma.userPreference.findUnique({
 
    where: {
 
      userId: admin.id
    }
  });
 
if (!preferenceExists) {
 
  await prisma.userPreference.create({
 
    data: {
 
      userId: admin.id,
 
      language: 'fr',
 
      timezone:
        'Europe/Paris',
 
      currency: 'EUR'
    }
  });
}

Étape 8 — Notifications Admin

Ajouter

const settingsExists =
  await prisma.notificationSetting.findUnique({
 
    where: {
 
      userId: admin.id
    }
  });
 
if (!settingsExists) {
 
  await prisma.notificationSetting.create({
 
    data: {
 
      userId: admin.id
    }
  });
}

Étape 9 — Rejouer le Seed

Exécuter

npx prisma db seed

Résultat attendu

Starting seed...

Tenant created

Permissions created

Roles created

Admin created

Profile created

Preferences created

Notification settings created

Seed completed

Étape 10 — Vérification Prisma Studio

Ouvrir

npx prisma studio

Vérifier

User

↓

UserProfile

↓

UserPreference

↓

NotificationSetting

Étape 11 — Vérification SQL

UserProfile

SELECT *
 
FROM "UserProfile";

UserPreference

SELECT *
 
FROM "UserPreference";

NotificationSetting

SELECT *
 
FROM "NotificationSetting";

Étape 12 — Test Register

Requête

POST /auth/register

Vérifier

Après inscription :

SELECT *
 
FROM "UserProfile"
 
WHERE "userId" =
'new-user-id';

Résultat attendu

1 ligne

Optimisation Enterprise

Évolution recommandée

Créer un événement métier :

UserCreated

Puis

UserCreated

↓

CreateUserProfile

↓

CreateUserPreferences

↓

CreateNotificationSettings

via :

Domain Events

CQRS

EventBus

afin de découpler complètement Auth et Users.


Définition de terminé

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

✓ UserProfile créé automatiquement

✓ UserPreference créé automatiquement

✓ NotificationSetting créé automatiquement

✓ Seed mis à jour

✓ Register mis à jour

✓ Aucun compte incomplet

Livrables

UserFactoryService

AuthService enrichi

seed.ts enrichi

Création automatique du profil utilisateur

Sprint 2-B.1 — Création du UsersModule

Objectif

Implémenter le premier module métier complet de la plateforme.

À l'issue de cette étape :

✓ UsersModule

✓ UsersController

✓ UsersService

✓ DTO

✓ CRUD Utilisateurs

✓ RBAC

✓ Swagger

✓ Validation

Architecture

Créer

src/modules/users

Structure

users

├── application
│
│   └── dto
│
│       ├── create-user.dto.ts
│       ├── update-user.dto.ts
│       ├── user-response.dto.ts
│       └── users-query.dto.ts
│
├── domain
│
│   └── services
│
│       └── users.service.ts
│
├── presentation
│
│   └── controllers
│
│       └── users.controller.ts
│
└── users.module.ts

Étape 1 — CreateUserDto

Créer

application/dto/create-user.dto.ts

Implémentation

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

Étape 2 — UpdateUserDto

Créer

application/dto/update-user.dto.ts

Implémentation

import {
  IsOptional,
  IsString
} from 'class-validator';
 
export class UpdateUserDto {
 
  @IsOptional()
  @IsString()
  firstName?: string;
 
  @IsOptional()
  @IsString()
  lastName?: string;
 
  @IsOptional()
  @IsString()
  phone?: string;
 
  @IsOptional()
  @IsString()
  status?: string;
}

Étape 3 — UserResponseDto

Créer

application/dto/user-response.dto.ts

Implémentation

export class UserResponseDto {
 
  id: string;
 
  tenantId: string;
 
  email: string;
 
  firstName: string;
 
  lastName: string;
 
  phone?: string;
 
  status: string;
 
  emailVerified: boolean;
 
  createdAt: Date;
}

Étape 4 — UsersQueryDto

Créer

application/dto/users-query.dto.ts

Implémentation

import {
  IsOptional,
  IsNumberString
} from 'class-validator';
 
export class UsersQueryDto {
 
  @IsOptional()
  search?: string;
 
  @IsOptional()
  @IsNumberString()
  page?: string;
 
  @IsOptional()
  @IsNumberString()
  limit?: string;
}

Étape 5 — Création UsersService

Créer

domain/services/users.service.ts

Injection

@Injectable()
export class UsersService {
 
  constructor(
 
    private readonly prisma:
      PrismaService,
 
    private readonly passwordService:
      PasswordService,
 
    private readonly userFactoryService:
      UserFactoryService
  ) {}
}

Étape 6 — Méthodes du service

Ajouter

findAll()
 
findOne()
 
create()
 
update()
 
remove()

Étape 7 — Implémentation findAll

Ajouter

async findAll(
  query: UsersQueryDto
) {
 
  const page =
    Number(query.page ?? 1);
 
  const limit =
    Number(query.limit ?? 20);
 
  const skip =
    (page - 1) * limit;
 
  return this.prisma.user.findMany({
 
    skip,
 
    take: limit,
 
    where: {
 
      deletedAt: null
    },
 
    orderBy: {
 
      createdAt: 'desc'
    }
  });
}

Étape 8 — Implémentation findOne

Ajouter

async findOne(
  id: string
) {
 
  const user =
    await this.prisma.user.findUnique({
 
      where: { id }
    });
 
  if (!user) {
 
    throw new NotFoundException(
      'User not found'
    );
  }
 
  return user;
}

Étape 9 — Implémentation create

Ajouter

async create(
  dto: CreateUserDto
) {
 
  const tenant =
    await this.prisma.tenant.findUnique({
 
      where: {
 
        code: 'MAIN'
      }
    });
 
  const passwordHash =
    await this.passwordService.hash(
      dto.password
    );
 
  const user =
    await this.prisma.user.create({
 
      data: {
 
        tenantId: tenant.id,
 
        email: dto.email,
 
        passwordHash,
 
        firstName: dto.firstName,
 
        lastName: dto.lastName,
 
        phone: dto.phone,
 
        status: 'ACTIVE'
      }
    });
 
  await this.userFactoryService
    .createDefaultUserEnvironment(
      user.id
    );
 
  return user;
}

Étape 10 — Implémentation update

Ajouter

async update(
 
  id: string,
 
  dto: UpdateUserDto
) {
 
  await this.findOne(id);
 
  return this.prisma.user.update({
 
    where: { id },
 
    data: dto
  });
}

Étape 11 — Implémentation remove

Soft Delete

async remove(
  id: string
) {
 
  await this.findOne(id);
 
  return this.prisma.user.update({
 
    where: { id },
 
    data: {
 
      deletedAt:
        new Date(),
 
      status:
        'INACTIVE'
    }
  });
}

Étape 12 — Création UsersController

Créer

presentation/controllers/users.controller.ts

Déclaration

@ApiTags('Users')
 
@Controller('users')
 
@UseGuards(
 
  JwtAuthGuard,
 
  PermissionsGuard
)
 
export class UsersController {
 
  constructor(
 
    private readonly usersService:
      UsersService
  ) {}
}

Étape 13 — GET /users

Route

@Get()
 
@Permissions(
  'users.read'
)

Implémentation

findAll(
 
  @Query()
  query: UsersQueryDto
) {
 
  return this.usersService
    .findAll(query);
}

Étape 14 — GET /users/{id}

Route

@Get(':id')
 
@Permissions(
  'users.read'
)

Implémentation

findOne(
 
  @Param('id')
  id: string
) {
 
  return this.usersService
    .findOne(id);
}

Étape 15 — POST /users

Route

@Post()
 
@Permissions(
  'users.create'
)

Implémentation

create(
 
  @Body()
  dto: CreateUserDto
) {
 
  return this.usersService
    .create(dto);
}

Étape 16 — PUT /users/{id}

Route

@Put(':id')
 
@Permissions(
  'users.update'
)

Implémentation

update(
 
  @Param('id')
  id: string,
 
  @Body()
  dto: UpdateUserDto
) {
 
  return this.usersService
    .update(
      id,
      dto
    );
}

Étape 17 — DELETE /users/{id}

Route

@Delete(':id')
 
@Permissions(
  'users.delete'
)

Implémentation

remove(
 
  @Param('id')
  id: string
) {
 
  return this.usersService
    .remove(id);
}

Étape 18 — UsersModule

Créer

users.module.ts

Implémentation

@Module({
 
  imports: [
 
    PrismaModule,
 
    AuthModule
  ],
 
  controllers: [
 
    UsersController
  ],
 
  providers: [
 
    UsersService,
 
    UserFactoryService
  ],
 
  exports: [
 
    UsersService
  ]
})
export class UsersModule {}

Étape 19 — Swagger

Vérifier

Les endpoints suivants doivent apparaître :

GET /users

GET /users/{id}

POST /users

PUT /users/{id}

DELETE /users/{id}

Étape 20 — Tests manuels

SUPER_ADMIN

Doit pouvoir :

Créer

Modifier

Lister

Supprimer

des utilisateurs.


CUSTOMER

Doit recevoir :

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

Définition de terminé

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

✓ UsersModule créé

✓ UsersController créé

✓ UsersService créé

✓ CRUD fonctionnel

✓ RBAC actif

✓ Swagger documenté

✓ Soft Delete opérationnel

Livrables

UsersModule

UsersController

UsersService

CreateUserDto

UpdateUserDto

UserResponseDto

UsersQueryDto

Sprint 2-B.2 — Amélioration Enterprise du UsersService

Objectif

Transformer le CRUD utilisateur basique en véritable service Enterprise.

À l'issue de cette étape :

✓ Pagination avancée

✓ Recherche multi-champs

✓ Tri dynamique

✓ Filtres

✓ Isolation Multi-Tenant

✓ Audit Logs

✓ Soft Delete sécurisé

✓ Réponses paginées

Problème actuel

Le service actuel :

findAll()

ne gère que :

Pagination simple

et ne protège pas encore :

Isolation tenant

Recherche

Audit

Tri

Étape 1 — Création UserQueryDto Enterprise

Remplacer

users-query.dto.ts

Nouveau DTO

import {
  IsOptional,
  IsString,
  IsNumberString
} from 'class-validator';
 
export class UsersQueryDto {
 
  @IsOptional()
  search?: string;
 
  @IsOptional()
  status?: string;
 
  @IsOptional()
  emailVerified?: string;
 
  @IsOptional()
  role?: string;
 
  @IsOptional()
  sortBy?: string;
 
  @IsOptional()
  sortOrder?: 'asc' | 'desc';
 
  @IsOptional()
  @IsNumberString()
  page?: string;
 
  @IsOptional()
  @IsNumberString()
  limit?: string;
}

Étape 2 — DTO de pagination

Créer

application/dto/paginated-response.dto.ts

Implémentation

export class PaginatedResponseDto<T> {
 
  data: T[];
 
  page: number;
 
  limit: number;
 
  total: number;
 
  totalPages: number;
}

Étape 3 — Sécurité Multi-Tenant

Principe

Aucun utilisateur ne doit voir :

Tenant A

↓

Tenant B

Récupération

Dans le contrôleur :

@Req()
request

Extraire

const tenantId =
  request.user.tenantId;

Étape 4 — Signature findAll

Modifier

async findAll(
 
  tenantId: string,
 
  query: UsersQueryDto
)

Étape 5 — Construction du filtre

Ajouter

const where: Prisma.UserWhereInput = {
 
  tenantId,
 
  deletedAt: null
};

Recherche

if (query.search) {
 
  where.OR = [
 
    {
      email: {
        contains: query.search,
        mode: 'insensitive'
      }
    },
 
    {
      firstName: {
        contains: query.search,
        mode: 'insensitive'
      }
    },
 
    {
      lastName: {
        contains: query.search,
        mode: 'insensitive'
      }
    }
  ];
}

Étape 6 — Filtre statut

Ajouter

if (query.status) {
 
  where.status =
    query.status;
}

Étape 7 — Filtre email vérifié

Ajouter

if (
 
  query.emailVerified !==
  undefined
 
) {
 
  where.emailVerified =
 
    query.emailVerified ===
    'true';
}

Étape 8 — Pagination

Ajouter

const page =
  Number(query.page ?? 1);
 
const limit =
  Number(query.limit ?? 20);
 
const skip =
  (page - 1) * limit;

Étape 9 — Tri dynamique

Ajouter

const sortBy =
  query.sortBy ??
  'createdAt';
 
const sortOrder =
  query.sortOrder ??
  'desc';

OrderBy

orderBy: {
 
  [sortBy]:
    sortOrder
}

Étape 10 — Count global

Ajouter

const total =
  await this.prisma.user.count({
 
    where
  });

Étape 11 — Chargement utilisateurs

Ajouter

const users =
  await this.prisma.user.findMany({
 
    where,
 
    skip,
 
    take: limit,
 
    orderBy: {
 
      [sortBy]:
        sortOrder
    }
  });

Étape 12 — Réponse paginée

Retour

return {
 
  data: users,
 
  page,
 
  limit,
 
  total,
 
  totalPages:
 
    Math.ceil(
      total / limit
    )
};

Étape 13 — Sécurisation findOne

Modifier

async findOne(
 
  tenantId: string,
 
  id: string
)

Requête

const user =
  await this.prisma.user.findFirst({
 
    where: {
 
      id,
 
      tenantId,
 
      deletedAt: null
    }
  });

Résultat

Impossible d'accéder à un utilisateur :

Autre tenant

Étape 14 — Sécurisation update

Ajouter

await this.findOne(
  tenantId,
  id
);

avant toute modification.


Étape 15 — Sécurisation remove

Ajouter

await this.findOne(
  tenantId,
  id
);

avant suppression.


Étape 16 — AuditLog

Préparation

Le modèle :

AuditLog

sera créé lors de la Phase 2-I.


Interface temporaire

Créer :

shared/interfaces/audit.interface.ts
export interface AuditEvent {
 
  action: string;
 
  entityType: string;
 
  entityId: string;
 
  userId: string;
 
  timestamp: Date;
}

Étape 17 — Audit Create

Ajouter

Après :

create()

Événement

this.logger.log({
 
  action: 'USER_CREATED',
 
  entityType: 'User',
 
  entityId: user.id
});

Étape 18 — Audit Update

Ajouter

USER_UPDATED

Étape 19 — Audit Delete

Ajouter

USER_DELETED

Étape 20 — Contrôleur Enterprise

Modifier

findAll(
 
  @Req()
  req,
 
  @Query()
  query
) {
 
  return this.usersService.findAll(
 
    req.user.tenantId,
 
    query
  );
}

Même logique

Pour :

findOne()

update()

remove()

Étape 21 — Swagger

Ajouter

Paramètres :

search

status

emailVerified

sortBy

sortOrder

page

limit

dans :

@ApiQuery()

Étape 22 — Exemples

Recherche

GET /users?search=john

Pagination

GET /users?page=2&limit=50

Tri

GET /users?sortBy=email&sortOrder=asc

Filtre

GET /users?status=ACTIVE

Étape 23 — Performance

Ajouter index Prisma

Dans :

model User

Vérifier

@@index([tenantId])
 
@@index([email])
 
@@index([status])
 
@@index([createdAt])
 
@@index([deletedAt])

Étape 24 — Tests

Vérifier

Recherche

Pagination

Tri

Filtre

Isolation tenant

Soft Delete

Définition de terminé

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

✓ Pagination Enterprise

✓ Recherche multi-champs

✓ Tri dynamique

✓ Filtres

✓ Multi-Tenant Security

✓ Audit préparé

✓ Swagger enrichi

✓ Tests verts

Livrables

UsersQueryDto

PaginatedResponseDto

UsersService Enterprise

Recherche

Filtres

Pagination

Isolation Multi-Tenant

Sprint 2-C.1 — Gestion du Profil Utilisateur

Objectif

Implémenter la gestion complète du profil utilisateur basée sur :

UserProfile

À l'issue de cette étape :

✓ ProfileModule

✓ ProfileController

✓ ProfileService

✓ GET /profile

✓ PUT /profile

✓ Gestion Avatar

✓ Gestion Informations personnelles

✓ Swagger

✓ RBAC

Architecture

Créer

src/modules/profile

Structure

profile

├── application
│
│   └── dto
│
│       ├── update-profile.dto.ts
│       └── profile-response.dto.ts
│
├── domain
│
│   └── services
│
│       └── profile.service.ts
│
├── presentation
│
│   └── controllers
│
│       └── profile.controller.ts
│
└── profile.module.ts

Étape 1 — UpdateProfileDto

Créer

application/dto/update-profile.dto.ts

Implémentation

import {
 
  IsOptional,
 
  IsString,
 
  IsDateString,
 
  IsUrl
 
} from 'class-validator';
 
export class UpdateProfileDto {
 
  @IsOptional()
 
  @IsUrl()
 
  avatarUrl?: string;
 
  @IsOptional()
 
  @IsDateString()
 
  birthDate?: string;
 
  @IsOptional()
 
  @IsString()
 
  gender?: string;
 
  @IsOptional()
 
  @IsString()
 
  language?: string;
 
  @IsOptional()
 
  @IsString()
 
  timezone?: string;
 
  @IsOptional()
 
  @IsString()
 
  biography?: string;
 
  @IsOptional()
 
  @IsString()
 
  firstName?: string;
 
  @IsOptional()
 
  @IsString()
 
  lastName?: string;
 
  @IsOptional()
 
  @IsString()
 
  phone?: string;
}

Étape 2 — ProfileResponseDto

Créer

application/dto/profile-response.dto.ts

Implémentation

export class ProfileResponseDto {
 
  id: string;
 
  email: string;
 
  firstName: string;
 
  lastName: string;
 
  phone?: string;
 
  avatarUrl?: string;
 
  birthDate?: Date;
 
  gender?: string;
 
  language?: string;
 
  timezone?: string;
 
  biography?: string;
 
  emailVerified: boolean;
}

Étape 3 — Création ProfileService

Créer

domain/services/profile.service.ts

Injection

@Injectable()
export class ProfileService {
 
  constructor(
 
    private readonly prisma:
      PrismaService
  ) {}
}

Étape 4 — Méthodes

Ajouter

getProfile()
 
updateProfile()

Étape 5 — Implémentation getProfile

Ajouter

async getProfile(
 
  userId: string,
 
  tenantId: string
) {

Chargement

const user =
  await this.prisma.user.findFirst({
 
    where: {
 
      id: userId,
 
      tenantId,
 
      deletedAt: null
    },
 
    include: {
 
      profile: true
    }
  });

Vérification

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

Retour

return {
 
  id: user.id,
 
  email: user.email,
 
  firstName: user.firstName,
 
  lastName: user.lastName,
 
  phone: user.phone,
 
  emailVerified:
    user.emailVerified,
 
  avatarUrl:
    user.profile?.avatarUrl,
 
  birthDate:
    user.profile?.birthDate,
 
  gender:
    user.profile?.gender,
 
  language:
    user.profile?.language,
 
  timezone:
    user.profile?.timezone,
 
  biography:
    user.profile?.biography
};

Étape 6 — Implémentation updateProfile

Signature

async updateProfile(
 
  userId: string,
 
  tenantId: string,
 
  dto: UpdateProfileDto
)

Étape 7 — Mise à jour User

Ajouter

await this.prisma.user.update({
 
  where: {
 
    id: userId
  },
 
  data: {
 
    firstName:
      dto.firstName,
 
    lastName:
      dto.lastName,
 
    phone:
      dto.phone
  }
});

Étape 8 — Mise à jour UserProfile

Ajouter

await this.prisma.userProfile.update({
 
  where: {
 
    userId
  },
 
  data: {
 
    avatarUrl:
      dto.avatarUrl,
 
    birthDate:
      dto.birthDate,
 
    gender:
      dto.gender,
 
    language:
      dto.language,
 
    timezone:
      dto.timezone,
 
    biography:
      dto.biography
  }
});

Étape 9 — Retour profil

Ajouter

return this.getProfile(
 
  userId,
 
  tenantId
);

Étape 10 — Création Controller

Créer

presentation/controllers/profile.controller.ts

Déclaration

@ApiTags('Profile')
 
@Controller('profile')
 
@UseGuards(
 
  JwtAuthGuard
)
 
@ApiBearerAuth()

Étape 11 — GET /profile

Route

@Get()

Implémentation

getProfile(
 
  @Req() req
) {
 
  return this.profileService
    .getProfile(
 
      req.user.id,
 
      req.user.tenantId
    );
}

Étape 12 — PUT /profile

Route

@Put()

Implémentation

updateProfile(
 
  @Req() req,
 
  @Body()
  dto: UpdateProfileDto
) {
 
  return this.profileService
    .updateProfile(
 
      req.user.id,
 
      req.user.tenantId,
 
      dto
    );
}

Étape 13 — Swagger

GET

@ApiOperation({
 
  summary:
    'Get current profile'
})

PUT

@ApiOperation({
 
  summary:
    'Update current profile'
})

Réponse

@ApiOkResponse({
 
  type:
    ProfileResponseDto
})

Étape 14 — ProfileModule

Créer

profile.module.ts

Implémentation

@Module({
 
  imports: [
 
    PrismaModule,
 
    AuthModule
  ],
 
  controllers: [
 
    ProfileController
  ],
 
  providers: [
 
    ProfileService
  ],
 
  exports: [
 
    ProfileService
  ]
})
export class ProfileModule {}

Étape 15 — Import AppModule

Ajouter

imports: [
 
  ...
 
  ProfileModule
]

Étape 16 — Test GET

Requête

GET /profile
Authorization: Bearer xxx

Réponse

{
  "id": "...",
  "email": "john@test.com",
  "firstName": "John",
  "lastName": "Doe",
  "avatarUrl": null,
  "language": "fr",
  "timezone": "Europe/Paris"
}

Étape 17 — Test PUT

Requête

PUT /profile

Body

{
  "avatarUrl":
    "https://cdn.app/avatar.jpg",
 
  "language":
    "fr",
 
  "timezone":
    "Europe/Paris",
 
  "biography":
    "Property manager"
}

Résultat

Profil mis à jour.


Étape 18 — Sécurisation

Vérifier

Un utilisateur :

Ne peut modifier

Que son propre profil

Garantie

Grâce à :

req.user.id

aucun identifiant utilisateur n'est exposé dans l'API.


Étape 19 — Audit futur

Préparer

Événement :

PROFILE_UPDATED

qui sera connecté plus tard à :

AuditLog

EntityHistory

du Sprint 19.


Définition de terminé

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

✓ ProfileModule créé

✓ ProfileController créé

✓ ProfileService créé

✓ GET /profile opérationnel

✓ PUT /profile opérationnel

✓ Swagger documenté

✓ Sécurité validée

Livrables

ProfileModule

ProfileController

ProfileService

UpdateProfileDto

ProfileResponseDto

Sprint 2-C.2 — Gestion avancée du Profil

Objectif

Transformer le profil utilisateur en profil Enterprise complet.

À l'issue de cette étape :

✓ Upload Avatar

✓ Validation image

✓ Suppression Avatar

✓ Stockage MinIO

✓ Compatibilité S3

✓ Historique modifications

✓ Audit des changements

✓ URLs sécurisées

Architecture cible

Client

↓

Upload Avatar

↓

Validation

↓

Storage Service

↓

MinIO / S3

↓

UserProfile

↓

AuditLog

Étape 1 — Création du StorageModule

Créer

src/modules/storage

Structure

storage

├── domain
│
│   └── services
│
│       └── storage.service.ts
│
├── infrastructure
│
│   └── providers
│
│       └── minio.provider.ts
│
└── storage.module.ts

Étape 2 — Installation MinIO

Installer

npm install minio

Étape 3 — Variables d'environnement

Ajouter

MINIO_ENDPOINT=localhost

MINIO_PORT=9000

MINIO_ACCESS_KEY=minioadmin

MINIO_SECRET_KEY=minioadmin

MINIO_BUCKET=avatars

MINIO_SSL=false

Étape 4 — Docker Compose

Ajouter

minio:

  image: minio/minio

  container_name: minio

  command:

    server /data

  ports:

    - "9000:9000"
 
    - "9001:9001"

  environment:

    MINIO_ROOT_USER:
      minioadmin

    MINIO_ROOT_PASSWORD:
      minioadmin

  volumes:

    - minio-data:/data

Étape 5 — Minio Provider

Créer

infrastructure/providers/minio.provider.ts

Implémentation

import * as Minio
from 'minio';
 
export const MinioProvider = {
 
  provide: 'MINIO',
 
  useFactory: () => {
 
    return new Minio.Client({
 
      endPoint:
        process.env.MINIO_ENDPOINT,
 
      port:
        Number(
          process.env.MINIO_PORT
        ),
 
      useSSL:
        process.env.MINIO_SSL ===
        'true',
 
      accessKey:
        process.env.MINIO_ACCESS_KEY,
 
      secretKey:
        process.env.MINIO_SECRET_KEY
    });
  }
};

Étape 6 — StorageService

Créer

storage.service.ts

Méthodes

uploadAvatar()
 
deleteAvatar()
 
generateUrl()

Étape 7 — Upload Avatar

Ajouter

async uploadAvatar(
 
  file: Express.Multer.File,
 
  userId: string
)

Nom du fichier

const fileName =
 
  `avatars/${userId}/${
    randomUUID()
  }.jpg`;

Upload

await this.minioClient.putObject(
 
  process.env.MINIO_BUCKET,
 
  fileName,
 
  file.buffer
);

Retour

return fileName;

Étape 8 — Validation image

Contraintes

jpg

jpeg

png

webp

Taille

5 MB max

Validator

Créer :

shared/validators/avatar.validator.ts

Implémentation

export const ALLOWED_TYPES = [
 
  'image/jpeg',
 
  'image/png',
 
  'image/webp'
];

Contrôle

if (
 
  !ALLOWED_TYPES.includes(
    file.mimetype
  )
 
) {
 
  throw new BadRequestException(
    'Invalid image'
  );
}

Étape 9 — DTO Upload

Créer

application/dto/avatar-response.dto.ts

Contenu

export class AvatarResponseDto {
 
  avatarUrl: string;
}

Étape 10 — Nouveau endpoint

Route

POST /profile/avatar

Controller

@Post('avatar')
 
@UseInterceptors(
 
  FileInterceptor(
    'file'
  )
)

Swagger

@ApiConsumes(
  'multipart/form-data'
)

Étape 11 — Upload

Implémentation

uploadAvatar(
 
  @Req() req,
 
  @UploadedFile()
  file
) {
 
  return this.profileService
    .uploadAvatar(
 
      req.user.id,
 
      file
    );
}

Étape 12 — Service Profile

Ajouter

async uploadAvatar(
 
  userId: string,
 
  file:
    Express.Multer.File
)

Upload

const avatarUrl =
 
  await this.storageService
    .uploadAvatar(
 
      file,
 
      userId
    );

Mise à jour profil

await this.prisma.userProfile.update({
 
  where: {
 
    userId
  },
 
  data: {
 
    avatarUrl
  }
});

Étape 13 — Suppression Avatar

Endpoint

DELETE /profile/avatar

Service

async deleteAvatar(
  userId: string
)

Étapes

Load Profile

↓

Delete Object MinIO

↓

avatarUrl = null

Étape 14 — Génération URL

Objectif

Ne jamais exposer :

bucket interne

Utiliser

presignedGetObject()

Exemple

const url =
 
  await this.minioClient
    .presignedGetObject(
 
      bucket,
 
      fileName,
 
      3600
    );

Étape 15 — Historique Profil

Créer

ProfileHistory

Modèle Prisma

model ProfileHistory {
 
  id                String @id @default(uuid())
 
  userId            String
 
  changedField      String
 
  oldValue          String?
 
  newValue          String?
 
  changedAt         DateTime @default(now())
}

Étape 16 — Relation User

Ajouter

profileHistory ProfileHistory[]

Étape 17 — Audit Modification

Lors du update

Ajouter :

PROFILE_UPDATED

Données

Champ

Ancienne valeur

Nouvelle valeur

Date

Étape 18 — Audit Avatar

Événements

AVATAR_UPLOADED

AVATAR_DELETED

Étape 19 — Swagger

Vérifier

GET /profile

PUT /profile

POST /profile/avatar

DELETE /profile/avatar

Étape 20 — Tests

Upload

jpg valide

↓

200

PNG

png valide

↓

200

PDF

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

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.


ujusum/3-codage/2-sprints/2-sprint-2.txt · Dernière modification : 2026/06/10 00:43 de 83.202.252.200

DokuWiki Appliance - Powered by TurnKey Linux