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
{
"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é)
- 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)