# Backend — Risques & vigilance : Général
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
## Observabilité insuffisante (logs non structurés, pas de corrélation)
### Risques
- MTTR très élevé : on devine
- Incapacité à mesurer l'impact utilisateur
### Symptômes
- Logs "ça a crash" sans contexte
- Impossible de relier une requête à une erreur
- Latence qui dérive sans alerte
### Bonnes pratiques / mitigations
- Logs structurés + requestId/traceId
- Métriques de base (latence, erreurs, throughput)
- Alertes simples sur 5xx/latence
---
## Migrations risquées / non reproductibles
### Risques
- Downtime
- Perte de données
- Incohérence entre environnements
### Symptômes
- "Ça marche en local" mais pas en prod
- Migration qui échoue à mi-chemin
- Rollback impossible
### Bonnes pratiques / mitigations
- Migrations versionnées + tests staging
- Stratégie expand/contract si besoin
- Plan de rollback/mitigation
---
## Boucle `upsert` N+1 sur synchronisation provider
### Risques
- Latence multipliée par le nombre d'items
- Charge DB inutile
- Timeouts ou contention sur gros volumes
### Symptômes
- Une boucle applicative exécute un `upsert` par item
- Temps de traitement qui explose avec le volume
- Logs SQL répétitifs et séquentiels
### Bonnes pratiques / mitigations
- Batcher quand c'est possible
- Précharger les données nécessaires avant boucle
- Mesurer explicitement le coût d'un `upsert` unitaire dans les flux de sync
- Contexte technique : Prisma / synchronisation provider — 10-03-2026
---
## Anti-pattern : Authorize-after-fetch (contrôle d'accès après chargement)
### Risques
- Le contrôle d'accès effectué après chargement des relations métier volumineuses augmente le coût DB et la surface de fuite temporelle pour des requêtes finalement refusées
### Symptômes
- Handler qui charge un agrégat complet (avec includes/relations) puis vérifie l'accès
- Requêtes refusées après un temps de réponse anormalement long
### Bonnes pratiques / mitigations
- Pour les endpoints détail sensibles, filtrer l'accès dans la requête DB (`where` + scope grade/tenant) ou faire un pré-check minimal avant de charger les relations
- Les accès non autorisés ne doivent pas déclencher un fetch complet des données métier
### RBAC-before-parse — autoriser avant de parser le body
- Le guard d'authentification/autorisation doit TOUJOURS être évalué AVANT tout accès au body de la requête. Un attaquant non authentifié peut sinon sonder les erreurs de validation Zod (codes, messages, structure du schéma) avant d'être rejeté, ce qui expose la surface d'attaque.
- Ordre obligatoire dans chaque handler HTTP : (1) `requireAuth` / `requireRoleAccess`, (2) validation du path/query params, (3) `request.json()` + validation Zod du body. Ne jamais inverser les étapes 1 et 3.
- Ne pas parser un payload pour un appel qui sera de toute façon refusé : c'est à la fois une fuite d'info et un coût inutile.
- **Signal review** : `request.json()` ou `schema.parse(body)` qui précède l'appel au guard d'autorisation dans un handler.
- Cas vécu : `handleUpdateReportManualSections` dans `seasonReportService.ts` (RBAC après parse), corrigé en revue adversariale v2-3-1.
- Contexte technique : backend général — RL799_V2 02-04-2026 (RBAC-before-parse ajouté 19-06-2026)
---
## Valeur sentinelle `""` au lieu de `null` dans un DTO de date nullable
### Risques
- Un champ de date optionnel dans le modèle Prisma (`Date | null`) sérialisé en `""` dans le DTO au lieu de `null`
- Le type DTO ment (`string` au lieu de `string | null`), et tout consommateur testant `if (profile.field)` a un comportement incorrect sur `""`
### Symptômes
- `?? ''` sur une date issue de `?.toISOString()` dans un repository Prisma
- Type DTO avec `someDate: string` non optionnel pour un modèle qui autorise `null` en DB
### Bonnes pratiques / mitigations
- Si un champ de date peut être `null` en DB, le type DTO doit être `string | null`
- Le repository utilise `?? null` (pas `?? ''`) pour les dates nulles
- Le frontend utilise `??` (nullish coalescing) pas `||` pour les fallbacks d'affichage sur ces champs
- Contexte technique : Prisma / DTO — RL799_V2 02-04-2026
---
## Endpoint idempotent sans contrôle de doublon
### Risques
- Un endpoint crée un event/enregistrement censé être unique par contexte (ex: un vote par tenue, un PDF par planche) sans vérifier l'existence préalable
- Un double-clic ou retry réseau crée des doublons silencieux, ou des fichiers orphelins en cas d'artefact physique
### Symptômes
- `writeFile` + `prisma.create` sans `findFirst` préalable dans le même handler
- Plusieurs enregistrements identiques avec le même contexte en base
- Fichiers physiques (PDF, export) dupliqués sur disque
### Bonnes pratiques / mitigations
- Avant tout `create` d'un event contextuel, lire les events existants avec le même filtre de contexte
- Retourner 409 si l'event existe déjà, ou mettre à jour l'existant selon la sémantique métier
- Pour les artefacts physiques : retourner l'existant (200) au lieu de recréer (201), ou supprimer l'ancien avant d'en créer un nouveau
- Contexte technique : backend général — RL799_V2 03-04-2026
---
## Fichier orphelin sur échec de persistance en base
### Risques
- Un service écrit un fichier sur disque puis crée l'entrée correspondante en base ; si la création en base échoue, le fichier reste orphelin
- Un `catch {}` vide rend le debug impossible
### Symptômes
- Fichiers physiques présents sur disque sans enregistrement correspondant en base
- Erreurs de persistance invisibles dans les logs
### Bonnes pratiques / mitigations
```ts
const file = await writeFileToDisk(...);
try {
await createDbRecord(...);
} catch (err) {
await cleanupFile(file.path);
console.error('Erreur persistance:', err);
throw err;
}
```
- Si la création en base échoue, supprimer le fichier physique (cleanup)
- Toujours logger l'erreur dans le catch, même si on retourne une réponse d'erreur propre au client
- Contexte technique : backend / fichiers — RL799_V2 03-04-2026
---
## Mélange Date locale / Date UTC dans les données de seed
### Risques
- Des dates dynamiques construites via `new Date(year, month, day, hour)` (heure locale) cohabitent avec des dates fixes en UTC (`new Date('...T19:00:00Z')`) dans le même fichier
- L'incohérence est silencieuse et produit des décalages horaires non reproductibles entre environnements
### Symptômes
- `new Date(year, month, day, hour)` (local) dans un fichier qui utilise aussi `new Date('...Z')` (UTC)
- Données de seed dont les heures varient selon la timezone de l'environnement dev
### Bonnes pratiques / mitigations
- Les dates dynamiques dans un seed doivent être construites en UTC (`Date.UTC(...)`) si les dates fixes sont en UTC
- **Signal review** : `new Date(year, month, day, hour)` dans un fichier qui utilise aussi `new Date('...Z')`
- Contexte technique : Node.js / seed — RL799_V2 03-04-2026
---
## Champ fantôme accepté par validation Zod mais non persisté
### Risques
- Un champ optionnel ajouté dans un schéma Zod est accepté par la validation mais jamais exploité par la chaîne handler → repository → modèle Prisma
- L'utilisateur envoie une donnée sans erreur, mais elle est silencieusement perdue
### Symptômes
- Champ optionnel dans le schéma Zod absent du modèle Prisma
- Champ passé au service mais non inclus dans l'appel `create`/`update` du repository
### Bonnes pratiques / mitigations
- Avant d'ajouter un champ au schéma de validation, s'assurer que le modèle DB le supporte
- Si le modèle ne le supporte pas, ne pas accepter le champ du tout
- **Signal review** : champ dans le schéma Zod qui n'apparaît pas dans le `data` de l'appel Prisma
- Contexte technique : Zod / Prisma — RL799_V2 06-04-2026
---
## Catch vide avalant les exceptions sans logging
### Risques
- Les `catch {}` ou `catch { // ignore }` dans les services métier masquent les erreurs réelles
- En production, un disque plein ou un bug PDFKit retourne un message générique sans trace serveur
### Symptômes
- `catch {` sans `console.error` ni logger dans un service métier
- Bloc catch qui retourne un message d'erreur générique sans capturer `err`
### Bonnes pratiques / mitigations
- Toujours capturer et logger l'exception dans les catch des services métier
- Les catch de rollback secondaire (ex: unlink d'un fichier) peuvent être silencieux mais devraient au minimum émettre un `warn`
- **Signal review** : `catch {` sans variable dans un service métier (hors rollback secondaire)
- Contexte technique : backend général — RL799_V2 06-04-2026
---
## Paramètres de route non validés avant passage à Prisma
### Risques
- Un paramètre de route (`id`, `memberId`) passé directement à `prisma.findUnique({ where: { id } })` sans validation de format (UUID, slug)
- Des chaînes arbitraires atteignent la couche DB, polluant les logs et violant le principe de validation aux frontières
### Symptômes
- `const { id } = params; prisma.model.findUnique({ where: { id } })` sans `isValidUuid(id)`
- Erreurs Prisma non significatives en production sur des identifiants malformés
### Bonnes pratiques / mitigations
- Toujours valider les identifiants (UUID, slug, etc.) dès l'entrée du handler ou du service, avant tout appel DB
- Retourner 400 immédiatement si invalide
- Contexte technique : backend / validation — RL799_V2 07-04-2026
---
## Cast TypeScript brut sur valeurs DB non contraintes
### Risques
- Un cast `value as 'expected_a' | 'expected_b'` sur un champ Prisma de type `String` sans enum DB masque silencieusement toute valeur inattendue
- Le type-checking passe, mais une valeur inattendue en DB provoque des comportements incohérents en aval
### Symptômes
- `status as 'pending' | 'late'` sur un champ Prisma `String` sans enum DB
- Pas de fallback explicite pour les valeurs non attendues
### Bonnes pratiques / mitigations
- Utiliser des conditions explicites (`if (s === 'a') ... else if (s === 'b') ... else fallback`) ou un mapping validé
- Ne jamais faire confiance au type runtime via un simple cast TS
- **Signal review** : `as 'literal_a' | 'literal_b'` sur un champ Prisma de type `String`
- Contexte technique : TypeScript / Prisma — RL799_V2 07-04-2026
---
## Création de ressource temporelle sans détection de chevauchement
### Risques
- Un endpoint crée une ressource avec `startDate`/`endDate` sans vérifier les collisions avec les ressources existantes
- Un double-clic ou une erreur humaine génère des doublons avec les mêmes dates
### Symptômes
- Deux ressources avec des plages temporelles qui se chevauchent pour le même contexte (cotisations, abonnements, sessions)
### Bonnes pratiques / mitigations
- Avant insertion, query les ressources existantes dont les plages se chevauchent : `WHERE start < newEnd AND end > newStart`
- Retourner 409 Conflict si collision
- S'applique aux cotisations, abonnements, périodes de garde, sessions — tout ce qui a des bornes temporelles
- Contexte technique : backend général — RL799_V2 07-04-2026
---
## TOCTOU sur opérations conditionnelles sans transaction
### Risques
- `findUnique` → check status → `update` en séquence sans transaction crée une fenêtre TOCTOU (Time-of-Check Time-of-Use)
- Un traitement concurrent peut modifier l'état entre la vérification et la mise à jour
### Symptômes
- Données incohérentes sous charge concurrente (rare en MVP mais critique en prod)
- Double relance de cotisation, double changement de statut
### Bonnes pratiques / mitigations
- Wrapper dans `prisma.$transaction()` ou utiliser un `UPDATE ... WHERE status != 'paid'` atomique
- **Règle** : toute séquence read → validate → write sur la même entité doit être atomique
- Contexte technique : Prisma / backend général — RL799_V2 07-04-2026
---
## Calcul de moyenne vs ratio global — biais d'agrégation
### Risques
- Le ratio global (somme numérateurs / somme dénominateurs) donne un résultat biaisé vers les entités avec les dénominateurs les plus grands quand on veut calculer une "moyenne de taux" sur N entités
### Symptômes
- "% de présence moyenne par degré" calculé comme total_présents / total_inscrits au lieu d'une moyenne par tenue
- Tests verts car les données de test symétriques donnent le même résultat avec les deux méthodes
### Bonnes pratiques / mitigations
- Moyenne de présence par tenue → calculer le taux par tenue, puis la moyenne des taux — pas le ratio global
- Rédiger un test qui construit des données asymétriques pour détecter la différence (ex: une tenue avec 10 inscrits/8 présents, une avec 2 inscrits/2 présents → 83% global vs 90% moyenne correcte)
- Contexte technique : backend / statistiques — RL799_V2 07-04-2026
---
## Couplage sémantique sur les types d'erreur partagés
### Risques
- Un helper générique (ex: authHelpers) importe un type nommé d'après un domaine métier spécifique (ex: ConvocationErrorCode)
- Chaque nouveau domaine qui utilise l'auth helper "dépend" conceptuellement du module d'origine
### Symptômes
- Un helper générique utilisé par 12+ fichiers importe un type nommé d'après un seul domaine métier
- Couplage sémantique incohérent hérité de la première implémentation
### Bonnes pratiques / mitigations
- À la prochaine refacto touchant le helper, extraire un type `ApiErrorCode` générique dans le package partagé et faire pointer les types domaine vers lui
- Contexte technique : backend / architecture — RL799_V2 07-04-2026
---
## Service HTTP-aware — violation de la séparation des couches
### Risques
- Un fichier `service.ts` importe `Response`, construit les headers et status HTTP, et retourne un objet `Response`
- Le service devient intestable sans mock HTTP, la logique métier est couplée au transport, et la réutilisation (CLI, cron, WebSocket) est impossible
### Symptômes
- Le fichier route.ts ne fait que `(req) => handleX(req)` — pass-through d'une ligne
- Import de `Response` dans un fichier service
### Bonnes pratiques / mitigations
- Le service doit retourner `{ data: T } | { error: { code, message } }` et la route construit la Response
- Refactoriser quand un service est touché
- Contexte technique : architecture en couches — RL799_V2 07-04-2026
---
## Count sans filtre sur User — confusion membres / utilisateurs
### Risques
- `prisma.user.count()` sans `where` inclut les comptes techniques (admin, secretaire) dans les comptages métier "nombre de membres inscrits"
- Les stats métier sont faussées silencieusement
### Symptômes
- `prisma.user.count()` sans `where` pour un affichage de type "effectifs inscrits"
- Écart entre le comptage affiché et la réalité métier
### Bonnes pratiques / mitigations
- Toujours filtrer `{ where: { role: { not: 'admin' } } }` (ou équivalent) pour les comptages métier
- À terme, ajouter un champ `status` (active/inactive/suspended) pour distinguer les membres actifs
- **Signal review** : `prisma.user.count()` sans `where` dans un contexte de statistiques
- Contexte technique : Prisma / stats — RL799_V2 07-04-2026
---
## Variables d'environnement lues au top-level d'un module
### Risques
- `const FOO = process.env.FOO || 'default'` en dehors d'une fonction fige la valeur au premier `import`
- Les tests qui modifient `process.env` après l'import ne voient pas l'effet
- Config non-reloadable sans redémarrage
### Symptômes
- `process.env.X` assigné à une constante au top-level d'un module Node
- Tests qui doivent manipuler l'ordre d'import pour surcharger une variable d'environnement
### Bonnes pratiques / mitigations
- Encapsuler dans un getter : `const getFoo = () => process.env.FOO || 'default'`
- Permet la surcharge en test et le rechargement dynamique
- Contexte technique : Node.js / tests — RL799_V2 07-04-2026
---
## 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
---
## 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
---
## `.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 ` que l'exception fonctionne
- **Signal review** : `.env*` dans `.gitignore` sans `!.env.example`
- Contexte technique : git / configuration — RL799_V2 08-04-2026
---
## Strip HTML regex — single-pass insuffisant
### Risques
- `input.replace(/<[^>]*>/g, '')` en un seul passage laisse des fragments exploitables sur des inputs malformés type `ipt>alert(1)`
### 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
---
## 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
---
## 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
---
## 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
---
## 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
---
## 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
---
## 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
---
## 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
---
## 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
---
## 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
---
## 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
---
## Docker Compose — migrations exécutées après redémarrage applicatif
### Risques
- Fenêtre de non-compatibilité entre code déployé et schéma DB.
- Crash silencieux sur colonnes/contraintes nouvellement requises.
### Symptômes
- Redéploiement "vert" puis erreurs runtime immédiates sur accès DB.
### Bonnes pratiques / mitigations
- Appliquer les migrations avant le redémarrage applicatif.
- Séquence recommandée : `docker compose build` -> `docker compose run --rm api prisma migrate deploy` -> `docker compose up -d`.
- Contexte technique : Docker Compose / déploiement — RL799_V2 08-04-2026
---
## Scripts shell — sourcing global de `.env`
### Risques
- `set -a; source .env` exporte tous les secrets à tous les sous-processus.
### Symptômes
- Secrets inutiles visibles dans l'environnement de commandes annexes (docker/curl/webhooks).
### Bonnes pratiques / mitigations
- Charger uniquement les variables nécessaires (`grep`/`cut` ou équivalent).
- Réserver `source .env` aux scripts qui ont réellement besoin de tout le contexte.
- Contexte technique : shell / secrets env — RL799_V2 08-04-2026
---
## Docker Compose — services auxiliaires sans `healthcheck`
### Risques
- Faux positifs de disponibilité d'un service qui démarre mais n'est pas prêt.
### Symptômes
- Service "up" mais non exploitable (port bloqué, DB locale corrompue, etc.).
### Bonnes pratiques / mitigations
- Ajouter un `healthcheck` homogène sur tous les services critiques et auxiliaires.
- Aligner la politique de readiness/liveness sur l'ensemble du compose.
- Contexte technique : Docker Compose / observabilité service — RL799_V2 08-04-2026
---
## Configuration fail-open d'un bind réseau en production
### Risques
- Dashboard/service exposé publiquement si variable d'environnement manquante.
### Symptômes
- Service censé rester localement accessible exposé sur `0.0.0.0`.
### Bonnes pratiques / mitigations
- Forcer le bind sûr dans l'override de production (`127.0.0.1` ou réseau privé explicite).
- Ne pas dépendre d'un `.env` optionnel pour une contrainte de sécurité.
- Contexte technique : Docker Compose / sécurité réseau — RL799_V2 08-04-2026
---
## Logging de previews payload/HTML contenant des PII
### Risques
- Fuite de données personnelles via logs applicatifs agrégés.
### Symptômes
- Logs de debug contenant noms/emails/contenu message en clair.
### Bonnes pratiques / mitigations
- Ne jamais logger le body complet d'un message métier en prod.
- Journaliser uniquement des métadonnées minimales (id, statut, taille, hash tronqué).
- Contexte technique : logs / conformité — RL799_V2 15-04-2026
---
## Base64 invalide — `Buffer.from(..., 'base64')` ne lève pas d'exception
### Risques
- Configuration/signature invalide traitée comme erreur avaleuse (401 répétés sans cause claire).
### Symptômes
- Échec systématique de vérification HMAC sans signal de configuration corrompue.
### Bonnes pratiques / mitigations
- Valider explicitement le format/base64 attendu avant dérivation HMAC.
- Retourner un signal d'erreur opérationnelle explicite (config invalide) côté logs internes.
- Contexte technique : crypto / intégration webhook — RL799_V2 15-04-2026
---
## Dates localisées sans `timeZone` explicite
### Risques
- Rendu de date divergent entre dev/CI/prod selon TZ machine.
### Symptômes
- Même ISO affiché sur des jours différents selon environnement.
### Bonnes pratiques / mitigations
- Toujours passer `timeZone` dans `toLocaleDateString`/`Intl.DateTimeFormat` pour les sorties métier.
- Définir une timezone métier unique pour les communications utilisateur.
- Contexte technique : dates / formatage serveur — RL799_V2 15-04-2026
---
## Dérive silencieuse DTO liste vs DTO détail
### Risques
- Un DTO "détail" expose un ensemble complet de champs métier, pendant qu'un DTO "liste" ne propage qu'un sous-ensemble jugé "suffisant" au moment où il est créé
- Au fil du temps, le front a besoin de plus de champs et découvre que les DTOs de liste sont **amputés** — workarounds ad-hoc, champs morts produits jamais consommés, helpers partagés impossibles à appeler sur les listes sans cast
### Symptômes
- Un consommateur front appelle l'endpoint détail juste pour obtenir un champ qui existe côté détail mais pas liste (N+1 réseau déguisé)
- Workarounds ad-hoc (`soireeClosedAt: Date | null` dans un mapper TenueSummary, copie partielle de champs) parce que le champ racine manque
- Helper partagé (`getSoireeLifecycle(input)`) qui accepte un `SoireeLifecycleInput` qu'**aucun** DTO de liste n'implémente réellement
- Type "sous-ensemble" (`SoireeCalendarStatus = 'draft' | 'pending_vm_approval' | 'published'`) aligné sur un filtre SQL transitoire plutôt que sur la sémantique du domaine
### Bonnes pratiques / mitigations
- **Règle par défaut** : DTO liste = sous-ensemble de DTO détail, pas un type parallèle. Extraire une base commune si besoin (`SoireeCore`).
- Pour chaque champ scalaire ajouté au DTO détail, se poser la question : doit-il aussi être dans les DTOs de liste ? Si oui, le propager sur-le-champ
- **Typage fort sur les sous-ensembles** : `SoireeCalendarStatus = SoireeStatus` (alias) plutôt qu'une union locale qui reflète un filtre SQL
- Test de coverage statique qui vérifie, pour chaque DTO ciblé, que tous ses mappers exposent les champs requis
- Audit périodique après une livraison qui ajoute des champs (ex : `openedAt` persistant) : lister les DTOs de liste et vérifier qu'aucun n'est amputé
- Contexte technique : DTO / contrats partagés — RL799_V2 23-04-2026
---
## Notification `linkUrl` non rôle-aware → page vide / 403 silencieux
### Risques
- Une notification envoyée à N destinataires multi-rôles avec un `linkUrl` constant route certains utilisateurs vers une page à laquelle ils n'ont pas accès
- Symptôme côté membre : "la notif m'envoie sur une page vide" — UX cassée sans message d'erreur explicite
### Symptômes
- Code de création de notif qui fait `recipients.map((r) => ({ linkUrl: 'constant' }))` sans lire `r.role`
- Notif qui cible plusieurs rôles (ex : "tous les membres") mais utilise un linkUrl pointant vers un module à accès restreint
### Bonnes pratiques / mitigations
```typescript
// Toujours sélectionner role dans le select des recipients
const recipients = await prisma.user.findMany({
where: { isActive: true, role: { in: [...ROLES_ALL_ACTIVE] } },
select: { id: true, role: true },
});
// Brancher le linkUrl par rôle
const secretariatRoles = new Set(['secretaire', 'venerable', 'admin']);
linkUrl: secretariatRoles.has(recipient.role)
? `/secretariat?soireeId=${id}`
: `/tenues?tab=calendrier`;
```
**Règle d'or** : le `linkUrl` d'une notif doit ouvrir une page **que l'utilisateur a le droit de voir ET où le contexte de la notif est visible**. Un membre qui reçoit "Soirée annulée" doit atterrir sur le calendrier (carte rouge), pas sur un module secrétariat qu'il ne peut pas consulter.
**Test E2E suggéré** : publier une notif multi-rôles, se connecter avec chaque rôle, cliquer, vérifier que chacun arrive sur une page accessible et pertinente.
- Contexte technique : notifications / RBAC — RL799_V2 23-04-2026
---
## Matrice documentée ≠ code — dérive silencieuse
### Risques
- Une matrice de permissions / contrats publiée dans une story (markdown) diverge discrètement de l'implémentation
- La doc dit "X peut Y", le code refuse Y à X (ou inversement). Aucun test ne couvre la combinaison rare
- La divergence se paye au prochain audit RBAC ou au touchement suivant du module — souvent par surprise
### Symptômes
- Story d'origine qui annonce une perm que le code ne grant pas (ou inversement)
- Un nouvel agent lit la story et la matrice, pense que la perm est active, et écrit du code qui repose dessus → faux positif aval
- Bug détecté plusieurs cycles après publication, par hasard
### Bonnes pratiques / mitigations
1. **Audit pré-flight systématique** avant tout PATCH d'un module RBAC : `grep -rn '' apps/` pour confirmer les call sites, comparer avec la matrice de la story d'origine
2. **Réconciliation atomique** : si on touche un helper de permission, mettre à jour les **deux couches** (granulaire `permissions.ts` + fonctionnelle `documentPermissions.ts`) dans la même PR
3. **Test de matrice dédié** : un test unitaire qui itère la matrice de la story et vérifie chaque cellule. Casse à la première dérive
4. Préférer **un seul source of truth** (le code) et générer la doc automatiquement (markdown depuis tests, ou inverse)
- Contexte technique : RBAC / documentation — RL799_V2 20-04-2026
---
## Format `User.id` : UUID OU slug, jamais les deux
### Risques
- Un schéma où `User.id` est un `String` libre finit par mélanger deux formats : IDs lisibles du seed (`admin`, `membre-m05`) et vrais UUIDs générés à l'invitation
- Conséquence : impossible de mettre `z.string().uuid()` dans les DTOs qui prennent un `userId` sans casser la prod
- Surface d'injection grande (payloads de 100 caractères acceptés au lieu de UUID stricts)
### Symptômes
- Schéma Zod avec `z.string().min(1).max(128)` là où on voudrait `z.string().uuid()`
- Commentaire "l'ID n'est pas forcément un UUID, on accepte toute chaîne"
- Deux populations d'ids coexistantes en base (seed slug + invitations UUID)
### Bonnes pratiques / mitigations
- Décider tôt : soit `@default(uuid())` côté Prisma partout, soit IDs structurés documentés avec une regex stricte (`^[a-z]+-[a-z0-9]+$`) publiée dans un helper shared (`isValidUserId`)
- **Ne jamais mélanger**
- Ajouter un test d'invariant : à la fin du seed, assert que tous les `users.id` matchent le format choisi
- Si migration vers UUID en cours de route : prévoir un script qui propage sur **toutes** les FKs (`audit_logs.user_id`, `notifications.recipient_id`, `refresh_tokens.user_id`, et tout `@relation` vers `User`)
- Pattern de migration : UUID v5 déterministe via `seedUserId(slug)` (cf. `pattern-uuid-v5-deterministe-seed` dans `patterns/prisma.md`)
- Contexte technique : Prisma / Zod — RL799_V2 22-04-2026
---
## Web Push `topic` header > 32 chars rejeté/tronqué (RFC 8030)
### Risques
- La [RFC 8030 §5.4](https://datatracker.ietf.org/doc/html/rfc8030#section-5.4) limite le header `Topic` à 32 caractères URL-safe
- FCM tronque silencieusement (topics distincts pour deux notifs censées dédupliquer), Apple Push rejette la requête, Mozilla autopush comportement variable
- Symptôme : déduplication absente → avalanche de notifs au reconnect d'un device offline
### Symptômes
- Push provider qui retourne 4xx sur des `topic` longs
- Plusieurs notifs reçues là où une seule devrait l'être
### Bonnes pratiques / mitigations
```typescript
import crypto from 'node:crypto';
const hashTopic = (seed: string): string =>
crypto.createHash('sha256').update(seed).digest('base64url').slice(0, 32);
await webpush.sendNotification(sub, body, {
TTL: 86_400,
urgency: 'high',
topic: hashTopic(`${type}-${contextId}`), // toujours ≤ 32 chars URL-safe
});
```
**Notes** :
- `base64url` (Node `crypto` natif depuis 16.x) produit un encoding URL-safe (`A-Za-z0-9_-`)
- Tronquer à 32 chars **après** encoding base64url, pas avant le hash
- Test unitaire : assert `topic.length <= 32` ET `topic.match(/^[A-Za-z0-9_-]+$/)` pour toutes les seeds réalistes
- Contexte technique : Web Push / RFC 8030 — RL799_V2 28-04-2026
---
## Lib npm avec types annoncés mais non embarqués
### Risques
- Certaines libs Node prétendent embarquer leurs types TS depuis une version donnée mais le package npm publié ne les contient pas
- `@types/` DefinitelyTyped existe mais peut être legacy, non maintenu, ou en conflit avec les exports réels du package
### Symptômes
- `Could not find a declaration file for module ''. … Try \`npm i --save-dev @types/\``
- TS7016 après `pnpm add ` alors que la doc annonce que les types sont embarqués
### Bonnes pratiques / mitigations
Créer une déclaration TS locale minimaliste qui couvre uniquement la surface consommée par le projet :
```typescript
// apps/api/src/types/web-push.d.ts (exemple)
declare module 'web-push' {
export interface PushSubscriptionLike {
endpoint: string;
keys: { p256dh: string; auth: string };
}
export interface RequestOptions {
TTL?: number;
urgency?: 'very-low' | 'low' | 'normal' | 'high';
topic?: string;
}
export function setVapidDetails(subject: string, publicKey: string, privateKey: string): void;
export function sendNotification(
sub: PushSubscriptionLike,
payload?: string | Buffer | null,
options?: RequestOptions,
): Promise<{ statusCode: number; body: string; headers: Record }>;
const _default: { setVapidDetails: typeof setVapidDetails; sendNotification: typeof sendNotification };
export default _default;
}
```
**Bénéfices** :
- on est maître du contrat utilisé (si la lib évolue, on étend volontairement)
- pas de dépendance `@types/*` legacy
- documentable : commentaire JSDoc en tête `Pourquoi pas @types/`
**Préventif** :
- `tsconfig.json` doit `include` le dossier `src/types/**/*.d.ts`
- documenter en commentaire en tête du `.d.ts` POURQUOI on a écrit ça soi-même
- Contexte technique : TypeScript / npm — RL799_V2 28-04-2026
---
## Form HTML POST dans un mail = neutralisé par tous les clients
### Risques
- Un `