capitalisation: intégration 28 entrées knowledge + 2 CLAUDE.md RL799_V2 (triage branche mcp_v1)

28 nouvelles sections intégrées dans 12 fichiers knowledge (backend risques/patterns,
frontend risques/patterns, workflow risques). Couvre rate limiting, RGPD, CSP Next.js,
refresh token TOCTOU, catch-all Prisma, distinction 401/403, tests E2E Playwright, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-04-08 20:11:02 +02:00
parent 72758c1adc
commit 7767f1f947
12 changed files with 662 additions and 0 deletions

View File

@@ -438,3 +438,313 @@ try {
- Permet la surcharge en test et le rechargement dynamique
- Contexte technique : Node.js / tests — RL799_V2 07-04-2026
---
<a id="risque-rate-limiting-couverture-test"></a>
## Rate limiting — couverture de test insuffisante
### Risques
- Tester uniquement l'objet `RateLimiter` en isolation (`.check()`, `.reset()`) donne une fausse confiance. Les bugs de câblage (limiter non appelé, mauvais limiter sur le mauvais endpoint, format de réponse 429 incorrect) passent au travers.
### Symptômes
- Tests unitaires verts mais réponse 429 absente ou mal formatée en intégration
- Header `retry-after` manquant dans la réponse HTTP
### Bonnes pratiques / mitigations
- Pour chaque rate limiter intégré dans un endpoint, ajouter au minimum :
1. Un test d'intégration qui dépasse le seuil via le handler HTTP et vérifie status 429 + body + header `retry-after`
2. Un test d'expiration de fenêtre (mock `Date.now` et avancer le temps)
3. Vérifier que le logging sécurité est déclenché (au minimum visible dans la sortie test)
- Contexte technique : backend / rate limiting — RL799_V2 07-04-2026
---
<a id="risque-nommage-metriques-agregees-dto"></a>
## Nommage des métriques agrégées dans les DTO
### Risques
- Nommer un champ `xxxCount` ou `xxxThisYear` sans que le nom reflète exactement ce qui est compté
- Confusion en maintenance, bugs d'affichage, labels UI incohérents
### Symptômes
- `tenuesThisYear` qui compte en réalité les *présences* du membre, pas le nombre total de tenues
- Labels frontend incohérents avec le comportement réel du champ
### Bonnes pratiques / mitigations
- Le nom du champ DTO doit refléter exactement l'entité comptée ET le scope du comptage
- Préférer `presencesThisYear` à `tenuesThisYear` si on compte les attendances d'un user, réserver `tenuesThisYear` pour le COUNT de tenues elles-mêmes
- **Signal review** : tout champ `xxxCount` ou `xxxThisYear` dont le nom ne correspond pas à la requête sous-jacente
- Contexte technique : backend / DTO — RL799_V2 07-04-2026
---
<a id="risque-gitignore-env-wildcard"></a>
## `.gitignore` — `.env*` wildcard capture `.env.example`
### Risques
- Utiliser `.env*` dans `.gitignore` sans exception exclut aussi `.env.example`, qui est le fichier standard de documentation des variables d'environnement et qui DOIT être versionné
### Symptômes
- Le fichier `.env.example` existe sur le disque mais n'apparaît pas dans `git status`
- Les développeurs ne savent pas quelles variables configurer
### Bonnes pratiques / mitigations
- Toujours ajouter `!.env.example` après le wildcard `.env*` dans chaque `.gitignore` (racine ET sous-projets du monorepo)
- Vérifier avec `git check-ignore <path>` que l'exception fonctionne
- **Signal review** : `.env*` dans `.gitignore` sans `!.env.example`
- Contexte technique : git / configuration — RL799_V2 08-04-2026
---
<a id="risque-strip-html-regex-single-pass"></a>
## Strip HTML regex — single-pass insuffisant
### Risques
- `input.replace(/<[^>]*>/g, '')` en un seul passage laisse des fragments exploitables sur des inputs malformés type `<scr<script>ipt>alert(1)</script>`
### Symptômes
- Après strip, le résultat contient encore des fragments `>` ou des reconstitutions partielles de tags
### Bonnes pratiques / mitigations
- Toujours (1) boucler le strip jusqu'à stabilisation (`while (prev !== result)`) ET (2) supprimer les chevrons orphelins `<>` après la boucle
- Pour du plain text (pas de rich text), c'est suffisant. Pour du rich text, utiliser une lib dédiée (DOMPurify, sanitize-html)
- **Signal review** : `replace(/<[^>]*>/g, '')` sans boucle dans un code qui traite de l'input utilisateur
- Contexte technique : sécurité / sanitisation — RL799_V2 08-04-2026
---
<a id="risque-incoherence-source-verite-filtrage-affichage"></a>
## Incohérence source de vérité — filtrage vs affichage sur des tables différentes
### Risques
- Filtrer des entités sur un champ d'une table relationnelle (`profile.grade`) tout en affichant le résultat depuis une autre table (`directory.grade`). Si les deux tables ne sont pas synchronisées en permanence, les résultats de filtrage et d'affichage divergent silencieusement.
### Symptômes
- Un membre apparaît éligible mais avec un grade affiché incohérent, ou inversement un membre éligible est invisible parce que seule la table d'affichage a été mise à jour
### Bonnes pratiques / mitigations
- Toujours utiliser la même table comme source de vérité pour le filtrage ET l'affichage d'un même attribut
- Si deux tables portent la même information, choisir celle qui fait autorité et aligner le code dessus
- **Signal review** : `where` sur `tableA.field` avec `select` sur `tableB.field` pour le même attribut
- Contexte technique : Prisma / relations — RL799_V2 08-04-2026
---
<a id="risque-check-then-create-p2002"></a>
## Check-then-create non atomique — catcher P2002
### Risques
- Vérifier l'existence d'un enregistrement (`findFirst/findUnique`) puis créer dans un second appel est une race condition classique
- Deux requêtes concurrentes passent le check, l'une échoue sur la contrainte unique
### Symptômes
- Erreur 500 générique au lieu d'un conflit 409, intermittent et difficile à reproduire
### Bonnes pratiques / mitigations
- Toujours catcher l'erreur Prisma `P2002` (unique constraint violation) dans le bloc catch de la création et la transformer en réponse métier explicite (`USER_ALREADY_EXISTS`, `CONFLICT`, etc.)
- Le check préalable reste utile pour le cas nominal mais ne doit pas être la seule protection
- **Règle** : tout `prisma.create` sur une entité à contrainte unique doit avoir un catch `P2002`
- Contexte technique : Prisma / concurrence — RL799_V2 08-04-2026
---
<a id="risque-double-update-non-transactionnel"></a>
## Double update non transactionnel sur la même entité
### Risques
- Enchaîner deux `prisma.update` séparés sur la même entité (ex: update status puis update metadata) sans transaction laisse l'entité dans un état partiel si le second échoue
### Symptômes
- L'entité est dans un état qui ne correspond à aucune transition définie
- Les flux aval (workflows, UI) se retrouvent bloqués
### Bonnes pratiques / mitigations
- Quand plusieurs champs d'une même entité doivent être modifiés ensemble pour représenter une transition d'état cohérente, les regrouper dans un seul appel `prisma.update` ou les envelopper dans une `$transaction`
- **Signal review** : deux `prisma.update` séquentiels sur le même `where: { id }` sans `$transaction`
- Contexte technique : Prisma / atomicité — RL799_V2 08-04-2026
---
<a id="risque-fallback-legacy-bypass-workflow"></a>
## Fallback "legacy" qui bypass un nouveau workflow
### Risques
- Laisser un fallback de compatibilité dans une transition d'état (ex: accepter `'draft'` en plus de `'pending_vm_approval'` comme état source de la publication) crée un chemin de contournement du nouveau workflow
- Un appel direct à la fonction interne bypass le contrôle métier
### Symptômes
- Transition d'état possible depuis un état qui ne devrait plus être accepté
- `{ in: ['ancien', 'nouveau'] }` dans un filtre Prisma sur une transition d'état
### Bonnes pratiques / mitigations
- Quand un nouveau workflow remplace un flux direct, retirer le fallback vers l'ancien état dans la transition atomique
- Si la rétro-compatibilité est nécessaire, l'encapsuler dans un flag explicite ou une route dédiée, pas dans un `{ in: ['ancien', 'nouveau'] }` silencieux
- Contexte technique : workflow / transitions d'état — RL799_V2 08-04-2026
---
<a id="risque-couverture-chemins-ecriture-securite"></a>
## Couverture incomplète des chemins d'écriture lors d'ajout de sécurité transverse
### Risques
- Quand on ajoute une mesure de sécurité (chiffrement, sanitisation, validation, audit) sur un chemin d'écriture (ex: upload), les chemins alternatifs vers la même ressource (ex: create-from-text, import CSV, seed) sont oubliés, laissant une faille
### Symptômes
- Un chemin d'écriture protégé, un autre non protégé, pour la même ressource
- Données non chiffrées ou non sanitisées créées par un chemin secondaire
### Bonnes pratiques / mitigations
- Avant de marquer une tâche sécurité comme terminée, lister TOUS les chemins d'écriture vers la ressource ciblée (grep `createDocument`, `writeFile`, `prisma.model.create`) et vérifier que chacun est couvert
- Documenter la liste dans les Dev Notes de la story
- Contexte technique : sécurité / transverse — RL799_V2 08-04-2026
---
<a id="risque-anonymisation-rgpd-pieges"></a>
## Anonymisation RGPD — pièges courants
### Risques
- **Données personnelles dans les audit logs** : stocker email ou nom dans les métadonnées d'un audit log d'anonymisation annule le droit à l'oubli (art. 17)
- **Password en clair après anonymisation** : remplacer le password par une string comme `'ANONYMIZED'` est un signal exploitable en base
- **Dernier admin anonymisable** : le système peut se retrouver sans administrateur
- **Sessions non révoquées** : le JWT existant reste valide jusqu'à expiration
### Symptômes
- Email en clair dans les métadonnées d'audit après anonymisation
- Password `'ANONYMIZED'` visible en base au lieu d'un hash bcrypt structurellement invalide
- Aucun admin actif restant après anonymisation
- Utilisateur anonymisé qui peut encore accéder à l'application
### Bonnes pratiques / mitigations
- Utiliser un hash tronqué (ex: `sha256(email).slice(0,12)`) pour la corrélation dans les audit logs, pas l'email brut
- Remplacer le password par un hash bcrypt structurellement invalide (ex: `$2b$10$INVALID...`)
- Vérifier qu'au moins un admin actif reste après anonymisation
- Révoquer tous les refresh tokens du user dans la même transaction
- Contexte technique : RGPD / sécurité — RL799_V2 08-04-2026
---
<a id="risque-assertions-body-erreur-migration-codes"></a>
## Assertions de body d'erreur non mises à jour après migration de codes
### Risques
- Lors d'une migration de codes d'erreur (ex: distinguer `UNAUTHORIZED` de `FORBIDDEN`), les assertions de status HTTP sont mises à jour mais pas les assertions sur le body JSON
### Symptômes
- `assert.equal(response.status, 401)` passe, mais `assert.equal(body.error.code, 'FORBIDDEN')` échoue — le code réel est `UNAUTHORIZED`
- Détecté seulement quand on relance la suite complète
### Bonnes pratiques / mitigations
- Lors de toute modification d'un code d'erreur dans un helper centralisé, rechercher TOUTES les assertions qui testent l'ancien code (`grep -rn 'FORBIDDEN' __tests__/`) et les mettre à jour en cohérence
- Ne pas se fier au fait que "les tests passent" sans les exécuter réellement
- Contexte technique : tests / migration de codes — RL799_V2 08-04-2026
---
<a id="risque-audit-conditionnel-lookup-secondaire"></a>
## Audit conditionnel sur un lookup DB secondaire
### Risques
- Le handler utilise `requireRoleAccess` qui ne retourne que `{ email, role }` mais pas `userId`. Pour logger l'audit, un second lookup (`getUserByEmail`) est nécessaire et peut échouer silencieusement
### Symptômes
- Une action sensible (création, promotion, suppression) réussit mais aucun log d'audit n'est écrit
- Le caller est pourtant authentifié
### Bonnes pratiques / mitigations
- Sur tout endpoint qui doit journaliser un audit, utiliser un helper qui retourne `{ userId, email, role }` directement depuis le JWT `sub` claim, plutôt qu'un helper + lookup DB
- Le userId est déjà dans le token — pas besoin d'aller le chercher en base
- Contexte technique : audit / auth — RL799_V2 08-04-2026
---
<a id="risque-derive-format-erreur-api"></a>
## Dérive du format d'erreur API entre services
### Risques
- Chaque service recrée sa propre fonction `errorResponse` locale au lieu de réutiliser un helper centralisé
- Le format standard `{ error: { code, message, requestId } }` n'est pas imposé par le typage
### Symptômes
- Certaines routes API retournent `{ error: { code, message } }` sans `requestId`, d'autres incluent le `requestId`
- Les erreurs sans requestId sont impossibles à tracer en production
### Bonnes pratiques / mitigations
- Tout nouveau service doit inclure `requestId: crypto.randomUUID()` dans ses réponses d'erreur
- Factoriser un helper `createApiErrorResponse` partagé dans `lib/` pour éviter la divergence
- **Signal review** : réponse d'erreur sans `requestId` dans un nouveau service
- Contexte technique : observabilité / API — RL799_V2 08-04-2026
---
<a id="risque-unicite-applicative-sans-contrainte-db"></a>
## Unicité applicative sans contrainte DB
### Risques
- Un contrôle d'unicité uniquement applicatif sur des affectations "actives" reste vulnérable aux races concurrentes et peut créer deux titulaires actifs sur un même rôle
### Symptômes
- Deux enregistrements actifs pour un slot censé être unique (ex: deux titulaires pour un rôle d'officier)
- Bug intermittent sous charge concurrente, invisible en dev
### Bonnes pratiques / mitigations
- Pour tout agrégat avec invariant "un seul actif" (ex: mandat d'officier par rôle), imposer une contrainte d'unicité au niveau base (index unique partiel ou stratégie équivalente) en plus des checks service
- Les checks applicatifs seuls ne suffisent pas sous concurrence
- Contexte technique : Prisma / contraintes — RL799_V2 08-04-2026