Aller au contenu

PD-32 — Plan d'implémentation

1. Découpage en composants

1.1 Nouveaux composants

Composant Responsabilité Chemin
UserProfileController Exposition des endpoints GET/PUT /user/profile src/modules/user/controllers/user-profile.controller.ts
UserProfileService Logique métier de consultation/mise à jour du profil src/modules/user/services/user-profile.service.ts
UpdateUserProfileDto Validation du payload PUT (name, avatar_url, preferences) src/modules/user/dto/update-user-profile.dto.ts
UserProfileResponseDto Contrat de sortie (minimisation des données) src/modules/user/dto/user-profile-response.dto.ts
UserPreferencesDto Validation du schéma preferences (JSONB) src/modules/user/dto/user-preferences.dto.ts
UserModule Module NestJS regroupant les composants user src/modules/user/user.module.ts

1.2 Composants modifiés

Composant Modification Chemin
User (entité) Ajout colonnes name, avatar_url, preferences src/modules/auth/entities/user.entity.ts
Migration TypeORM Migration ajout colonnes src/migrations/YYYYMMDD-add-user-profile-fields.ts
AppModule Import UserModule src/app.module.ts

1.3 Composants réutilisés (sans modification)

Composant Usage
AuthenticationGuard Protection JWT sur les endpoints
RateLimitGuard Rate limiting global (configuration globale existante : src/config/rate-limit.config.ts. Pour les tests TC-ERR-08, utiliser l'environnement de test avec seuil reduit configurable.)
RlsMiddleware Contexte RLS pour isolation des données
@CurrentUser() Décorateur extraction userId du JWT

2. Flux techniques

2.1 GET /user/profile (F-32-01)

1. Client → GET /user/profile (Header: Authorization: Bearer <JWT>)
2. AuthenticationGuard → Valide JWT, extrait userId (sub claim)
3. RateLimitGuard → Vérifie seuil global
4. RlsMiddleware → SET LOCAL app.current_user_id = userId
5. UserProfileController.getProfile(@CurrentUser() user)
6. UserProfileService.getProfile(userId)
   6.1 SELECT name, email, avatar_url, preferences FROM users WHERE id = userId
   6.2 (RLS filtre automatiquement sur current_user_id)
7. Transformation → UserProfileResponseDto (exclut champs sensibles)
8. Response 200 → { name, email, avatar_url, preferences }

2.2 PUT /user/profile (F-32-02)

1. Client → PUT /user/profile (Header: Authorization, Body: UpdateUserProfileDto)
2. AuthenticationGuard → Valide JWT, extrait userId
3. ValidationPipe → Valide UpdateUserProfileDto
   3.1 Si champ protégé présent (forbidNonWhitelisted: true rejecte) → 400 ERR-32-PROTECTED-FIELD
   3.2 Si preferences non conforme au schéma → 400 ERR-32-VALIDATION
   3.3 Si avatar_url non https ou > 2048 chars → 400 ERR-32-VALIDATION
4. RateLimitGuard → Vérifie seuil global
5. RlsMiddleware → SET LOCAL app.current_user_id = userId
6. UserProfileController.updateProfile(@CurrentUser(), @Body() dto)
7. UserProfileService.updateProfile(userId, dto)
   7.1 UPDATE users SET name=?, avatar_url=?, preferences=? WHERE id = userId
   7.2 (RLS filtre automatiquement)
8. Transformation → UserProfileResponseDto
9. Response 200 → { name, email, avatar_url, preferences }

2.3 Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    Client([Client HTTP])

    subgraph NestJS Pipeline
        AG[AuthenticationGuard]
        RLG[RateLimitGuard]
        RLS[RlsMiddleware]
        VP[ValidationPipe]
    end

    subgraph UserModule
        UPC[UserProfileController]
        UPS[UserProfileService]
        UUPD[UpdateUserProfileDto]
        UPRD[UserProfileResponseDto]
        UPD[UserPreferencesDto]
    end

    subgraph Shared
        CU["@CurrentUser()"]
        EF[ExceptionFilter]
    end

    subgraph Persistence
        UE[User Entity]
        PG[(PostgreSQL + RLS)]
    end

    Client --> AG
    AG --> RLG
    RLG --> RLS
    RLS --> UPC
    UPC --> UPS
    UPC -.-> CU
    UPC -.-> VP
    VP -.-> UUPD
    VP -.-> UPD
    UPS --> UE
    UPS --> UPRD
    UE --> PG
    RLS --> PG
    EF -.-> UPC

    UUPD --> UPD

    style AG fill:#f9d71c,stroke:#333
    style RLG fill:#f9d71c,stroke:#333
    style RLS fill:#f9d71c,stroke:#333
    style VP fill:#f9d71c,stroke:#333
    style PG fill:#4a90d9,stroke:#333,color:#fff

Diagramme de séquence — PUT /user/profile (F-32-02)

sequenceDiagram
    participant C as Client
    participant AG as AuthenticationGuard
    participant RLG as RateLimitGuard
    participant VP as ValidationPipe
    participant RLS as RlsMiddleware
    participant Ctrl as UserProfileController
    participant Svc as UserProfileService
    participant DB as PostgreSQL (RLS)

    C->>AG: PUT /user/profile (JWT + Body)
    AG->>AG: Valide JWT, extrait userId
    alt JWT invalide
        AG-->>C: 401 ERR-32-UNAUTHENTICATED
    end

    AG->>VP: Payload → UpdateUserProfileDto
    VP->>VP: whitelist + forbidNonWhitelisted
    alt Champ protégé présent
        VP-->>C: 400 ERR-32-PROTECTED-FIELD
    end
    VP->>VP: @ValidateNested → UserPreferencesDto
    alt Validation échouée
        VP-->>C: 400 ERR-32-VALIDATION
    end

    VP->>RLG: Requête validée
    alt Seuil dépassé
        RLG-->>C: 429 ERR-32-RATE-LIMIT
    end

    RLG->>RLS: Requête autorisée
    RLS->>DB: SET LOCAL app.current_user_id = userId

    RLS->>Ctrl: getProfile(@CurrentUser)
    Ctrl->>Svc: updateProfile(userId, dto)
    Svc->>DB: UPDATE users SET name, avatar_url, preferences WHERE id = userId
    Note over DB: RLS filtre automatiquement sur current_user_id
    DB-->>Svc: Updated row
    Svc->>Svc: Transformation → UserProfileResponseDto
    Note over Svc: @Exclude() supprime passwordHash, srpSalt, etc.
    Svc-->>Ctrl: UserProfileResponseDto
    Ctrl-->>C: 200 { name, email, avatar_url, preferences }

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-32-01 JWT obligatoire @UseGuards(AuthenticationGuard) sur controller UserProfileController 401 si JWT absent/invalide Guard mal configuré
INV-32-02 Accès propre profil uniquement RlsMiddleware + RLS PostgreSQL RlsMiddleware, PostgreSQL SELECT/UPDATE filtrés par user_id RLS désactivé en prod
INV-32-03 Champs non sensibles uniquement UserProfileResponseDto avec @Exclude() UserProfileResponseDto Réponse JSON sans passwordHash, srpSalt, etc. Nouveau champ ajouté sans exclusion
INV-32-04 Pas de fuite de secrets @Exclude() sur entité + DTO explicite User entity, UserProfileResponseDto Champs sensibles absents de toute réponse Serialization bypass
INV-32-05 PUT accepte uniquement name, avatar_url, preferences whitelist: true dans ValidationPipe + DTO strict UpdateUserProfileDto 400 si autre champ whitelist désactivé
INV-32-06 Rejet modification champ protégé forbidNonWhitelisted: true UpdateUserProfileDto, ValidationPipe 400 ERR-32-PROTECTED-FIELD ValidationPipe mal configuré
INV-32-07 Schema preferences strict UserPreferencesDto avec @ValidateNested() UserPreferencesDto 400 si clé inconnue Nested validation oubliée
INV-32-08 Compatibilité PD-106 UserProfileResponseDto inclut name, email, avatar_url UserProfileResponseDto Présence des 3 champs dans GET Champ manquant dans DTO
INV-32-09 Rate limiting global @UseGuards(RateLimitGuard) UserProfileController 429 après dépassement Guard non appliqué
INV-32-10 Erreurs explicites sans mutation Transactions implicites TypeORM + format erreur Service, ExceptionFilter JSON {error, message} + état inchangé Transaction partielle
INV-32-11 RGPD hors périmètre N/A N/A Artefact conformité externe

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-32-01 AuthenticationGuard Controller 401 Unauthorized
CA-32-02 AuthenticationGuard Controller 401 Unauthorized
CA-32-03 RLS PostgreSQL Middleware, DB Aucune donnée autre user RLS off
CA-32-04 @Exclude() + DTO explicite Entity, DTO Absence champs sensibles
CA-32-05 UserProfileResponseDto DTO name, email, avatar_url présents
CA-32-06 Service.updateProfile() Service name mis à jour en réponse
CA-32-07 Service.updateProfile() + validation URL Service, DTO avatar_url mis à jour
CA-32-08 Service.updateProfile() + UserPreferencesDto Service, DTO preferences mis à jour
CA-32-09 forbidNonWhitelisted + @ValidateNested() DTO 400 ERR-32-VALIDATION
CA-32-10 whitelist + forbidNonWhitelisted DTO 400 ERR-32-PROTECTED-FIELD
CA-32-11 whitelist + forbidNonWhitelisted DTO 400 ERR-32-PROTECTED-FIELD
CA-32-12 RateLimitGuard Guard 429 ERR-32-RATE-LIMIT
CA-32-13 ExceptionFilter + format Filter Code erreur ERR-32-*
CA-32-14 Hors périmètre N/A Artefact externe

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau test
TC-NOM-01 F-32-01, INV-32-03/04/08 Service + DTO JSON réponse Integration
TC-NOM-02 F-32-02, INV-32-05 Service + DTO name en sortie Integration
TC-NOM-03 F-32-02, INV-32-05 Service + validation URL avatar_url en sortie Integration
TC-NOM-04 F-32-02, INV-32-05/07 Service + UserPreferencesDto preferences en sortie Integration
TC-ERR-01 INV-32-01 AuthenticationGuard Status 401 Integration
TC-ERR-02 INV-32-01 AuthenticationGuard Status 401 Integration
TC-ERR-03 INV-32-02 RLS PostgreSQL Absence données U2 Integration
TC-ERR-04 INV-32-02 RLS PostgreSQL État U2 inchangé Integration
TC-ERR-05 INV-32-07 UserPreferencesDto Status 400, ERR-32-VALIDATION Unit + Integration
TC-ERR-06 INV-32-06 ValidationPipe whitelist Status 400, ERR-32-PROTECTED-FIELD Unit + Integration
TC-ERR-07 INV-32-06 ValidationPipe whitelist Status 400, ERR-32-PROTECTED-FIELD Unit + Integration
TC-ERR-08 INV-32-09 RateLimitGuard Status 429 Integration
TC-ERR-09 INV-32-10 ValidationPipe Status 400 + état inchangé Integration
TC-ERR-10 INV-32-10 ExceptionFilter Status 500 + état inchangé Integration
TC-INV-01 INV-32-03/04 @Exclude() + DTO Absence champs sensibles Unit + Integration
TC-INV-02 INV-32-10 ExceptionFilter JSON Integration
TC-INV-03 INV-32-11 N/A Hors périmètre

Note test acces croise (TC-ERR-03/04) : Les tests TC-ERR-03/04 verifient l'isolation RLS en creant deux utilisateurs U1 et U2 en base, puis en executant une requete avec le contexte RLS de U1 tout en verifiant que les donnees de U2 ne sont jamais exposees ni modifiees. Le test controle l'etat de U2 avant/apres via SELECT direct.

6. Gestion des erreurs

6.1 Format de réponse d'erreur

{
  "error": "ERR-32-XXX",
  "message": "Description lisible"
}

6.2 Mapping codes erreur

Code HTTP Status Condition Mécanisme
ERR-32-UNAUTHENTICATED 401 JWT absent/invalide/révoqué AuthenticationGuard
ERR-32-FORBIDDEN-CROSS-ACCESS 403 Contexte identité incohérent RLS (théoriquement jamais atteint via API)
ERR-32-VALIDATION 400 Payload invalide ValidationPipe + class-validator
ERR-32-PROTECTED-FIELD 400 Champ protégé dans payload ValidationPipe (forbidNonWhitelisted)
ERR-32-RATE-LIMIT 429 Seuil dépassé RateLimitGuard
ERR-32-INTERNAL 500 Erreur interne ExceptionFilter global

6.3 ExceptionFilter dédié

Un ExceptionFilter (global ou dédié UserProfileExceptionFilter) DOIT mapper les exceptions vers le format contractuel {error: ERR-32-*, message}. La verification de ce mapping fait partie des tests TC-INV-02.

7. Impacts sécurité

7.1 Risques et mitigations

Risque Mitigation Observable
Injection SQL via preferences Paramètres liés TypeORM, pas de SQL brut Aucune concaténation SQL
XSS via avatar_url Validation https:// + longueur max URL stockée telle quelle, rendu client
IDOR (accès profil autre user) RLS PostgreSQL obligatoire Test TC-ERR-03/04
Enumération utilisateurs Pas d'endpoint par ID, uniquement /me API design
Rate limiting bypass Guard appliqué avant logique métier Ordre des guards

7.2 Journalisation

  • Toute mise à jour profil → log d'audit (userId, champs modifiés, timestamp)
  • Erreurs 4xx/5xx → log avec contexte (userId, payload sanitisé)

7.3 Conformité

  • RGPD Art. 15 (droit d'accès) : satisfait par GET /user/profile
  • RGPD minimisation : satisfait par UserProfileResponseDto (exclusion champs sensibles)
  • Preuve juridique exhaustive : hors périmètre PD-32

8. Hypothèses techniques

ID Hypothèse Impact si faux
HT-01 ValidationPipe global configuré avec whitelist: true, forbidNonWhitelisted: true INV-32-05/06 non garantis
HT-02 RLS PostgreSQL actif en environnement cible INV-32-02 non garanti
HT-03 RateLimitGuard global appliqué sur routes /user/* INV-32-09 non garanti
HT-04 class-transformer @Exclude() fonctionne avec intercepteur global INV-32-03/04 non garantis
HT-05 TypeORM opère avec transactions implicites (pas de mutations partielles) INV-32-10 non garanti
HT-06 Colonne preferences de type JSONB acceptée par PostgreSQL Blocage déploiement

9. Points de vigilance (risques, dette, pièges)

9.1 Risques

  • Migration existante : S'assurer que les utilisateurs existants ont des valeurs par défaut valides (name=null, avatar_url=null, preferences={})
  • Serialization : Vérifier que ClassSerializerInterceptor est bien appliqué globalement
  • Nested validation : class-validator nécessite @Type(() => NestedDto) pour valider les objets imbriqués

9.2 Dette technique

  • Les préférences sont un schéma ouvert côté évolution — tout ajout nécessite modification DTO

9.3 Pièges courants

  • Oublier de vérifier que ClassSerializerInterceptor est appliqué globalement
  • Oublier @ValidateNested() sur le champ preferences
  • Oublier @Type(() => UserPreferencesDto) pour la transformation
  • Ne pas appliquer les guards dans le bon ordre (Auth → RateLimit → logique)

10. Hors périmètre

  • Gestion MFA (PD-27)
  • Changement de mot de passe
  • Suppression de compte
  • Logout / sessions (PD-30)
  • Modification email (flux de revalidation)
  • Modification plan (flux facturation)
  • Upload fichier avatar
  • Preuve RGPD exhaustive (artefact conformité externe)