mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Refonte Structure
This commit is contained in:
18
knowledge/backend/risques/README.md
Normal file
18
knowledge/backend/risques/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Backend — Risques & vigilance — Index
|
||||
|
||||
Risques backend susceptibles de provoquer des incidents prod, failles de sécurité, bugs non diagnostiquables, ou régressions coûteuses.
|
||||
|
||||
Avant toute proposition backend, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
|
||||
|
||||
---
|
||||
|
||||
| Fichier | Domaine | Entrées clés |
|
||||
|---------|---------|--------------|
|
||||
| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e |
|
||||
| `contracts.md` | Contrats, validation, codes erreur | Contrats implicites, erreurs non standardisées, duplication constantes, schema orphelin, code erreur générique 409, ForbiddenException pour validation |
|
||||
| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé |
|
||||
| `stripe.md` | Stripe, paiements, webhooks, subscriptions | billing_cycle_anchor vs current_period_end, list() sans has_more, concurrence trial→payant, non-idempotence, 200 pendant processing |
|
||||
| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète |
|
||||
| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h |
|
||||
| `nextjs.md` | Next.js, build, routing | Prisma init au chargement module, server-only dans repositories, redirect boucle infinie feature flags |
|
||||
| `general.md` | Observabilité, migrations, performance | Observabilité insuffisante, migrations non reproductibles, upsert N+1 provider |
|
||||
225
knowledge/backend/risques/auth.md
Normal file
225
knowledge/backend/risques/auth.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Backend — Risques & vigilance : Auth
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-authn-authz-dispersee"></a>
|
||||
## AuthN/AuthZ dispersée (contrôles d'accès au fil de l'eau)
|
||||
|
||||
### Risques
|
||||
|
||||
- Règles de permissions incohérentes selon endpoints
|
||||
- Failles "oubliées" sur un endpoint secondaire
|
||||
- Audit impossible
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Utilisateurs qui accèdent à des ressources non prévues
|
||||
- Correctifs en urgence "on ajoute un if ici"
|
||||
- Bugs qui réapparaissent après refactor
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Centraliser authn/authz (middleware/policies)
|
||||
- Tests sur règles critiques
|
||||
- Logs/audit des décisions d'accès
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-guard-global-manquant"></a>
|
||||
## Guard global manquant (request.user jamais peuplé)
|
||||
|
||||
### Risques
|
||||
|
||||
- Chaîne auth bâtie sur une fondation inopérante (tout "a l'air OK" en dev/tests, mais casse en prod)
|
||||
- Guards aval qui dépendent de `request.user` en erreur (ou contournements involontaires)
|
||||
- Découvert tard (souvent uniquement en code review ou en prod)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `request.user` vaut `undefined` dans un guard supposé "après auth"
|
||||
- Endpoints qui passent alors qu'ils devraient être refusés (si les guards aval se désactivent/retournent true par défaut)
|
||||
- Tests "verts" car trop mockés (pas de test e2e qui valide le pipeline complet)
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Poser explicitement le guard global dès les foundations (au moins `AuthGuard`)
|
||||
- Vérifier l'ordre des `APP_GUARD` (AuthGuard avant tout guard qui lit `request.user`)
|
||||
- Ajouter au minimum 1 test d'intégration/e2e qui prouve que `request.user` est bien peuplé sur un endpoint protégé
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-guard-request-user-null"></a>
|
||||
## Guard NestJS route-level — null-check manquant sur `request.user`
|
||||
|
||||
### Risques
|
||||
|
||||
- Un guard route-level qui lit `request.user.userId` sans null-check lève une `TypeError` (500) si `request.user` est absent
|
||||
- Mauvaise registration de module, test d'intégration mal configuré, ou middleware custom peuvent produire cet état
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `TypeError: Cannot read properties of undefined (reading 'userId')` en prod
|
||||
- Tests "verts" car `request.user` mocké globalement, mais pas le guard isolé
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
const user = (request as any).user as { userId: string } | undefined;
|
||||
if (!user?.userId) {
|
||||
throw new UnauthorizedException({ error: { code: 'UNAUTHENTICATED', message: '...' } });
|
||||
}
|
||||
```
|
||||
|
||||
- **Règle** : les guards route-level ne font pas confiance aux guards globaux pour leurs invariants — ils se défendent eux-mêmes.
|
||||
- Contexte technique : NestJS v10+ — 09-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-cookie-apres-revocation-db"></a>
|
||||
## Suppression du cookie après révocation DB sur logout
|
||||
|
||||
### Risques
|
||||
|
||||
- Si la révocation DB échoue avant la suppression du cookie, l'utilisateur garde un cookie local devenu incohérent
|
||||
- L'utilisateur peut rester bloqué dans un état où il ne peut plus se déconnecter proprement
|
||||
- Le comportement diffère selon la disponibilité de la base
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Logout qui échoue par intermittence quand la DB est instable
|
||||
- Cookie de session toujours présent côté navigateur après erreur serveur
|
||||
- Réessais de logout qui produisent des états difficiles à diagnostiquer
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Toujours supprimer le cookie en premier, même si la révocation DB échoue ensuite
|
||||
- Traiter la suppression côté DB en best-effort ou avec gestion d'idempotence adaptée
|
||||
- Vérifier en test qu'un échec DB ne laisse pas l'accès browser actif
|
||||
- Contexte technique : Next.js / auth par cookie / session persistée — 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-get-sans-controle-acces"></a>
|
||||
## Endpoints GET sans contrôle d'accès sur ressource protégée
|
||||
|
||||
### Risques
|
||||
|
||||
- Un endpoint de lecture expose des données premium/protégées à tout utilisateur authentifié
|
||||
- La règle "seuls les writes vérifient les droits" est un anti-pattern qui cause des fuites silencieuses
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `getCategories`, `getThreads` ou équivalent accessible sans vérification d'entitlements
|
||||
- Endpoint write protégé par `assertForumAccess` mais GET correspondant non protégé
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Tout endpoint retournant des données liées à une ressource protégée (forum pack, contenu premium) doit appeler `assertForumAccess` ou équivalent, même pour les GET
|
||||
- **Checklist review** : pour chaque nouveau GET, vérifier qu'il passe par le guard/helper d'accès si la ressource appartient à un scope protégé
|
||||
|
||||
- Contexte technique : NestJS / app-alexandrie — 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-adminroleguard-sans-decorateur"></a>
|
||||
## NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert
|
||||
|
||||
### Risques
|
||||
|
||||
- `AdminRoleGuard.canActivate()` lit la metadata `REQUIRE_ADMIN_ROLE_KEY` posée par `@RequireAdminRole()`
|
||||
- Si le décorateur est absent, `requiresAdmin = false/undefined` → le guard retourne `true` et laisse passer sans vérification
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Endpoint admin accessible à tout utilisateur authentifié
|
||||
- Zéro erreur de compilation ou de démarrage — le bug est silencieux
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ✅ Correct — les deux décorateurs ensemble
|
||||
@Post('admin/ressource')
|
||||
@UseGuards(AdminRoleGuard)
|
||||
@RequireAdminRole()
|
||||
async createRessource(...) {}
|
||||
|
||||
// ❌ Silencieusement non protégé — @RequireAdminRole() manquant
|
||||
@Post('admin/ressource')
|
||||
@UseGuards(AdminRoleGuard)
|
||||
async createRessource(...) {}
|
||||
```
|
||||
|
||||
- Règle : s'applique à tout guard NestJS qui délègue la décision à une metadata de décorateur
|
||||
- **Checklist review** : vérifier systématiquement les endpoints admin que `@RequireAdminRole()` est présent
|
||||
|
||||
- Contexte technique : NestJS / guards metadata — app-alexandrie 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-mock-session-sans-expiresat"></a>
|
||||
## Mock Prisma session sans filtre `expiresAt` — divergence test/prod
|
||||
|
||||
### Risques
|
||||
|
||||
- Le mock `session.findFirst` omet de filtrer `expiresAt` → des sessions expirées passent en test alors qu'elles seraient rejetées en prod
|
||||
- Masque des régressions sur la logique d'expiration de session
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Tests e2e verts avec un token de session expiré
|
||||
- Bug découvert uniquement en prod quand la TTL est dépassée
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Le mock doit répliquer **tous** les critères de `getUserByToken()` en prod : `revokedAt === null` ET `expiresAt > now` :
|
||||
|
||||
```typescript
|
||||
// ✅ Mock complet fidèle à la prod
|
||||
findFirst: jest.fn().mockImplementation(({ where }) => {
|
||||
const session = store[where.accessToken];
|
||||
if (!session) return null;
|
||||
if (where.revokedAt === null && session.revokedAt !== null) return null;
|
||||
if (where.expiresAt?.gt && session.expiresAt <= where.expiresAt.gt) return null;
|
||||
return session;
|
||||
})
|
||||
```
|
||||
|
||||
- **Règle** : `seedSession()` doit initialiser `expiresAt` à +30j par défaut. Ajouter un helper `seedExpiredSession()` si des tests de session expirée sont nécessaires.
|
||||
|
||||
- Contexte technique : NestJS / Prisma mock / e2e — app-alexandrie 24-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-tests-e2e-buildapp-partage"></a>
|
||||
## Tests e2e autorisation : scénarios non-abonné avec `buildApp` partagé
|
||||
|
||||
### Risques
|
||||
|
||||
- Un `describe` e2e avec `buildApp` partagé en `beforeAll` (entitlements actifs) rend impossible le test de scénarios non-abonné sans pollution entre tests
|
||||
- Tenter de surcharger le mock partagé (`jest.fn().mockResolvedValueOnce(...)`) dans un `it` intermédiaire est fragile et crée des effets de bord
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Scénario "non-abonné → 403" n'est jamais testé, ou pollue les autres tests si le mock est modifié en cours de describe
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Créer une instance `buildApp` isolée pour les scénarios d'autorisation alternatifs :
|
||||
|
||||
```typescript
|
||||
it('retourne 403 si subscription inactive', async () => {
|
||||
const isolatedApp = await buildApp({
|
||||
getEntitlementsForUser: jest.fn().mockResolvedValue({
|
||||
subscription: { isActive: false, plan: 'free' }
|
||||
})
|
||||
});
|
||||
// ... tests
|
||||
await isolatedApp.close();
|
||||
});
|
||||
```
|
||||
|
||||
- **Règle** : ne jamais tenter de surcharger un mock partagé dans un `it` — créer un `buildApp` isolé avec `app.close()` en fin de test
|
||||
|
||||
- Contexte technique : NestJS / Jest e2e — app-alexandrie 24-03-2026
|
||||
165
knowledge/backend/risques/contracts.md
Normal file
165
knowledge/backend/risques/contracts.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Backend — Risques & vigilance : Contracts
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-contrats-api-implicites"></a>
|
||||
## Contrats API implicites (validation faible ou absente)
|
||||
|
||||
### Risques
|
||||
|
||||
- Entrées non validées → erreurs bizarres / vulnérabilités
|
||||
- Changements qui cassent le front et les intégrations
|
||||
|
||||
### Symptômes
|
||||
|
||||
- 500 sur erreurs utilisateur
|
||||
- Incohérences de format de réponse
|
||||
- "Ça marche en staging, pas en prod" (données réelles)
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Schémas (OpenAPI/JSON Schema) + validation serveur
|
||||
- Formats de réponse cohérents
|
||||
- Versionner/éviter breaking changes
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-erreurs-non-standardisees"></a>
|
||||
## Erreurs non standardisées (4xx/5xx incohérents)
|
||||
|
||||
### Risques
|
||||
|
||||
- Front et automatisations impossibles à rendre robustes
|
||||
- Debug long (pas de codes internes, pas de corrélation)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Clients qui "retry" sur des 4xx
|
||||
- Messages techniques exposés aux utilisateurs
|
||||
- Logs inexploitables
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Mapping HTTP standard + format d'erreur stable
|
||||
- Codes internes d'erreurs applicatives
|
||||
- requestId/traceId partout
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-duplication-constantes-contracts"></a>
|
||||
## Duplication silencieuse de constantes partagées (contracts) via fichier orphelin
|
||||
|
||||
### Risques
|
||||
|
||||
- Deux sources de vérité qui divergent silencieusement (ex : topics officiels, enums métier, slugs)
|
||||
- Bug non détecté par TypeScript si la duplication est dans un fichier non importé (code mort)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Incohérences entre API et client sur des listes/enums "censées être partagées"
|
||||
- "Ça marche chez moi" selon l'endroit où la constante est importée
|
||||
- Un fichier de config existe dans `apps/*` mais n'est jamais importé/greffé au runtime
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Toute constante partagée vit dans `packages/contracts/src/` et est importée depuis là (jamais recopiée dans `apps/*`)
|
||||
- En review : repérer les fichiers "config/constants" ajoutés dans `apps/*` sur des domaines déjà couverts par `contracts`
|
||||
- (Optionnel) Outillage : intégrer une étape de détection de code mort / exports inutilisés au CI si ça devient récurrent
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-contracts-schema-orphelin"></a>
|
||||
## Contracts : schema orphelin / type de retour désynchronisé
|
||||
|
||||
### Risques
|
||||
|
||||
- Un `RequestSchema` défini dans `packages/contracts` mais jamais importé dans le controller ni le service mobile → dead code silencieux qui crée une fausse confiance
|
||||
- Un type de retour inline (`string` brut) à la place du type contracts → désynchronisation silencieuse entre contrat et implémentation
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `grep` du nom du schema ne trouve aucun `import` en dehors de sa définition
|
||||
- Service retourne `Promise<{ status: string }>` au lieu de `Promise<CurationResponse>` — le `status` n'est pas validé comme `CurationStatus`
|
||||
- Endpoints `POST /action` sans body ayant un schema `{ pathParam: string }` — le param vient du path, pas du body
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
À chaque story qui ajoute des schemas dans `packages/contracts`, vérifier en review :
|
||||
|
||||
1. Chaque `RequestSchema` est utilisé dans un `ZodValidationPipe` (API) ou importé dans le service mobile.
|
||||
2. Les `ResponseSchema` correspondent au type de retour typé du service (`Promise<TheType>`, pas un type inline).
|
||||
3. Les endpoints sans body (`POST /action`) définissent `z.object({})` ou omettent le body schema — ne jamais placer les path params dans le body schema.
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — type inline, status non typé
|
||||
async showcaseThread(...): Promise<{ threadId: string; status: string }> { ... }
|
||||
|
||||
// ✅ Pattern correct — type contracts importé
|
||||
import type { CurationResponse } from '@app-alexandrie/contracts';
|
||||
async showcaseThread(...): Promise<CurationResponse> { ... }
|
||||
```
|
||||
|
||||
- Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-code-erreur-generique-409"></a>
|
||||
## Code d'erreur générique sur statut HTTP sémantique (409 CONFLICT)
|
||||
|
||||
### Risques
|
||||
|
||||
- Utiliser `VALIDATION_ERROR` ou `INTERNAL_ERROR` sur un 409 rend les erreurs indistinguables côté client et monitoring
|
||||
- Les clients (mobile, monitoring, tests) ne peuvent pas brancher une logique conditionnelle sans un code sémantique
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Tous les conflits métier remontent le même code → impossible de distinguer "alias déjà résolu" de "handle déjà pris"
|
||||
- Tests forcés à matcher le message texte au lieu du code → fragiles
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Chaque scénario métier distinct doit avoir son propre code dans `error-code.ts` :
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — code générique sur 409
|
||||
throw new ConflictException({ error: { code: 'VALIDATION_ERROR', message: '...' } });
|
||||
|
||||
// ✅ Correct — code sémantique spécifique
|
||||
throw new ConflictException({ error: { code: 'ALIAS_ALREADY_RESOLVED', message: '...' } });
|
||||
throw new ConflictException({ error: { code: 'HANDLE_ALREADY_TAKEN', message: '...' } });
|
||||
```
|
||||
|
||||
- **Règle** : 1 scénario métier distinct = 1 code d'erreur distinct
|
||||
- **Checklist review** : tout 409/422 doit avoir un code dans `error-code.ts`, jamais `VALIDATION_ERROR` ou `INTERNAL_ERROR`
|
||||
|
||||
- Contexte technique : NestJS / error-code.ts — app-alexandrie 24-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-forbidden-pour-validation"></a>
|
||||
## `ForbiddenException` (403) utilisé pour des erreurs de validation
|
||||
|
||||
### Risques
|
||||
|
||||
- Les clients qui filtrent par HTTP 400 manquent les erreurs de validation lancées en 403
|
||||
- Sémantique API incorrecte → comportements clients imprévisibles
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `ForbiddenException` lancée pour des tags invalides, des formats incorrects, des liens HTTP
|
||||
- Clients API qui ignorent ces erreurs ou les traitent comme des refus d'accès
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Tableau de correspondance :
|
||||
|
||||
| Cas | Exception correcte | Code HTTP |
|
||||
|---|---|---|
|
||||
| Tags invalides, contenu trop long, format incorrect | `BadRequestException` | 400 |
|
||||
| Accès refusé explicitement (accès forum, trial read-only) | `ForbiddenException` | 403 |
|
||||
| Quota dépassé | `HttpException(429)` via `HttpStatus.TOO_MANY_REQUESTS` | 429 |
|
||||
|
||||
- **Règle** : HTTP 403 = "tu n'as pas le droit d'effectuer cette action". HTTP 400 = "ta requête est mal formée".
|
||||
- Contexte technique : NestJS / HTTP — 20-03-2026
|
||||
72
knowledge/backend/risques/general.md
Normal file
72
knowledge/backend/risques/general.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Backend — Risques & vigilance : Général
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-observabilite-insuffisante"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-migrations-risquees"></a>
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-upsert-n-plus-un-provider"></a>
|
||||
## 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
|
||||
102
knowledge/backend/risques/nestjs.md
Normal file
102
knowledge/backend/risques/nestjs.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Backend — Risques & vigilance : NestJS
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-nestjs-toomanyrequest"></a>
|
||||
## NestJS 11 — `TooManyRequestsException` inexistante
|
||||
|
||||
### Risques
|
||||
|
||||
- `TooManyRequestsException` n'est pas exportée par `@nestjs/common` en NestJS ≥ 11
|
||||
- Erreur de compilation ou 500 si utilisée directement
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `Cannot find name 'TooManyRequestsException'` à la compilation
|
||||
- Test qui passe sur NestJS 10 mais échoue sur 11+
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// Pattern sûr pour HTTP 429
|
||||
throw new HttpException(
|
||||
{ error: { code: 'QUOTA_EXCEEDED', message: '...' } },
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
```
|
||||
|
||||
- Contexte technique : NestJS v11+ — 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-controller-corrompu-insertions"></a>
|
||||
## Controller NestJS corrompu par insertions multiples
|
||||
|
||||
### Risques
|
||||
|
||||
- Des méthodes imbriquées, décorateurs orphelins ou routes dupliquées cassent la syntaxe TypeScript sans que le compilateur ne l'attrape toujours
|
||||
- La story est marquée "completed" alors que le code ne compile pas
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `@Get('/route')` apparaît dans le corps d'une autre méthode
|
||||
- La même route est déclarée 2-3 fois dans le même controller
|
||||
- Erreur NestJS au runtime mais pas à la compilation
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Quand on ajoute >3 endpoints à un controller existant, réécrire le fichier entier en partant du fichier original
|
||||
- Ne jamais insérer par blocs séparés — la concaténation casse la structure AST
|
||||
- **Checklist review** : grep `@Get\|@Post\|@Patch\|@Delete` dans le controller et vérifier qu'aucune route n'est dupliquée
|
||||
|
||||
- Contexte technique : NestJS / TypeScript — app-alexandrie 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-repository-dead-layer"></a>
|
||||
## Repository layer non branché (dead layer)
|
||||
|
||||
### Risques
|
||||
|
||||
- Donner une impression de sécurité alors que le code métier continue d'appeler l'ORM directement
|
||||
- Multiplier les chemins d'accès aux données avec des règles différentes
|
||||
- Payer le coût d'une abstraction qui n'a aucun effet réel
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Un repository est créé mais les anciens call sites Prisma restent en place
|
||||
- Les nouvelles règles de scoping ou de sécurité ne s'appliquent pas partout
|
||||
- La review montre des fichiers de repository peu ou jamais importés
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Vérifier qu'une nouvelle couche d'abstraction est réellement branchée dans les call sites existants
|
||||
- Rechercher explicitement les appels directs restants lors de la review
|
||||
- Refuser l'introduction d'une couche repository tant que la migration effective n'est pas faite
|
||||
- Contexte technique : TypeScript / Prisma / refactor d'accès aux données — 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-interface-provider-incomplete"></a>
|
||||
## Interface provider incomplète ou divergente de ses implémentations
|
||||
|
||||
### Risques
|
||||
|
||||
- Une implémentation expose des méthodes non déclarées dans le contrat commun
|
||||
- Les appelants contournent l'interface et se couplent à un provider concret
|
||||
- Une stratégie provider devient non interchangeable en pratique
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Appels avec cast ou accès direct à une implémentation spécifique
|
||||
- Méthodes présentes dans une classe mais absentes de l'interface
|
||||
- Régression lors d'un changement de provider
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Toute capacité commune attendue par les appelants doit être déclarée dans l'interface
|
||||
- Interdire les méthodes "cachées" consommées hors contrat
|
||||
- Tester au moins une implémentation par le contrat abstrait
|
||||
- Contexte technique : TypeScript / provider strategy — 10-03-2026
|
||||
75
knowledge/backend/risques/nextjs.md
Normal file
75
knowledge/backend/risques/nextjs.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Backend — Risques & vigilance : Next.js
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-prisma-init-module-build"></a>
|
||||
## Prisma initialisé au chargement de module — casse le build Next.js
|
||||
|
||||
### Risques
|
||||
|
||||
- Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si `DATABASE_URL` n'est pas disponible dans l'environnement de build
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `PrismaClientInitializationError` ou `Error: Environment variable not found: DATABASE_URL` au `next build`
|
||||
- L'app tourne en dev mais le build CI échoue
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier
|
||||
- Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB
|
||||
- Ne jamais instancier `new PrismaClient()` au top-level d'un module importé par Next.js
|
||||
|
||||
- Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-server-only-repositories-tests"></a>
|
||||
## `server-only` dans les repositories — bloque les tests unitaires
|
||||
|
||||
### Risques
|
||||
|
||||
- `import "server-only"` empêche l'exécution des fichiers hors runtime Next.js
|
||||
- Les tests Node.js échouent avec `Error: This module cannot be imported from a Client Component module`
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Tests qui passent via le dev server mais échouent via `jest` en mode node
|
||||
- Erreur au `require()` d'un repository depuis un test unitaire
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Ne mettre `server-only` que dans les fichiers qui utilisent des APIs Next.js runtime (`cookies()`, `headers()`, `redirect()`)
|
||||
- **Ne pas** mettre `server-only` dans les repositories purs (qui n'appellent que Prisma)
|
||||
- Alternative de secours : créer un stub `node_modules/server-only/index.js` no-op pour les tests
|
||||
|
||||
- Contexte technique : Next.js App Router / Jest — app-template-resto 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-redirect-boucle-infinie"></a>
|
||||
## Redirect vers la page désactivée elle-même (boucle infinie feature flags)
|
||||
|
||||
### Risques
|
||||
|
||||
- Une page désactivée redirige vers elle-même via le fallback — boucle infinie silencieuse absorbée par Next.js mais UX cassée
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Page `/` désactivée → redirect vers `buildLocalizedPath("home")` = `/` → boucle
|
||||
- Next.js absorbe la boucle mais l'utilisateur voit un écran bloqué ou vide
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// Si la page est sa propre destination de fallback, ne pas rediriger
|
||||
if (pageKey === "home") return null; // évite redirect home → home
|
||||
return buildLocalizedPath(locale, "home");
|
||||
```
|
||||
|
||||
- Règle : dans tout mécanisme de redirection sur page désactivée, toujours vérifier que `pageKey !== fallbackKey`
|
||||
- Retourner `null` (accès non bloqué) plutôt que de boucler
|
||||
|
||||
- Contexte technique : Next.js App Router / feature flags tenant — app-template-resto 17-03-2026
|
||||
306
knowledge/backend/risques/prisma.md
Normal file
306
knowledge/backend/risques/prisma.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Backend — Risques & vigilance : Prisma
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-prisma-unique-nullable"></a>
|
||||
## PostgreSQL / Prisma : `@unique` sur champ nullable (idempotence cassée)
|
||||
|
||||
### Risques
|
||||
|
||||
- Doublons en base malgré un "unique" attendu (PostgreSQL autorise plusieurs `NULL` dans un index UNIQUE)
|
||||
- Upserts non idempotents si la clé peut être `null` (`where: { externalId: null }` crée plusieurs lignes)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Plusieurs enregistrements "équivalents" avec `externalId = NULL`
|
||||
- Rejouer un webhook / retry réseau crée une nouvelle ligne au lieu d'upsert
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Toute clé utilisée dans un `where` d'`upsert` doit être **non-nullable**
|
||||
- Si un identifiant externe peut légitimement être `null`, ne pas l'utiliser comme clé d'idempotence : choisir une autre clé unique non-nullable
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-prisma-transaction-toctou-tenantid"></a>
|
||||
## Prisma `$transaction` : fenêtres TOCTOU (check hors transaction)
|
||||
|
||||
### Risques
|
||||
|
||||
- Un pre-check + une `$transaction` avec un `update` non sécurisé crée une fenêtre TOCTOU
|
||||
- Deux appels concurrents peuvent tous deux passer le check et agir simultanément
|
||||
- En multi-tenant : un bug upstream peut permettre une écriture cross-tenant malgré le guard applicatif
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Double action sur un état booléen (ex : double mise en vitrine) si le check n'est pas dans la transaction
|
||||
- Écriture sur une ressource d'un autre tenant possible en race condition
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
**Cas 1 — Multi-tenant : inclure `tenantId` dans chaque écriture**
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern — check OK mais écriture sans tenantId
|
||||
const existing = await prisma.item.findMany({ where: { id: { in: ids }, tenantId } });
|
||||
await prisma.$transaction(
|
||||
ids.map((id, idx) => prisma.item.update({ where: { id }, data: { sortOrder: idx + 1 } }))
|
||||
);
|
||||
|
||||
// ✅ Défense en profondeur — tenantId dans chaque écriture
|
||||
await prisma.$transaction(
|
||||
ids.map((id, idx) => prisma.item.updateMany({ where: { id, tenantId }, data: { sortOrder: idx + 1 } }))
|
||||
);
|
||||
```
|
||||
|
||||
- Règle : toute écriture Prisma sur une ressource tenant-aware doit inclure `tenantId` dans le WHERE, même dans une transaction précédée d'un check
|
||||
- Utiliser `updateMany`/`deleteMany` pour inclure `tenantId` sans exception si 0 lignes
|
||||
|
||||
**Cas 2 — Idempotence / plafond : re-check d'état à l'intérieur de la transaction**
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern : check d'état hors transaction
|
||||
if (resource.isActive) throw ...;
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// resource.isActive a pu changer entre-temps
|
||||
return tx.resource.update(...);
|
||||
});
|
||||
|
||||
// ✅ Pattern correct : check ET update dans la transaction
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const current = await tx.resource.findUnique({ where: { id } });
|
||||
if (current?.isActive) throw ...; // re-check atomique
|
||||
const count = await tx.resource.count(...);
|
||||
if (count >= LIMIT) throw ...;
|
||||
return tx.resource.update(...);
|
||||
});
|
||||
```
|
||||
|
||||
- Règle : tout guard métier de type "déjà fait / plafond atteint" doit être vérifié à l'intérieur de la transaction, pas avant
|
||||
|
||||
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026 ; NestJS / Prisma — app-alexandrie 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-prisma-or-tenantid-null"></a>
|
||||
## Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système
|
||||
|
||||
### Risques
|
||||
|
||||
- Sur un modèle à `tenantId` nullable distinguant ressources "système" et "tenant", un filtre `{ isSystem: true }` sans `tenantId: null` expose des ressources corrompues à tous les tenants
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Un tag `isSystem: true` avec `tenantId` non-null est exposé à tous les tenants
|
||||
- Bug de sécurité difficile à détecter car le comportement nominal semble correct
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Trop permissif
|
||||
OR: [{ isSystem: true }, { tenantId, isSystem: false }]
|
||||
|
||||
// ✅ Défense en profondeur — double condition sur la branche système
|
||||
OR: [{ isSystem: true, tenantId: null }, { tenantId, isSystem: false }]
|
||||
```
|
||||
|
||||
- Règle : sur tout modèle `tenantId?` (nullable) + flag `isSystem`/`isGlobal`/`isPublic`, la branche "ressource publique" du filtre OR doit toujours inclure `tenantId: null`
|
||||
|
||||
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-nextorder-hors-transaction"></a>
|
||||
## Calcul de `nextOrder` hors transaction (race condition `sortOrder`)
|
||||
|
||||
### Risques
|
||||
|
||||
- Deux requêtes concurrentes obtiennent le même `MAX(sortOrder)` et créent deux entités avec le même `sortOrder`
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Deux items avec le même `sortOrder` dans la même catégorie/scope
|
||||
- Bug aléatoire selon la charge — invisible en dev, présent en prod
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ✅ Calcul dans la transaction interactive
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const maxOrder = await tx.entity.aggregate({
|
||||
where: { tenantId, scopeId },
|
||||
_max: { sortOrder: true },
|
||||
});
|
||||
const nextOrder = (maxOrder._max.sortOrder ?? 0) + 1;
|
||||
return tx.entity.create({ data: { ..., sortOrder: nextOrder } });
|
||||
});
|
||||
```
|
||||
|
||||
- Règle : ne jamais calculer `maxOrder` hors de la transaction qui crée l'entité
|
||||
|
||||
- Contexte technique : Prisma / transactions — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-tenantid-sans-fk-relation"></a>
|
||||
## Champ `tenantId` sans FK ni relation Prisma vers `Tenant`
|
||||
|
||||
### Risques
|
||||
|
||||
- Un `tenantId TEXT NOT NULL` sans relation Prisma ne génère aucune FK en DB
|
||||
- L'isolation multi-tenant n'est pas enforced au niveau base de données
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Migration SQL sans `ALTER TABLE ... ADD CONSTRAINT ... REFERENCES "tenants"`
|
||||
- Prisma ne génère pas de FK automatiquement sans `@relation` déclarée
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Tout modèle tenant-scoped doit avoir les trois :
|
||||
1. `tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)` dans le modèle Prisma
|
||||
2. La relation inverse dans `Tenant` (ex: `menuCategories MenuCategory[]`)
|
||||
3. La FK correspondante dans la migration SQL
|
||||
|
||||
- **Checklist review** : vérifier systématiquement que les nouveaux modèles respectent ce guardrail
|
||||
|
||||
- Contexte technique : Prisma / multi-tenant — app-template-resto 17-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-schema-divergence-spec-story"></a>
|
||||
## Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)
|
||||
|
||||
### Risques
|
||||
|
||||
- Une tâche de story cochée ✅ implique un champ (ex: `consumedAt`, `tokenHash`) qui n'existe pas dans `schema.prisma`
|
||||
- Le code compile ou passe en review sans que le champ soit réellement présent en DB
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Erreur à l'exécution sur un champ inexistant malgré une story marquée "done"
|
||||
- `schema.prisma` ne contient pas le champ mentionné dans les tâches
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Avant de marquer une tâche ✅, croiser avec `schema.prisma` pour confirmer que le champ existe réellement
|
||||
- Une story peut décrire un champ comme stratégie de conception sans l'avoir intégré — toujours vérifier
|
||||
|
||||
- Contexte technique : Prisma / app-template-resto — 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-prismaservice-getter-manquant"></a>
|
||||
## PrismaService — getter explicite manquant sur nouveau modèle
|
||||
|
||||
### Risques
|
||||
|
||||
- L'ajout d'un modèle dans `schema.prisma` sans son getter dans `PrismaService` casse le typecheck
|
||||
- Erreur silencieuse si les modules sont peu typés
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `Property 'forum' does not exist on type 'PrismaService'` à la compilation
|
||||
- Module fonctionnel sur le `PrismaClient` direct mais cassé via `PrismaService`
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
Tout ajout de modèle Prisma = **deux actions** :
|
||||
|
||||
1. Ajouter le modèle dans `schema.prisma`
|
||||
2. Ajouter le getter dans `prisma.service.ts`
|
||||
|
||||
```typescript
|
||||
// apps/api/src/infra/prisma/prisma.service.ts
|
||||
get forum() {
|
||||
return this.client.forum;
|
||||
}
|
||||
```
|
||||
|
||||
- **Checklist review** : à chaque nouvelle migration Prisma, vérifier que `prisma.service.ts` est mis à jour.
|
||||
- Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-prisma-init-module-build"></a>
|
||||
## Prisma initialisé au chargement de module — casse le build Next.js
|
||||
|
||||
### Risques
|
||||
|
||||
- Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si `DATABASE_URL` n'est pas disponible dans l'environnement de build
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `PrismaClientInitializationError` ou `Error: Environment variable not found: DATABASE_URL` au `next build`
|
||||
- L'app tourne en dev mais le build CI échoue
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier
|
||||
- Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB
|
||||
- Ne jamais instancier `new PrismaClient()` au top-level d'un module importé par Next.js
|
||||
|
||||
- Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-jest-clearallmocks-imbrique"></a>
|
||||
## `jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma
|
||||
|
||||
### Risques
|
||||
|
||||
- Remise à zéro d'un setup attendu par un scope de test plus profond
|
||||
- Tests verts ou rouges pour de mauvaises raisons
|
||||
- Forte difficulté à comprendre l'état réel des mocks
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Comportement différent selon l'ordre ou le niveau d'imbrication des `describe`
|
||||
- Mocks Prisma "perdus" entre deux tests
|
||||
- Corrections locales qui cassent d'autres blocs de tests
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Centraliser la stratégie de reset des mocks
|
||||
- Éviter les `clearAllMocks()` concurrents à plusieurs niveaux de nesting
|
||||
- Préférer un setup explicite et local par scénario quand les mocks Prisma sont structurants
|
||||
- Contexte technique : Jest / Prisma / tests NestJS — 10-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-cursor-pagination-opaque"></a>
|
||||
## Cursor de pagination opaque — validation manquante (500 au lieu de 400)
|
||||
|
||||
### Risques
|
||||
|
||||
- Un cursor base64url+JSON non validé crash en HTTP 500 si malformé ou corrompu
|
||||
- Exposé à des attaques par input malveillant sur les endpoints paginés publics ou semi-publics
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `JSON.parse` ou décodage base64 lève une exception non catchée → 500 en prod
|
||||
- Les logs montrent une stack trace sur un endpoint paginé avec un cursor externe
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ DANGEREUX — crash 500 si cursor corrompu
|
||||
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());
|
||||
|
||||
// ✅ CORRECT — validation avec code d'erreur sémantique
|
||||
let decoded = null;
|
||||
if (cursor) {
|
||||
try {
|
||||
decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());
|
||||
if (!decoded.createdAt || !decoded.id) throw new Error('Champs manquants');
|
||||
} catch {
|
||||
throw new BadRequestException({ error: { code: 'INVALID_CURSOR', message: 'Cursor de pagination invalide.' } });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Règle** : ajouter un test unitaire "cursor invalide → 400" sur tout endpoint paginé par cursor
|
||||
|
||||
- Contexte technique : NestJS / pagination — app-alexandrie 24-03-2026
|
||||
105
knowledge/backend/risques/redis.md
Normal file
105
knowledge/backend/risques/redis.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Backend — Risques & vigilance : Redis
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-redis-thrash-connexion"></a>
|
||||
## Redis — thrash de connexion sous charge
|
||||
|
||||
### Risques
|
||||
|
||||
- Connexions concurrentes multiples si `connect()` est appelé "à la demande" sans lock
|
||||
- Spam logs + saturation connexions quand Redis est down ou lent
|
||||
|
||||
### Symptômes
|
||||
|
||||
- N appels simultanés → N tentatives de connexion en parallèle
|
||||
- Logs "Redis connection failed" en rafale au démarrage ou lors d'un restart Redis
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// Pattern single-flight + cooldown + fallback DB best-effort
|
||||
if (!this.connectPromise) {
|
||||
this.connectPromise = this.client.connect().finally(() => { this.connectPromise = null; });
|
||||
}
|
||||
await this.connectPromise;
|
||||
// Si échec → nextConnectRetryAtMs = now + 1000 → return false → fallback DB
|
||||
```
|
||||
|
||||
- Contexte technique : Redis / NestJS — 09-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-entitlements-ttl-sla"></a>
|
||||
## Entitlements — TTL cache supérieur au SLA de propagation
|
||||
|
||||
### Risques
|
||||
|
||||
- TTL cache > SLA propagation → un webhook raté viole mécaniquement le SLA (accès stale plus long que garanti)
|
||||
- Utilisateur avec accès périmé ou sans accès dû, pendant toute la durée du TTL résiduel
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Accès premium encore actif après annulation (ou inversement)
|
||||
- NFR "propagation ≤ 60s" non respecté en cas de webhook manqué
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- TTL cache ≤ SLA cible (ex : NFR "≤ 60s" → TTL = 60s max)
|
||||
- Toujours coupler TTL + invalidation explicite via webhook (les deux, pas l'un ou l'autre)
|
||||
- Contexte technique : Redis / entitlements / NestJS — 09-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-compteurs-inmemory"></a>
|
||||
## Compteurs in-memory ≠ métriques persistées
|
||||
|
||||
### Risques
|
||||
|
||||
- Compteurs in-memory remis à zéro au restart (perte de données)
|
||||
- Non agrégables sur plusieurs instances (données partielles par pod)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Métriques qui "repartent de 0" à chaque déploiement
|
||||
- Dashboards incorrects en environnement multi-instance
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- V1 low-cost : `Redis INCRBY` best-effort par `eventType` → persisté et agrégé multi-instances
|
||||
- Évolutif vers Prometheus/OTel sans changer l'interface (abstraction dès le départ)
|
||||
- Contexte technique : Redis / NestJS — 09-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-ttl-redis-heure-locale"></a>
|
||||
## TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)
|
||||
|
||||
### Risques
|
||||
|
||||
- Le reset du quota journalier dérive selon le timezone du serveur, pouvant aller jusqu'à ±12h d'écart par rapport à minuit UTC
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Quota qui se remet à zéro à des heures inattendues selon l'environnement de déploiement
|
||||
- Comportement différent en dev local (TZ machine) et en prod (TZ container)
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT — UTC midnight garanti
|
||||
const midnight = new Date(
|
||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
|
||||
);
|
||||
const ttlMs = midnight.getTime() - now.getTime();
|
||||
|
||||
// ❌ RISQUÉ — heure locale du serveur
|
||||
const endOfDay = new Date();
|
||||
endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
|
||||
```
|
||||
|
||||
- Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` — vérifier systématiquement en review
|
||||
|
||||
- Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026
|
||||
116
knowledge/backend/risques/stripe.md
Normal file
116
knowledge/backend/risques/stripe.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Backend — Risques & vigilance : Stripe
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-stripe-current-period-end"></a>
|
||||
## Stripe (v17+) : confusion `billing_cycle_anchor` vs `current_period_end`
|
||||
|
||||
### Risques
|
||||
|
||||
- Stocker une date de fin de période incorrecte en DB (bug silencieux)
|
||||
- État d'abonnement incohérent (UI, relances, accès premium)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `currentPeriodEnd` correspond à une date "bizarre" (souvent proche de la création), ou à un jour du mois
|
||||
- Des accès premium expirent trop tôt / trop tard
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Ne jamais interpréter `billing_cycle_anchor` comme une date de fin de période
|
||||
- Utiliser `subscription.current_period_end` (timestamp) pour la fin de période courante
|
||||
- Ajouter un test sur un événement webhook/Subscription qui vérifie la date persistée
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-stripe-list-has-more"></a>
|
||||
## Stripe `list()` sans gestion de `has_more`
|
||||
|
||||
### Risques
|
||||
|
||||
- Pagination tronquée silencieusement
|
||||
- Réconciliation incomplète d'abonnements, achats ou moyens de paiement
|
||||
- Décisions métier prises sur un jeu de données partiel
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Comportement correct sur petits comptes mais faux sur comptes plus chargés
|
||||
- Premiers éléments traités, les suivants ignorés
|
||||
- Absence de boucle de pagination ou d'auto-pagination
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Traiter explicitement `has_more`
|
||||
- Utiliser l'auto-pagination Stripe si adaptée
|
||||
- Tester au moins un cas avec plusieurs pages de résultats
|
||||
- Contexte technique : Stripe API — 10-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-trial-payant-concurrence"></a>
|
||||
## Concurrence entre activation locale et webhook sur transition trial → payant
|
||||
|
||||
### Risques
|
||||
|
||||
- Double création ou double attachement d'une ressource unique
|
||||
- Conflit `P2002`
|
||||
- État local différent de l'état Stripe pendant la transition
|
||||
|
||||
### Symptômes
|
||||
|
||||
- La transition fonctionne parfois, puis échoue aléatoirement
|
||||
- Un webhook Stripe et une action applicative écrivent la même mutation métier
|
||||
- Erreurs d'unicité lors de l'activation payante
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Définir une seule source autorisée pour chaque transition d'état
|
||||
- Rendre les écritures idempotentes
|
||||
- Sérialiser ou réconcilier explicitement les transitions pilotées à la fois par action utilisateur et webhook
|
||||
- Contexte technique : Stripe / Prisma / trial subscription — 10-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-non-idempotence"></a>
|
||||
## Non-idempotence sur opérations sensibles
|
||||
|
||||
### Risques
|
||||
|
||||
- Doubles paiements / doubles créations
|
||||
- Webhooks rejoués qui cassent l'état
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Doublons de lignes en DB
|
||||
- Actions exécutées 2 fois après timeout/retry
|
||||
- Incidents difficiles à reproduire
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Idempotency key sur endpoints critiques
|
||||
- Protection anti-doublon côté DB (contraintes uniques)
|
||||
- Comportement défini en cas de retry
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-webhook-200-processing"></a>
|
||||
## Webhooks entrants — répondre 200 pendant `processing` (event perdu)
|
||||
|
||||
### Risques
|
||||
|
||||
- Le provider (Stripe, etc.) arrête ses retries après un 2xx, même si le premier worker a échoué
|
||||
- Event non appliqué mais marqué "traité" → état incohérent silencieux
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Webhook reçu, 200 retourné, mais l'état en base n'est pas mis à jour
|
||||
- Aucun retry du provider → impossible à détecter sans monitoring actif
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Lock DB (`WebhookEvent`) avec machine d'état : `pending` → `processing` → `processed` / `failed`
|
||||
- Si `processing` détecté (concurrent) : attendre brièvement la transition `processed`, sinon répondre **non-2xx** (force retry provider)
|
||||
- Ne jamais passer à `processed` sans preuve d'un traitement effectif
|
||||
- Contexte technique : Stripe / NestJS — 09-03-2026
|
||||
Reference in New Issue
Block a user