mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md (lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation remote antérieure (triage 2026-05-02). ~73 entrées intégrées sur knowledge/backend/, dont : - patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist - patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C), row réactivable, endpoint replace atomique, updateMany conditionnel, etc. - risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste, fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.) - patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests - compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...) - README patterns/risques mis à jour Doublons internes corrigés en relecture (suppression-champ .map() → general seul ; e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés. Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -307,6 +307,21 @@ it('retourne 403 si subscription inactive', async () => {
|
||||
|
||||
- Contexte technique : auth / refresh token — RL799_V2 08-04-2026
|
||||
|
||||
### Complément — rotation du refresh token IdP en BFF : cookie rafraîchi non réécrit → déconnexions erratiques
|
||||
|
||||
Angle distinct mais lié à la rotation : en archi BFF, si le cookie de session rafraîchi (`sessionCookieToApply`) n'est réécrit que par UN seul handler (typiquement `/me`), la rotation du refresh token côté IdP déconnecte les utilisateurs de façon erratique.
|
||||
|
||||
- Les N autres call-sites d'auth déclenchent bien le refresh (access token rafraîchi en mémoire → requête autorisée → 200) mais JETTENT le nouveau cookie.
|
||||
- Tant que l'IdP n'a PAS la rotation activée, c'est inoffensif (l'ancien refresh token reste valide). MAIS si le realm a `Revoke Refresh Token` / rotation activée (durcissement prod COURANT chez Keycloak/Auth0/etc.), chaque refresh INVALIDE l'ancien refresh token côté IdP : le cookie non réécrit garde un refresh token révoqué → la requête suivante échoue → `SESSION_EXPIRED` → re-login forcé.
|
||||
- **Piège** : INVISIBLE en dev (rotation souvent off par défaut), il n'apparaît qu'au déploiement quand un ops active la rotation pour durcir.
|
||||
|
||||
Règles :
|
||||
1. Si la réécriture du cookie n'est pas généralisée à TOUS les handlers (via un wrapper qui attache `sessionCookieToApply` systématiquement), alors NE PAS activer la rotation du refresh token côté realm — et le documenter comme garde-fou de déploiement explicite.
|
||||
2. Inversement, si on veut la rotation (recommandé en sécurité), généraliser la réécriture du cookie AVANT.
|
||||
3. Ne jamais traiter « le refresh marche en dev » comme preuve que la rotation marchera en prod — tester avec la rotation activée.
|
||||
|
||||
- Cas vécu : RL799 K1.5, seul `/me` consomme `sessionCookieToApply`, ~202 autres call-sites l'ignorent ; garde-fou « pas de rotation realm avant généralisation » renvoyé au Lot 6 déploiement — 15-06-2026.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-drift-auth-copier-coller"></a>
|
||||
@@ -331,6 +346,16 @@ it('retourne 403 si subscription inactive', async () => {
|
||||
|
||||
- Contexte technique : auth / architecture — RL799_V2 08-04-2026
|
||||
|
||||
### Complément — cohérence des filtres d'autorisation entre TOUS les chemins ciblant la même population
|
||||
|
||||
Le drift ne touche pas que les codes d'erreur : il touche aussi les FILTRES appliqués sur la même population résolue à plusieurs endroits.
|
||||
|
||||
- Quand une même population (ex. « les membres actifs d'un grade ») est résolue à plusieurs endroits — un chemin de NOTIFICATION qui filtre `isActive: true` et un chemin d'AUTORISATION qui fait `getUserByEmail` sans filtre `isActive` — la divergence crée une faille : un compte désactivé/démissionnaire avec un JWT encore valide (fenêtre ≤ TTL) n'est pas notifié MAIS peut encore agir.
|
||||
- **Règle** : tout contrôle d'autorisation basé sur un fetch user doit re-vérifier `isActive` à chaque requête (le JWT ne reflète pas une désactivation survenue après émission).
|
||||
- **Audit** : grep des `getUserByEmail` / `findUser*` dans les services, vérifier que chaque usage en contexte d'autorisation filtre/contrôle `isActive`.
|
||||
- **Symptôme de l'incohérence** : « la liste des destinataires d'un effet et la liste des autorisés à le déclencher ne coïncident pas ».
|
||||
- Cas vécu : isolation de réponse aux instructions RL799 — le fetch DB avait été ajouté EXPRÈS pour capter les changements d'état à chaque requête, mais ignorait `isActive`, annulant le bénéfice.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-auth-acl-unique-champ-sensible"></a>
|
||||
@@ -494,4 +519,54 @@ if (user.deletedAt !== null) {
|
||||
- **Règle** : dans `login()`, toujours répondre `INVALID_CREDENTIALS` pour un compte soft-deleted — jamais un code spécifique.
|
||||
- **Nuance** : un code `ACCOUNT_DELETED` reste acceptable dans un flux `exchange()` OAuth, où le provider a déjà confirmé l'identité (pas d'énumération possible côté attaquant).
|
||||
|
||||
- Contexte technique : auth / soft-delete / anti-énumération — app-alexandrie 13-04-2026
|
||||
- Contexte technique : auth / soft-delete / anti-énumération — app-alexandrie 13-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-guard-abonnement-vs-droit-acquis"></a>
|
||||
## Guard d'abonnement global vs droits acquis permanents
|
||||
|
||||
### Risques
|
||||
|
||||
- Un guard de gating « abonnement actif » (ex. `RequireSubscriptionActive` / `RequireAccessLevel(FULL)`) posé uniformément sur TOUTES les routes d'un domaine coupe l'accès à un contenu déjà payé en one-shot (« possession à vie ») dès que l'abonnement est résilié
|
||||
- Violation silencieuse d'un invariant métier : « je garde ce que j'ai payé même sans abo »
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Couper l'abonnement rend inaccessible un contenu acheté de façon permanente
|
||||
- Aucun test ne couvre le cas « droit permanent + abo coupé » → régression non détectée
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Avant d'appliquer un guard « abonnement actif » uniformément, distinguer deux natures de droit :
|
||||
- **droit RÉCURRENT** (lié à l'abo : feed, communauté, contenu inclus)
|
||||
- **droit ACQUIS/permanent** (achat one-shot, possession « à vie »)
|
||||
- **Règle** : gater la LECTURE d'un bien acquis par la POSSESSION (helper `canAccess…`), pas par l'abonnement. Réserver le guard abo aux routes d'écriture/progression et aux contenus récurrents.
|
||||
- TOUJOURS écrire un test « bien possédé + abo coupé → lisible » : c'est l'angle mort classique qui laisse passer ce type de régression.
|
||||
|
||||
- Contexte technique : auth / gating abonnement — app-alexandrie 02-06-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-validite-jeton-vs-duree-acces"></a>
|
||||
## Confondre la validité du JETON d'octroi avec la durée de l'ACCÈS octroyé
|
||||
|
||||
### Risques
|
||||
|
||||
- Un helper d'accès lit le `expiresAt` d'un jeton d'octroi (code de déblocage, lien/token d'invitation) comme SOURCE D'ACCÈS directe
|
||||
- Mais `expiresAt` borne la fenêtre d'ACTIVATION du jeton (ex. 72 h), pas la durée de l'accès octroyé (censé être permanent) → l'accès expire en même temps que le jeton
|
||||
|
||||
### Symptômes
|
||||
|
||||
- L'accès « à vie » expire 72 h après l'émission du code
|
||||
- Bug non détecté par les tests (qui valident le helper tel qu'écrit, pas l'intention)
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Ne JAMAIS faire dépendre la vérification d'accès du `expiresAt` du jeton.
|
||||
- À l'activation, **matérialiser l'accès dans son entité propre** (ex. `UserPack`/possession) et vérifier l'accès via CETTE entité — pas via le jeton.
|
||||
- **Règle** : « le jeton expire, le droit qu'il a créé persiste. »
|
||||
- Test obligatoire : « jeton activé puis expiré → l'accès reste valide ».
|
||||
- **Corollaire** : un helper d'accès ne doit pas « anticiper » un mécanisme pas encore implémenté en lisant un état intermédiaire — il introduit un modèle d'accès parallèle qui diverge du modèle cible (la branche aurait dû passer par `UserPack` dès le départ).
|
||||
|
||||
- Contexte technique : auth / activation vs possession — app-alexandrie 02-06-2026
|
||||
Reference in New Issue
Block a user