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/patterns/README.md
Normal file
18
knowledge/backend/patterns/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Backend — Patterns validés — Index
|
||||
|
||||
Patterns backend testés et validés en conditions réelles.
|
||||
|
||||
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, tokens, erreurs API, corrélation | Format erreur standardisé, middleware requestId, anti-énumération, token usage unique, autorisation interne, opérations atomiques |
|
||||
| `contracts.md` | Contrats API, Zod, error codes, HTTP sémantique | Contracts-First/Zod-Infer/No-DTO, error codes comme contrat, HTTP 200 payload métier |
|
||||
| `prisma.md` | Prisma, DB, migrations, pagination | Soft delete, pagination cursor, idempotency key, P2002 unique, Decimal sérialisation, migration manuelle P3014, filtrage métier dans service |
|
||||
| `stripe.md` | Stripe, paiements, webhooks entrants, subscriptions | Provider-Strategy, metadata subscription_data, parsing webhook unique, restauration achats, Trial vs Paid |
|
||||
| `nestjs.md` | NestJS, guards, Redis, quotas | Guard global APP_GUARD, RedisHealthService cache court, quota INCR+EXPIREAT atomique |
|
||||
| `multi-tenant.md` | Multi-tenant, isolation, feature flags | 403 vs 404, repository tenant-aware, tenantId dans updates, helper tenant partagé, feature flag tenant, EN enforcement |
|
||||
| `nextjs.md` | Next.js App Router, Server Actions, isolation | Runtime-only logique pure, server-only isolation, utilitaires purs sans server-only, réutiliser champ V1, validation URL externe |
|
||||
| `async.md` | Jobs async, webhooks sortants, queues | Exécution asynchrone outbox light, webhooks sortants HMAC + retries idempotents |
|
||||
79
knowledge/backend/patterns/async.md
Normal file
79
knowledge/backend/patterns/async.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Backend — Patterns : Async
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-execution-asynchrone-taches-longues"></a>
|
||||
|
||||
## Pattern : Exécution asynchrone des tâches longues (queue + outbox light)
|
||||
|
||||
- Objectif : sortir les opérations longues ou fragiles du chemin request/response.
|
||||
- Contexte : envoi d'emails, appels SaaS, génération de PDF, traitements batch, webhooks sortants.
|
||||
- Quand l'utiliser : dès qu'une opération peut dépasser la latence acceptable ou dépendre d'un service externe.
|
||||
- Quand l'éviter : opérations réellement instantanées et sans dépendances externes.
|
||||
- Avantage :
|
||||
- API plus rapide et plus fiable
|
||||
- Retries maîtrisés
|
||||
- Meilleure résilience aux pannes externes
|
||||
- Limites / vigilance :
|
||||
- Demande une discipline stricte sur l'idempotence
|
||||
- Nécessite une stratégie minimale de dead-letter ou d'alerting
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : Backend agnostique + DB transactionnelle + worker
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- API écrit un job ou event en DB dans la transaction métier
|
||||
- Worker lit les jobs en attente et exécute
|
||||
- Retries avec backoff + compteur
|
||||
- Statut FAILED ou dead-letter + alerte
|
||||
- Idempotence par clé métier ou idempotency key
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Job créé dans une transaction (évite les pertes)
|
||||
- Retries et backoff définis
|
||||
- Dead-letter ou statut FAILED visible
|
||||
- Idempotence garantie
|
||||
- Logs corrélés (requestId/traceId)
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-webhooks-sortants-robustes-idempotents"></a>
|
||||
|
||||
## Pattern : Webhooks sortants robustes et idempotents
|
||||
|
||||
- Objectif : garantir des intégrations fiables avec des systèmes externes.
|
||||
- Contexte : notifications, synchronisations, événements métier sortants.
|
||||
- Quand l'utiliser : dès qu'un événement doit être transmis à un tiers.
|
||||
- Quand l'éviter : intégrations strictement synchrones et internes.
|
||||
- Avantage :
|
||||
- Tolérance aux pannes réseau
|
||||
- Retries maîtrisés
|
||||
- Observabilité des échecs
|
||||
- Limites / vigilance :
|
||||
- Gestion des retries et du volume
|
||||
- Nécessite une idempotence côté consommateur
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : Backend + HTTP + worker/queue
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Événement persisté (outbox) en DB
|
||||
- Envoi asynchrone via worker
|
||||
- Retries avec backoff
|
||||
- Signature du payload (HMAC)
|
||||
- Idempotency key dans le header
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Payload signé et vérifiable
|
||||
- Retries + backoff définis
|
||||
- Dead-letter ou statut FAILED visible
|
||||
- Idempotence documentée
|
||||
- Logs corrélés (requestId/traceId)
|
||||
230
knowledge/backend/patterns/auth.md
Normal file
230
knowledge/backend/patterns/auth.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Backend — Patterns : Auth
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-format-derreur-api-standardise"></a>
|
||||
|
||||
## Pattern : Format d'erreur API standardisé
|
||||
|
||||
- Objectif : fournir des erreurs prévisibles, exploitables et cohérentes pour tous les clients.
|
||||
- Contexte : API consommée par front-end, automatisations ou intégrations externes.
|
||||
- Quand l'utiliser : dès qu'une API est exposée à autre chose qu'un usage interne trivial.
|
||||
- Quand l'éviter : jamais.
|
||||
- Avantage :
|
||||
- Debug plus rapide
|
||||
- UX maîtrisée côté client
|
||||
- Observabilité améliorée
|
||||
- Limites / vigilance :
|
||||
- Discipline requise pour éviter les formats ad hoc
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API HTTP agnostique
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "USER_NOT_FOUND",
|
||||
"message": "Utilisateur introuvable",
|
||||
"requestId": "abc-123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Codes HTTP cohérents (4xx / 5xx)
|
||||
- Codes d'erreur applicatifs stables
|
||||
- Message utilisateur non technique
|
||||
- requestId présent
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-middleware-correlation-requestid-traceid"></a>
|
||||
|
||||
## Pattern : Middleware de corrélation (requestId / traceId)
|
||||
|
||||
- Objectif : relier chaque requête aux logs et erreurs associées.
|
||||
- Contexte : toute API ou service exposé.
|
||||
- Quand l'utiliser : systématiquement en production.
|
||||
- Quand l'éviter : jamais.
|
||||
- Avantage :
|
||||
- MTTR réduit drastiquement
|
||||
- Debug cross-services possible
|
||||
- Limites / vigilance :
|
||||
- Doit être propagé partout (logs, erreurs, appels sortants)
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : Backend agnostique (HTTP)
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Générer un requestId à l'entrée si absent
|
||||
- Le propager dans le contexte de requête
|
||||
- L'inclure dans chaque log et réponse d'erreur
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- requestId généré ou repris d'un header existant
|
||||
- Présent dans tous les logs
|
||||
- Présent dans les erreurs retournées
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-anti-enumeration-auth-email"></a>
|
||||
## Pattern : Anti-énumération sur endpoints auth liés à un email
|
||||
|
||||
- Objectif : empêcher qu'un endpoint auth révèle si un compte existe, n'existe pas ou n'est pas éligible.
|
||||
- Contexte : reset de mot de passe, invitation, vérification de compte, login ou tout flux qui part d'un email utilisateur.
|
||||
- Quand l'utiliser : dès qu'une requête auth touche un identifiant de type email.
|
||||
- Quand l'éviter : jamais sur une surface exposée.
|
||||
- Avantage :
|
||||
- réduit la fuite d'information sur les comptes existants
|
||||
- homogénéise les réponses côté client
|
||||
- se combine bien avec les garde-fous anti-abus
|
||||
- Limites / vigilance :
|
||||
- ne protège pas seul contre le brute-force, à combiner avec du rate-limiting
|
||||
- les logs internes doivent conserver la vraie cause sans l'exposer au client
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Node.js / auth applicative / API HTTP
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- retourner la même réponse HTTP 200 qu'un compte existe ou non
|
||||
- ne jamais distinguer "email inconnu", "email connu" ou "compte OAuth-only" dans la réponse
|
||||
- journaliser la cause réelle côté serveur
|
||||
- ajouter un rate-limiting basé sur email + IP
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Réponse client uniforme pour les cas compte connu/inconnu/non éligible
|
||||
- Aucune fuite d'existence dans le message ou le code d'erreur
|
||||
- Rate-limiting présent sur les endpoints exposés
|
||||
- Logs internes exploitables
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-token-usage-unique"></a>
|
||||
## Pattern : Token à usage unique — génération, hash et invalidation atomique
|
||||
|
||||
- Objectif : standardiser la création et la consommation de tokens sensibles sans stocker de secret brut en base.
|
||||
- Contexte : invitation, reset de mot de passe, vérification d'email, lien magique ou tout token one-shot.
|
||||
- Quand l'utiliser : pour tout token à usage unique transmis à l'utilisateur.
|
||||
- Quand l'éviter : sessions longues ou secrets devant être relus en clair côté serveur.
|
||||
- Avantage :
|
||||
- réduit l'impact d'une fuite de base
|
||||
- garde des tokens URL-safe
|
||||
- favorise une consommation atomique et réutilisable
|
||||
- Limites / vigilance :
|
||||
- la consommation doit rester atomique
|
||||
- la politique d'expiration doit être explicite
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Node.js `crypto` / Prisma / email ou URL signée
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- générer le token avec `crypto.randomBytes(32).toString("base64url")`
|
||||
- stocker uniquement le hash SHA-256 du token en base
|
||||
- transmettre le token brut uniquement via URL ou email
|
||||
- recalculer le hash côté serveur lors de la consommation
|
||||
- invalider le token dans une transaction atomique après usage
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Token brut jamais persisté en base
|
||||
- Hash recalculé côté serveur pour la vérification
|
||||
- Expiration explicite
|
||||
- Invalidation atomique après consommation
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-autorisation-interne-minimale"></a>
|
||||
|
||||
## Pattern : Autorisation interne minimale sans RBAC complet
|
||||
|
||||
- Objectif : sécuriser une capacité interne sensible sans ouvrir trop tôt un chantier RBAC complet.
|
||||
- Contexte : application avec peu de rôles, besoin ponctuel d'une capacité admin ou opérateur clairement identifiée.
|
||||
- Quand l'utiliser : quand une story métier demande un pouvoir interne limité mais réel.
|
||||
- Quand l'éviter : si les permissions deviennent nombreuses, hiérarchiques ou contextuelles.
|
||||
- Avantage :
|
||||
- sécurisation rapide et lisible d'une capacité sensible
|
||||
- source de vérité backend explicite
|
||||
- chemin d'évolution propre vers un RBAC plus complet
|
||||
- Limites / vigilance :
|
||||
- ne pas laisser proliférer des rôles ad hoc non gouvernés
|
||||
- ne remplace pas un vrai modèle de permissions si le domaine grossit
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : NestJS / auth par session ou JWT / API métier interne
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- introduire un enum de rôle minimal côté backend (ex. USER | ADMIN)
|
||||
- propager ce rôle dans la session ou le token d'auth
|
||||
- créer un décorateur + guard dédiés pour la capacité sensible
|
||||
- interdire les booléens front, emails hardcodés ou `if` dispersés dans les contrôleurs
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Le rôle vit dans la source de vérité backend
|
||||
- Le rôle est propagé dans le mécanisme d'auth existant
|
||||
- Les endpoints sensibles passent par un guard dédié
|
||||
- Aucun contrôle d'accès critique n'est piloté par le front
|
||||
- Le passage à RBAC reste possible sans casser le contrat existant
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-auth-operations-atomiques"></a>
|
||||
## Pattern : Opérations auth sensibles — atomiques, idempotentes et cohérentes
|
||||
|
||||
- Objectif : garantir que les opérations multi-étapes auth (reset, logout, révocation) ne laissent jamais un état incohérent.
|
||||
- Contexte : tout flux auth qui combine plusieurs writes : hash de mot de passe, invalidation de token, suppression de session.
|
||||
- Quand l'utiliser : systématiquement sur toute opération qui touche plusieurs tables auth en séquence.
|
||||
- Quand l'éviter : opérations de lecture pure.
|
||||
- Avantage :
|
||||
- pas de token valide après reset de mot de passe si l'opération est interrompue
|
||||
- suppression de session idempotente (P2025 absorbé silencieusement)
|
||||
- comportement prévisible même en cas de retry ou de concurrence
|
||||
- Limites / vigilance :
|
||||
- `$transaction` Prisma ne couvre pas les effets de bord réseau (email, cookies) — ces étapes restent hors transaction
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Node.js / Prisma / auth par session ou token
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// consumePasswordReset — atomique dans une transaction
|
||||
await prisma.$transaction([
|
||||
prisma.passwordResetToken.update({
|
||||
where: { tokenHash },
|
||||
data: { consumedAt: new Date() },
|
||||
}),
|
||||
prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { passwordHash: newHash },
|
||||
}),
|
||||
prisma.session.deleteMany({ where: { userId } }),
|
||||
]);
|
||||
|
||||
// Suppression de session — idempotente (P2025 absorbé)
|
||||
try {
|
||||
await prisma.session.delete({ where: { sessionToken } });
|
||||
} catch (err) {
|
||||
if (err?.code !== 'P2025') throw err; // session déjà supprimée → OK
|
||||
}
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Toute opération hash + update + delete dans une `$transaction`
|
||||
- [ ] `P2025` absorbé silencieusement sur les suppressions de session
|
||||
- [ ] Effets de bord hors transaction documentés (cookie, email)
|
||||
- [ ] Tests couvrant le cas d'une session déjà expirée
|
||||
138
knowledge/backend/patterns/contracts.md
Normal file
138
knowledge/backend/patterns/contracts.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Backend — Patterns : Contracts
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-contracts-first-zod-infer-no-dto"></a>
|
||||
|
||||
## Pattern : Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)
|
||||
|
||||
- Objectif : avoir une seule source de vérité pour les contrats d'interface entre API et client, sans redéfinition manuelle de types.
|
||||
- Contexte : monorepo TypeScript avec un package partagé (`packages/contracts` ou équivalent), consommé par le backend et le front/mobile.
|
||||
- Quand l'utiliser : dès qu'une API est consommée par un client TypeScript dans le même repo.
|
||||
- Quand l'éviter : si le client est externe (autre organisation, autre langage) — dans ce cas, OpenAPI reste la référence.
|
||||
- Avantage :
|
||||
- Zéro drift entre contrat et implémentation
|
||||
- Types TypeScript gratuits via `z.infer<>` — aucune réécriture
|
||||
- Changement de contrat = erreur de compilation immédiate côté client
|
||||
- Mocks de tests alignés automatiquement
|
||||
- Limites / vigilance :
|
||||
- Ne pas mettre de logique métier dans `packages/contracts` (IO only)
|
||||
- Attention aux dépendances circulaires si le package grossit
|
||||
- Validé le : 07-03-2026
|
||||
- Contexte technique : TypeScript / Zod / NestJS + Expo (React Native) — pattern agnostique framework
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// packages/contracts/src/auth/auth.schemas.ts
|
||||
export const RegisterRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>; // type GRATUIT
|
||||
|
||||
// packages/contracts/src/index.ts
|
||||
export * from './auth/auth.schemas';
|
||||
export * from './errors/error-code';
|
||||
|
||||
// apps/api/src/modules/auth/auth.controller.ts
|
||||
import type { RegisterRequest } from '@monrepo/contracts';
|
||||
// + ZodValidationPipe → validation automatique, zéro DTO manuel
|
||||
|
||||
// apps/mobile/src/domains/auth/auth.store.ts
|
||||
import type { RegisterRequest } from '@monrepo/contracts';
|
||||
// même type, même schéma, zéro duplication
|
||||
```
|
||||
|
||||
### Structure cible du package contracts
|
||||
|
||||
```
|
||||
packages/contracts/src/
|
||||
auth/auth.schemas.ts ← request/response auth
|
||||
users/users.schemas.ts ← request/response users
|
||||
billing/billing.schemas.ts ← request/response billing (Epic suivant)
|
||||
errors/error-code.ts ← enum codes d'erreur stables
|
||||
http/envelopes.ts ← { data, meta } / { error, meta }
|
||||
index.ts ← re-export tout
|
||||
```
|
||||
|
||||
### Ce qui appartient à contracts
|
||||
|
||||
- Schémas Zod request/response
|
||||
- Types inférés (`z.infer<>`)
|
||||
- Codes d'erreur applicatifs stables
|
||||
- Enums et constantes partagées (ex : liste officielle de sujets/topics)
|
||||
|
||||
### Ce qui n'appartient PAS à contracts
|
||||
|
||||
- Logique métier
|
||||
- Modules/services/guards framework (NestJS, etc.)
|
||||
- State management client (Zustand, Redux, etc.)
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Zéro DTO manuel dans l'API — uniquement `z.infer<typeof Schema>`
|
||||
- [ ] `ZodValidationPipe` global ou par endpoint pour la validation d'entrée
|
||||
- [ ] Constantes partagées (enums, listes) dans contracts, jamais dupliquées
|
||||
- [ ] Mocks de tests importent les types depuis contracts
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-contracts-error-codes"></a>
|
||||
|
||||
## Pattern : Contracts-First — error codes comme contrat obligatoire
|
||||
|
||||
- Objectif : maintenir les codes d'erreur API dans `packages/contracts` pour éviter les clients stringly-typed.
|
||||
- Contexte : monorepo TypeScript avec `packages/contracts/src/errors/error-code.ts`.
|
||||
- Règle : toute nouvelle erreur API ⇒ ajout obligatoire dans `error-code.ts` **avant merge**, pas après.
|
||||
- Risque si ignoré : clients qui testent des strings hardcodées au lieu d'importer l'enum → drift silencieux.
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : TypeScript / NestJS + Expo (React Native)
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Nouvel `error.code` → ajout dans `packages/contracts/src/errors/error-code.ts` en même commit
|
||||
- [ ] Clients importent l'enum, pas une string littérale
|
||||
- [ ] PR review : vérifier `error-code.ts` à chaque ajout d'endpoint d'erreur
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-http-200-payload-metier"></a>
|
||||
## Pattern : Réponse HTTP 200 avec payload métier pour les états d'accès
|
||||
|
||||
- Objectif : éviter les codes 4xx pour des états métier normaux qui nécessitent un rendu côté client.
|
||||
- Contexte : endpoints dont la réponse varie selon les droits ou l'état d'abonnement, sans que l'absence de contenu soit une erreur.
|
||||
- Quand l'utiliser : paywall, trial read-only, quota soft, état d'accès partiel — quand le client doit décider du rendu.
|
||||
- Quand l'éviter : accès réellement interdit côté serveur (403), non authentifié (401), endpoint inexistant (404).
|
||||
- Avantage :
|
||||
- pas de gestion d'exception côté client mobile pour des états courants
|
||||
- rendu conditionnel (paywall, teaser, empty) piloté par le payload
|
||||
- log serveur propre — 4xx réservés aux erreurs techniques/sécurité
|
||||
- Limites / vigilance :
|
||||
- ne pas généraliser aux vraies erreurs de sécurité — 401/403/404 gardent leur sémantique HTTP
|
||||
- Validé le : 20-03-2026
|
||||
- Contexte technique : NestJS / Expo React Native — app-alexandrie story 4.1
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// GET /community/forums
|
||||
// Sans abonnement → 200 + { data: { forums: [], paywallRequired: true }, meta }
|
||||
// Avec abonnement → 200 + { data: { forums: [...], paywallRequired: false }, meta }
|
||||
|
||||
// ❌ Anti-pattern
|
||||
return res.status(402).json({ error: { code: 'SUBSCRIPTION_REQUIRED' } });
|
||||
|
||||
// ✅ Pattern correct
|
||||
return res.status(200).json({
|
||||
data: { forums: [], paywallRequired: true },
|
||||
meta: { total: 0 },
|
||||
});
|
||||
```
|
||||
|
||||
### Règle
|
||||
|
||||
- **4xx** = erreur technique ou de sécurité (401 non authentifié, 403 accès interdit, 404 introuvable)
|
||||
- **200 + flag métier** = état métier normal que le client doit interpréter pour le rendu
|
||||
188
knowledge/backend/patterns/multi-tenant.md
Normal file
188
knowledge/backend/patterns/multi-tenant.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Backend — Patterns : Multi-tenant
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-guardrails-multi-tenant-403-404"></a>
|
||||
## Pattern : Guardrails multi-tenant — 403 vs 404 selon la sémantique
|
||||
|
||||
- Objectif : éviter les fuites d'information inter-tenant tout en gardant une sémantique d'erreur claire.
|
||||
- Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur.
|
||||
- Quand l'utiliser : dès qu'une vérification d'appartenance tenant peut soit refuser explicitement l'accès, soit masquer l'existence d'une ressource.
|
||||
- Quand l'éviter : contexte mono-tenant ou endpoints purement internes sans enjeu de fuite.
|
||||
- Avantage :
|
||||
- clarifie la convention de sécurité
|
||||
- évite les réponses incohérentes selon les modules
|
||||
- facilite les tests d'isolation tenant
|
||||
- Limites / vigilance :
|
||||
- la convention doit être documentée et appliquée partout
|
||||
- un mauvais choix entre 403 et 404 peut révéler une information sensible
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : API multi-tenant / HTTP / services métier
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais l'accès refusé
|
||||
- `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer l'existence d'une ressource d'un autre tenant
|
||||
- documenter la convention dans le module
|
||||
- couvrir les deux sémantiques par des tests dédiés
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Convention 403 vs 404 documentée
|
||||
- Helpers distincts selon la sémantique métier
|
||||
- Aucune fuite d'existence cross-tenant sur les ressources métier
|
||||
- Tests dédiés sur les deux comportements
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-repository-tenant-aware"></a>
|
||||
## Pattern : Repository tenant-aware — `tenantId` obligatoire dans la signature
|
||||
|
||||
- Objectif : rendre impossible par construction une query non scopée sur un domaine multi-tenant.
|
||||
- Contexte : repositories ou services d'accès aux données sur ressources tenant-scoped.
|
||||
- Quand l'utiliser : dès qu'un domaine métier est massivement filtré par tenant.
|
||||
- Quand l'éviter : domaines réellement globaux ou méthodes volontairement cross-tenant.
|
||||
- Avantage :
|
||||
- force le scoping dès la signature TypeScript
|
||||
- réduit les oublis de filtre tenant dans les call sites
|
||||
- rend les exceptions cross-tenant visibles
|
||||
- Limites / vigilance :
|
||||
- les exceptions cross-tenant doivent être rares et documentées explicitement
|
||||
- ne dispense pas d'un second garde-fou dans les mutations sensibles
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : TypeScript / Prisma / architecture repository
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- chaque méthode métier tenant-scoped prend `tenantId` en paramètre obligatoire
|
||||
- les méthodes réellement cross-tenant sont nommées et documentées comme exception
|
||||
- les call sites Prisma directs sur ces domaines sont interdits ou supprimés
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `tenantId` obligatoire sur les méthodes tenant-scoped
|
||||
- Exceptions cross-tenant documentées
|
||||
- Appels directs concurrents à Prisma supprimés
|
||||
- Tests sur scoping tenant au niveau repository
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-tenantid-dans-updates"></a>
|
||||
## Pattern : Défense en profondeur — inclure `tenantId` dans les updates
|
||||
|
||||
- Objectif : éviter une mutation cross-tenant même si un identifiant a été mal résolu en amont.
|
||||
- Contexte : `update` ou `updateMany` sur une ressource tenant-scoped.
|
||||
- Quand l'utiliser : dès qu'une mutation dépend d'un `id` reçu ou résolu dans un flux multi-tenant.
|
||||
- Quand l'éviter : ressources globales non liées à un tenant.
|
||||
- Avantage :
|
||||
- ajoute une seconde barrière côté base
|
||||
- réduit l'impact d'un call site mal scopé
|
||||
- rend la mutation plus sûre sans complexité forte
|
||||
- Limites / vigilance :
|
||||
- ne remplace pas le scoping en lecture ni la vérification d'autorisation
|
||||
- suppose que `tenantId` soit disponible au moment de la mutation
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Prisma / multi-tenant / mutations métier
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- préférer `where: { id, tenantId }` à `where: { id }` sur les updates tenant-scoped
|
||||
- appliquer la même règle sur `updateMany` et opérations de révocation
|
||||
- conserver les vérifications métier amont, mais ne pas leur déléguer toute la sécurité
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `tenantId` présent dans les clauses `where` des updates sensibles
|
||||
- Pas de mutation tenant-scoped basée sur `id` seul
|
||||
- Revue explicite des exceptions documentées
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-helper-tenant-module-partage"></a>
|
||||
## Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié
|
||||
|
||||
- Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant.
|
||||
- Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier.
|
||||
- Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier.
|
||||
- Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct.
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Next.js / TypeScript — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// ✅ src/server/tenant/resolvePublicTenant.ts
|
||||
export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... }
|
||||
|
||||
// ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire
|
||||
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-helper-feature-flag-tenant"></a>
|
||||
## Pattern : Helper centralisé d'activation de features tenant-scoped
|
||||
|
||||
- Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur.
|
||||
- Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations).
|
||||
- Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant.
|
||||
- Avantage :
|
||||
- helper pur et testable sans I/O
|
||||
- comportement par défaut sain (`null`/`undefined` → tout activé)
|
||||
- composants de navigation et pages importent ce helper, jamais Prisma directement
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Next.js App Router / TypeScript — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// src/server/public/publicPagesConfig.ts
|
||||
export function isPublicPageEnabled(
|
||||
config: PublicPagesConfigRecord | null | undefined,
|
||||
pageKey: PublicPageKey
|
||||
): boolean {
|
||||
if (!config) return true; // config absente = tout activé par défaut
|
||||
return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]];
|
||||
}
|
||||
```
|
||||
|
||||
**Règle :** `null`/`undefined` → tout activé. Évite les régressions si la config n'a pas été provisionnée.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-en-enforcement-tenant"></a>
|
||||
## Pattern : EN enforcement optionnel par tenant (toggle + publish gate)
|
||||
|
||||
- Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication.
|
||||
- Contexte : app multi-tenant avec internationalisation optionnelle.
|
||||
- Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n.
|
||||
- Validé le : 21-03-2026
|
||||
- Contexte technique : Prisma / Next.js App Router — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// 1. Modèle Tenant
|
||||
enableEn Boolean @default(false)
|
||||
|
||||
// 2. Vérification dans chaque action mutante (create/update)
|
||||
const { enableEn } = await getEnConfig(tenantId);
|
||||
if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 });
|
||||
|
||||
// 3. Gate publish — vérification de complétude
|
||||
const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all
|
||||
// Exclut : isSystem:true, tenantId:null, isVisible:false
|
||||
if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 });
|
||||
```
|
||||
|
||||
**Règles :**
|
||||
- `isVisible: false` n'est pas inclus dans le check (une entité masquée ne bloque pas la publication)
|
||||
- `revalidatePath` sur **toutes** les pages menu après toggle du flag (pas seulement `/settings`)
|
||||
138
knowledge/backend/patterns/nestjs.md
Normal file
138
knowledge/backend/patterns/nestjs.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Backend — Patterns : NestJS
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-guard-global-nestjs"></a>
|
||||
|
||||
## Pattern : Guard global NestJS — ordre d'enregistrement et décorateurs de bypass
|
||||
|
||||
- Objectif : protéger tous les endpoints par défaut, avec un mécanisme explicite pour les exceptions.
|
||||
- Contexte : API NestJS avec plusieurs guards globaux (authn, authz, feature flags...).
|
||||
- Quand l'utiliser : dès qu'on a 2+ guards globaux dont l'un dépend du résultat de l'autre.
|
||||
- Quand l'éviter : si un seul guard suffit.
|
||||
- Avantage :
|
||||
- Sécurité par défaut (opt-out, pas opt-in)
|
||||
- Ordre d'exécution garanti et explicite
|
||||
- Bypass documenté et traçable via décorateurs
|
||||
- Limites / vigilance :
|
||||
- L'ordre des `APP_GUARD` dans `providers[]` est l'ordre d'exécution — ne pas inverser
|
||||
- Exporter le service depuis son module si injecté dans un guard global d'un autre module
|
||||
- Validé le : 07-03-2026
|
||||
- Contexte technique : NestJS v10+
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// app.module.ts
|
||||
providers: [
|
||||
{ provide: APP_GUARD, useClass: AuthGuard }, // 1er : peuple request.user
|
||||
{ provide: APP_GUARD, useClass: EmailVerifiedGuard }, // 2ème : lit request.user
|
||||
{ provide: APP_GUARD, useClass: EntitlementsGuard }, // 3ème : lit request.user + entitlements
|
||||
]
|
||||
|
||||
// skip-auth.decorator.ts
|
||||
export const SKIP_AUTH = 'skipAuth';
|
||||
export const SkipAuth = () => SetMetadata(SKIP_AUTH, true);
|
||||
|
||||
// auth.guard.ts
|
||||
const skip = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH, [
|
||||
context.getHandler(),
|
||||
context.getClass(), // permet @SkipAuth() au niveau classe
|
||||
]);
|
||||
if (skip) return true;
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] AuthGuard enregistré en premier dans `providers[]`
|
||||
- [ ] AuthModule exporte AuthService si AuthGuard est dans AppModule
|
||||
- [ ] Décorateur `@SkipAuth()` sur tous les endpoints publics (auth, health, docs)
|
||||
- [ ] Tests unitaires sur le guard avec reflector mocké
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-redis-health-cache-court"></a>
|
||||
|
||||
## Pattern : RedisHealthService avec cache interne court
|
||||
|
||||
- Objectif : exposer un état Redis exploitable par les guards globaux sans ping Redis à chaque requête.
|
||||
- Contexte : backend Node/NestJS avec Redis consulté dans le chemin de décision d'écriture.
|
||||
- Quand l'utiliser : quand plusieurs requêtes concurrentes doivent consulter l'état Redis.
|
||||
- Quand l'éviter : si Redis n'est pas consulté dans le chemin request/response.
|
||||
- Avantage :
|
||||
- réduit fortement le flood de `PING`
|
||||
- garde un signal d'état suffisamment frais
|
||||
- Limites / vigilance :
|
||||
- la fenêtre de cache doit rester courte
|
||||
- l'état initial doit être explicite et assumé
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : NestJS / Redis
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Mémoriser lastStatus et lastCheck
|
||||
- Si le dernier check a moins de 5s, retourner l'état en cache
|
||||
- Sinon exécuter un vrai PING et mettre le cache à jour
|
||||
- Utiliser un état initial optimiste (`up`) si le produit ne doit pas bloquer les écritures au boot
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Cache court documenté
|
||||
- Pas de ping Redis à chaque requête
|
||||
- Comportement initial explicite
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-quota-redis-atomique"></a>
|
||||
## Pattern : Quota journalier Redis atomique (INCR + EXPIREAT pipeline)
|
||||
|
||||
- Objectif : implémenter un quota d'action journalier sans race condition ni clé TTL orpheline.
|
||||
- Contexte : quota par utilisateur sur une fenêtre calendaire UTC (posts, requêtes, actions sensibles).
|
||||
- Quand l'utiliser : toute limite d'action journalière avec Redis disponible.
|
||||
- Quand l'éviter : si Redis est down — prévoir un mode dégradé permissif (voir implémentation).
|
||||
- Avantage :
|
||||
- atomicité garantie : `INCR + EXPIREAT` dans un pipeline `MULTI/EXEC`
|
||||
- pas de clé sans TTL même en cas de deux requêtes simultanées (`count === 1` concurrent)
|
||||
- mode dégradé explicite si Redis down (`count === null` → permissif)
|
||||
- Limites / vigilance :
|
||||
- compensation `incrBy(-1)` en cas de dépassement — ne couvre pas les crashes entre INCR et la vérification
|
||||
- la fenêtre expire à minuit UTC, pas à minuit local
|
||||
- Validé le : 20-03-2026
|
||||
- Contexte technique : Redis / NestJS / app-alexandrie story 4.2
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// RedisService — méthode dédiée
|
||||
async incrWithExpireAt(key: string, expireAtMs: number): Promise<number | null> {
|
||||
const pipeline = this.client.multi();
|
||||
pipeline.incr(key);
|
||||
pipeline.expireAt(key, Math.floor(expireAtMs / 1000));
|
||||
const results = await pipeline.exec();
|
||||
return results[0] as number; // valeur post-INCR
|
||||
}
|
||||
|
||||
// Service métier
|
||||
const today = new Date().toISOString().split('T')[0]; // yyyy-mm-dd UTC
|
||||
const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
||||
const quotaKey = `app:quota:post:${userId}:${today}`;
|
||||
const count = await redis.incrWithExpireAt(quotaKey, midnight.getTime());
|
||||
|
||||
if (count !== null && count > QUOTA_MAX) {
|
||||
await redis.incrBy(quotaKey, -1); // compensation
|
||||
throw new HttpException({ error: { code: 'QUOTA_EXCEEDED' } }, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
// count === null → Redis down → mode dégradé permissif
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Vérifier le quota AVANT la création en DB
|
||||
- [ ] `INCR + EXPIREAT` dans un pipeline atomique
|
||||
- [ ] Mode dégradé permissif si `count === null` (Redis down)
|
||||
- [ ] Clé nommée `{app}:quota:{action}:{userId}:{yyyy-mm-dd}` (date UTC)
|
||||
- [ ] Anti-pattern évité : `incrBy` + `setEx` séparés (race condition si count === 1 concurrent)
|
||||
166
knowledge/backend/patterns/nextjs.md
Normal file
166
knowledge/backend/patterns/nextjs.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Backend — Patterns : Next.js
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-nextjs-runtime-only-logique-pure-testable"></a>
|
||||
## Pattern : Next.js runtime-only — orchestration en bord et logique pure testable
|
||||
|
||||
- Objectif : préserver la testabilité unitaire et la lisibilité du code serveur Next.js en limitant les dépendances runtime-only aux couches d'orchestration.
|
||||
- Contexte : applications Next.js avec Server Actions, route handlers, modules email/auth et logique métier testée côté Node.
|
||||
- Quand l'utiliser : dès qu'un flux serveur mélange APIs Next.js runtime-only (`cookies()`, `headers()`, `redirect()`, `server-only`) et logique métier réutilisable.
|
||||
- Quand l'éviter : petits modules purement runtime sans logique métier notable, ou fonctions triviales sans intérêt à être testées séparément.
|
||||
- Avantage :
|
||||
- garde la logique métier importable dans un runner Node standard
|
||||
- évite que `server-only` contamine des modules purs
|
||||
- facilite les tests unitaires sans mocks lourds du runtime Next.js
|
||||
- clarifie la responsabilité des Server Actions et handlers serveur
|
||||
- Limites / vigilance :
|
||||
- demande une discipline de découpage
|
||||
- peut introduire une indirection inutile si la logique extraite est réellement triviale
|
||||
- les frontières d'injection doivent rester simples pour éviter un excès d'abstraction
|
||||
- Validé le : 19-03-2026
|
||||
- Contexte technique : Next.js / Server Actions / Node test runner / modules backend injectables
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- réserver `import "server-only"` aux fichiers qui utilisent réellement des APIs runtime Next.js
|
||||
- garder la Server Action, route handler ou module email comme couche d'orchestration fine
|
||||
- extraire la logique métier pure dans une fonction ou un service sans dépendance à `cookies()`, `headers()`, `redirect()` ou `server-only`
|
||||
- injecter explicitement les dépendances utiles (client DB, token, callback de redirect, logger, etc.)
|
||||
- tester unitairement le module pur dans le runner Node ; tester l'orchestrateur plus légèrement
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `server-only` absent des modules de logique pure
|
||||
- APIs Next.js runtime-only limitées aux couches d'entrée
|
||||
- Logique métier principale testable sans runtime Next.js
|
||||
- Dépendances injectées explicitement quand utile
|
||||
- Server Action ou handler fin et lisible
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-nextjs-server-only-isolation"></a>
|
||||
## Pattern : Next.js server-only & Server Actions — règles d'isolation
|
||||
|
||||
- Objectif : permettre les tests unitaires Node tout en gardant les contraintes runtime Next.js là où elles sont nécessaires.
|
||||
- Contexte : monorepo Next.js App Router avec logique métier testée en Node runner natif.
|
||||
- Quand l'utiliser : dès qu'un module mixe logique pure et dépendances runtime Next.js.
|
||||
- Quand l'éviter : modules purement UI côté client.
|
||||
- Avantage :
|
||||
- logique pure testable sans friction (runner Node natif)
|
||||
- Server Action fine et lisible — orchestration uniquement
|
||||
- `server-only` explicite et intentionnel, pas par habitude
|
||||
- Limites / vigilance :
|
||||
- ne pas mettre `server-only` dans les repositories purs — casse les tests Node hors Next.js
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Next.js App Router / Node.js test runner
|
||||
|
||||
### Règles
|
||||
|
||||
```txt
|
||||
- `server-only` uniquement sur les modules qui appellent des APIs Next.js runtime
|
||||
(cookies(), headers(), redirect()) — pas sur les repositories ni la logique pure
|
||||
- Logique pure extraite dans un module injectable sans `server-only` :
|
||||
deleteSession({ prismaClient, sessionToken })
|
||||
→ testable avec le runner Node sans friction
|
||||
- Server Action = orchestration mince, elle appelle les modules purs injectés
|
||||
et gère les dépendances Next.js runtime uniquement
|
||||
- Logique de validation / sanitisation (safeHttpUrl, etc.) → module utilitaire séparé,
|
||||
sans import nodemailer / server-only
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] `server-only` absent des repositories et modules de logique pure
|
||||
- [ ] Server Action ≤ 10 lignes, délègue au module pur injectable
|
||||
- [ ] Modules purs couverts par des tests `.spec.ts` Node sans config spéciale
|
||||
- [ ] La logique pure ne dépend pas du runtime pour être exécutée
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-utilitaires-purs-module-partage"></a>
|
||||
## Pattern : Utilitaires purs — extraire dans un module sans `server-only`
|
||||
|
||||
- Objectif : permettre aux repositories et aux tests d'importer la même implémentation des utilitaires purs sans friction.
|
||||
- Contexte : fonctions pures (slugify, formatters, validators) utilisées par des repositories qui ont `server-only`.
|
||||
- Quand l'utiliser : dès qu'une fonction pure est utilisée dans un repository ET dans des tests.
|
||||
- Risque si ignoré : logique dupliquée dans les tests qui diverge silencieusement de l'implémentation réelle.
|
||||
- Validé le : 21-03-2026
|
||||
- Contexte technique : Node.js / Next.js — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```
|
||||
src/server/menuAdmin/
|
||||
allergensRepository.ts ← import { slugify } from "./slugify"
|
||||
slugify.ts ← export function slugify() {} // pas de "server-only"
|
||||
|
||||
tests/
|
||||
allergens-admin.test.ts ← import { slugify } from "../src/server/menuAdmin/slugify.ts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-reutiliser-champ-existant-v1"></a>
|
||||
## Pattern : Réutiliser un champ existant plutôt que créer un modèle dédié en V1
|
||||
|
||||
- Objectif : éviter la sur-ingénierie en V1 en réutilisant un champ existant quand le besoin est simple.
|
||||
- Contexte : early-stage, besoin de stocker une configuration simple (URL, flag, valeur unique).
|
||||
- Quand l'utiliser : quand la donnée a le même cycle de vie qu'un modèle existant et ne nécessite pas de relations.
|
||||
- Quand l'éviter : si la configuration a son propre cycle de vie, des cardinalités multiples, ou des relations distinctes.
|
||||
- Avantage : zéro migration supplémentaire, zéro scope creep
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Prisma / Node.js — app-template-resto
|
||||
|
||||
### Règle
|
||||
|
||||
```txt
|
||||
- Avant de créer un modèle ReservationConfig, vérifier si PublicHomeProfile.reservationUrl suffit
|
||||
- Un champ optionnel dans le modèle le plus proche est suffisant en V1
|
||||
- Ne créer un modèle dédié que si : cycle de vie distinct, relations, ou cardinalités multiples
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-validation-url-externe"></a>
|
||||
## Pattern : Valider le protocole d'une URL externe avant de la passer à un lien public
|
||||
|
||||
- Objectif : prévenir les injections `javascript:` et URLs malformées dans les `<a href>` ou `<img src>` publics.
|
||||
- Contexte : toute URL venant d'une config tenant, DB ou saisie utilisateur, rendue dans le HTML.
|
||||
- Quand l'utiliser : systématiquement sur tout champ URL libre stocké en DB et rendu côté HTML.
|
||||
- Risque si ignoré : injection `javascript:`, URL malformée, potentiel XSS.
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Node.js / Next.js — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
function isSafeUrl(url: string): boolean {
|
||||
try {
|
||||
const { protocol } = new URL(url);
|
||||
return protocol === "https:" || protocol === "http:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validation complète en service/repository
|
||||
if (mediaUrl) {
|
||||
try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); }
|
||||
if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://"))
|
||||
throw new HttpError("URL doit commencer par https://.", { status: 400 });
|
||||
if (mediaUrl.length > 500)
|
||||
throw new HttpError("URL trop longue.", { status: 400 });
|
||||
}
|
||||
// Retourner null si invalide — le composant gère l'absence d'URL
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Validation format (`new URL()`) + protocole + longueur max
|
||||
- [ ] Retourner `null` si invalide, jamais passer la string brute
|
||||
- [ ] Composant UI reçoit `string | null`, jamais une string non vérifiée
|
||||
247
knowledge/backend/patterns/prisma.md
Normal file
247
knowledge/backend/patterns/prisma.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Backend — Patterns : Prisma
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-soft-delete-archivage-explicite"></a>
|
||||
|
||||
## Pattern : Soft delete et archivage explicite
|
||||
|
||||
- Objectif : permettre la suppression logique sans perte immédiate de données.
|
||||
- Contexte : données métier critiques, besoins d'audit, restauration ou conformité.
|
||||
- Quand l'utiliser : dès qu'une suppression peut avoir des impacts métier ou légaux.
|
||||
- Quand l'éviter : données purement techniques ou réellement éphémères.
|
||||
- Avantage :
|
||||
- Restauration possible
|
||||
- Audit et traçabilité
|
||||
- Réduction des suppressions irréversibles
|
||||
- Limites / vigilance :
|
||||
- Complexité accrue sur les requêtes
|
||||
- Nécessite une discipline stricte (filtres par défaut)
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API + DB relationnelle
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Champ deletedAt (nullable) ou status
|
||||
- Les requêtes standards filtrent deletedAt IS NULL
|
||||
- Endpoints dédiés pour restauration / purge
|
||||
- Index DB tenant compte du soft delete
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Filtrage soft delete par défaut
|
||||
- Restauration explicite possible
|
||||
- Purge maîtrisée (cron / job)
|
||||
- Index DB adaptés
|
||||
- Tests sur cas supprimé / restauré
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-pagination-robuste-cursor-based"></a>
|
||||
|
||||
## Pattern : Pagination robuste (cursor-based) pour les listings
|
||||
|
||||
- Objectif : fournir des listings stables et performants sans incohérences entre pages.
|
||||
- Contexte : endpoints de liste (ex. /users, /orders) avec volume potentiellement important.
|
||||
- Quand l'utiliser : dès qu'un listing peut dépasser quelques dizaines/centaines d'items ou subir des écritures concurrentes.
|
||||
- Quand l'éviter : listes strictement petites et statiques.
|
||||
- Avantage :
|
||||
- Résultats stables malgré insertions/suppressions
|
||||
- Meilleure performance que l'offset sur gros volumes
|
||||
- Expérience client plus fiable
|
||||
- Limites / vigilance :
|
||||
- Nécessite un tri déterministe (champ + tie-breaker)
|
||||
- Complexité légèrement supérieure à offset/limit
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API HTTP + DB (Postgres/MySQL), agnostique framework
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Trier par (createdAt DESC, id DESC) (exemple)
|
||||
- Le client envoie cursor = dernier (createdAt,id) reçu
|
||||
- Le backend renvoie nextCursor si plus de résultats
|
||||
- Ne jamais exposer de cursor implicite ou non documenté
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Tri déterministe (avec tie-breaker)
|
||||
- nextCursor renvoyé et documenté
|
||||
- Limite max de page (protection)
|
||||
- Index DB aligné avec le tri
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-idempotency-key-operations-sensibles"></a>
|
||||
|
||||
## Pattern : Idempotency key pour opérations sensibles
|
||||
|
||||
- Objectif : empêcher les doublons lors de retries ou timeouts.
|
||||
- Contexte : création de ressources, paiements, webhooks.
|
||||
- Quand l'utiliser : toute opération non strictement en lecture.
|
||||
- Quand l'éviter : endpoints purement GET.
|
||||
- Avantage :
|
||||
- Protection contre doublons
|
||||
- Robustesse face aux retries
|
||||
- Limites / vigilance :
|
||||
- Stockage et expiration des clés à gérer
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API HTTP + DB transactionnelle
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Client fournit Idempotency-Key
|
||||
- Backend stocke la clé + résultat
|
||||
- Retry retourne le résultat initial
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Clé obligatoire sur endpoints sensibles
|
||||
- Contrainte d'unicité côté DB
|
||||
- Comportement documenté
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-prisma-p2002-update-unique"></a>
|
||||
|
||||
## Pattern : mapping explicite de `P2002` Prisma sur create/update de champ unique
|
||||
|
||||
- Objectif : transformer un conflit d'unicité prévisible en erreur métier exploitable plutôt qu'en 500 opaque.
|
||||
- Contexte : `create`, `update` ou `upsert` Prisma sur un champ `@unique` alimenté par une source externe, concurrente, ou après un pre-check.
|
||||
- Quand l'utiliser : dès qu'un champ unique peut entrer en collision — à la création ET à la modification.
|
||||
- Quand l'éviter : jamais si le champ peut réellement entrer en collision.
|
||||
- Avantage :
|
||||
- réponse client stable
|
||||
- diagnostic métier plus rapide
|
||||
- Limites / vigilance :
|
||||
- le mapping doit rester cohérent avec le format d'erreur API standardisé
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : Prisma / PostgreSQL / NestJS
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Catch explicite de PrismaClientKnownRequestError code P2002
|
||||
- Mapping vers une erreur métier stable
|
||||
- Conserver requestId et format d'erreur standardisé
|
||||
```
|
||||
|
||||
### Implémentation (exemple complet)
|
||||
|
||||
```typescript
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
try {
|
||||
await prisma.item.create({ data: { ... } });
|
||||
// ou: await prisma.item.update({ where: { id }, data: { ... } });
|
||||
} catch (err) {
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
|
||||
throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
**Important :** un pre-check applicatif (`findUnique` avant `create`) ne suffit pas contre les race conditions. Le `try/catch P2002` est le seul garde-fou fiable. S'applique à `create`, `update`, `updateMany`, `upsert`.
|
||||
|
||||
### Checklist
|
||||
|
||||
- `P2002` intercepté sur les creates ET les updates sensibles
|
||||
- Code d'erreur métier stable (409 Conflict)
|
||||
- Pas de 500 générique sur conflit prévisible
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-decimal-prisma-serialisation"></a>
|
||||
## Pattern : Sérialiser les champs `Decimal` Prisma en string au niveau du repository
|
||||
|
||||
- Objectif : éviter que les objets `Decimal` Prisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses.
|
||||
- Contexte : tout champ `Decimal` en Prisma (ex: `price`) retourné via API ou Server Action.
|
||||
- Quand l'utiliser : systématiquement sur tout champ `Decimal` dans les repositories.
|
||||
- Risque si ignoré : `Decimal` n'est pas JSON-sérialisable nativement — comportement varie selon Node vs browser vs test runner.
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Prisma / Node.js — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// Repository — convertir avant de retourner
|
||||
return {
|
||||
...dish,
|
||||
price: dish.price?.toString() ?? null, // Decimal → string
|
||||
};
|
||||
|
||||
// DTO public
|
||||
type DishDto = {
|
||||
price: string | null; // pas Decimal
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-prisma-migration-manuelle-p3014"></a>
|
||||
## Pattern : Prisma — Migration manuelle sans shadow DB (P3014)
|
||||
|
||||
- Objectif : créer et appliquer une migration Prisma quand la shadow database est interdite (DB managée, permissions restreintes).
|
||||
- Contexte : DB managées — Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser.
|
||||
- Quand l'utiliser : quand `prisma migrate dev` échoue avec `P3014 Prisma Migrate could not create the shadow database`.
|
||||
- Risque si ignoré : blocage complet de la migration sur env managé.
|
||||
- Validé le : 23-03-2026
|
||||
- Contexte technique : Prisma v7+ — app-alexandrie / Supabase
|
||||
|
||||
### Implémentation
|
||||
|
||||
```bash
|
||||
# 1. Écrire le SQL manuellement
|
||||
mkdir -p prisma/migrations/<timestamp>_<nom>
|
||||
# Créer migration.sql à la main
|
||||
|
||||
# 2. Appliquer le SQL directement en DB
|
||||
npx prisma db execute --file prisma/migrations/<timestamp>_<nom>/migration.sql
|
||||
|
||||
# 3. Marquer la migration comme appliquée dans _prisma_migrations
|
||||
npx prisma migrate resolve --applied <timestamp>_<nom>
|
||||
|
||||
# Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts
|
||||
```
|
||||
|
||||
**Ne pas utiliser `prisma db push` en production** — il ne versionne pas les migrations.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-filtrage-metier-service"></a>
|
||||
## Pattern : Filtrage des règles métier dans le service, pas dans le repository
|
||||
|
||||
- Objectif : séparer la couche d'accès aux données (repository) des règles de visibilité métier (service).
|
||||
- Contexte : entités publiques avec règles de filtrage (`isVisible`, `isActive`), qui varient selon le contexte appelant (public vs admin).
|
||||
- Quand l'utiliser : dès qu'une règle de visibilité dépend du contexte d'appel.
|
||||
- Quand l'éviter : filtres de performance (pagination, tenant scoping) — ceux-là restent dans le `where`.
|
||||
- Avantage :
|
||||
- la règle est testable unitairement sans Prisma (mock de données brutes)
|
||||
- la requête DB reste simple et stable entre contextes
|
||||
- les cas futurs (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Prisma / Node.js / Next.js — app-template-resto
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// Repository — charge tout ce qui est candidat
|
||||
async findCategories(tenantId: string) {
|
||||
return prisma.category.findMany({ where: { tenantId } }); // pas de filtre isVisible
|
||||
}
|
||||
|
||||
// Service — applique la règle métier et mappe vers DTO
|
||||
const raw = await repo.findCategories(tenantId);
|
||||
return raw.filter(c => c.isVisible).map(toPublicDto);
|
||||
|
||||
// Admin : même repo, filtre différent dans le service admin
|
||||
return raw.map(toAdminDto); // retourne tout, visible ou non
|
||||
```
|
||||
160
knowledge/backend/patterns/stripe.md
Normal file
160
knowledge/backend/patterns/stripe.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Backend — Patterns : Stripe
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-provider-strategy-integrations-tierces"></a>
|
||||
|
||||
## Pattern : Provider-Strategy pour intégrations tierces — périmètre complet
|
||||
|
||||
- Objectif : isoler intégralement la logique propre à un prestataire (Stripe, Brevo, Firebase…) derrière une interface stable, pour éviter la contamination du domaine par le SDK tiers.
|
||||
- Contexte : backend NestJS/TypeScript avec 1+ prestataires externes (paiement, email, storage…).
|
||||
- Quand l'utiliser : dès qu'un service applicatif dépend d'un SDK tiers (et plus encore s'il y a des webhooks).
|
||||
- Quand l'éviter : intégration ponctuelle non critique sans effet de bord (rare) — sinon on perd vite le contrôle.
|
||||
- Avantage :
|
||||
- Testabilité : mock du provider, pas du SDK
|
||||
- Remplacement du prestataire sans refactor "en cascade"
|
||||
- Responsabilités claires : provider = "parle Stripe", service = "parle domaine"
|
||||
- Limites / vigilance :
|
||||
- L'interface doit exposer des **types normalisés** (pas de types Stripe)
|
||||
- Le provider gère aussi les webhooks : validation signature, parsing event, mapping
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : NestJS v10+ / intégration Stripe (webhooks) — pattern généralisable
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// billing-provider.interface.ts (pas d'import Stripe)
|
||||
export type BillingPlan = 'MONTHLY' | 'ANNUAL';
|
||||
|
||||
export type BillingWebhookResult = {
|
||||
userId: string;
|
||||
externalId: string;
|
||||
plan: BillingPlan;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'CANCELLED';
|
||||
currentPeriodEnd: Date | null;
|
||||
};
|
||||
|
||||
export interface BillingProvider {
|
||||
createCheckoutSession(userId: string, plan: BillingPlan): Promise<{ checkoutUrl: string }>;
|
||||
cancelSubscription(externalId: string): Promise<void>;
|
||||
handleWebhook(rawBody: Buffer, signature: string): Promise<BillingWebhookResult | null>;
|
||||
}
|
||||
|
||||
// billing.service.ts (domaine uniquement)
|
||||
async handleWebhook(rawBody: Buffer, signature: string): Promise<void> {
|
||||
const result = await this.billingProvider.handleWebhook(rawBody, signature);
|
||||
if (!result) return;
|
||||
await this.prisma.subscription.upsert({ /* données normalisées */ });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-stripe-subscription-metadata"></a>
|
||||
|
||||
## Pattern : Stripe — metadata sur `subscription_data`, pas sur la Session
|
||||
|
||||
- Objectif : garantir que `userId` (ou tout identifiant métier) soit accessible dans les events `customer.subscription.*`, pas seulement dans `checkout.session.completed`.
|
||||
- Contexte : intégration Stripe Checkout avec webhooks abonnement.
|
||||
- Quand l'utiliser : systématiquement dès qu'on crée une Checkout Session liée à une Subscription.
|
||||
- Risque si ignoré : `metadata.userId` absent des events `customer.subscription.updated/deleted` → silent failure en prod.
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : Stripe API v17+ / NestJS
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
stripe.checkout.sessions.create({
|
||||
metadata: { userId }, // pour checkout.session.completed
|
||||
subscription_data: { metadata: { userId } }, // pour customer.subscription.*
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-webhook-parsing-unique"></a>
|
||||
|
||||
## Pattern : Webhooks entrants — parsing unique (single `constructWebhookEvent`)
|
||||
|
||||
- Objectif : appeler `constructWebhookEvent` une seule fois par requête, puis router vers des extracteurs purs.
|
||||
- Contexte : endpoint webhook recevant des events de plusieurs types (subscription, pack, facture…).
|
||||
- Quand l'utiliser : dès qu'on a 2+ handlers webhook sur le même endpoint.
|
||||
- Risque si ignoré : double vérification de signature + états partiels possibles (sub OK / pack KO).
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : Stripe / NestJS
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// 1. Parser unique — 1 seul constructWebhookEvent(rawBody, sig) → event opaque
|
||||
// 2. Extracteurs purs, sans effet de bord :
|
||||
handleSubscriptionWebhookEvent(event): WebhookResult | null
|
||||
handlePackWebhookEvent(event): PackWebhookResult | null
|
||||
// 3. Orchestrateur unique appelle les extracteurs, persiste les résultats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-restauration-achats-stripe"></a>
|
||||
|
||||
## Pattern : restauration d'achats Stripe en 3 étapes
|
||||
|
||||
- Objectif : reconstruire un état local cohérent à partir de Stripe sans dépendre d'une hypothèse fragile.
|
||||
- Contexte : flux de restore purchases mobile/web avec état local potentiellement désynchronisé.
|
||||
- Quand l'utiliser : dès qu'un utilisateur peut restaurer des achats depuis un nouveau device ou après désynchronisation.
|
||||
- Quand l'éviter : si l'état Stripe n'est pas la source de vérité.
|
||||
- Avantage :
|
||||
- rend la réconciliation explicite
|
||||
- supporte retries et restaurations tardives
|
||||
- Limites / vigilance :
|
||||
- la pagination Stripe et l'idempotence d'écriture restent obligatoires
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : Stripe API / backend Node/NestJS
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
1. Résolution du customer Stripe (ID persisté en DB, fallback robuste si absent)
|
||||
2. Reconstruction de l'état Stripe utile au domaine
|
||||
3. Réconciliation et écritures locales idempotentes
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `stripeCustomerId` persistant côté app
|
||||
- Réconciliation explicite documentée
|
||||
- Upsert ou écriture idempotente
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-subscription-trial-vs-paid"></a>
|
||||
|
||||
## Pattern : Sémantique explicite `Trial` vs `Paid` dans Subscription
|
||||
|
||||
- Objectif : aligner le modèle métier, les guards et les jeux de tests sur une définition unique de l'abonnement payant actif.
|
||||
- Contexte : modèle `Subscription` où `trialEndsAt` matérialise un essai.
|
||||
- Quand l'utiliser : dès qu'un même enregistrement supporte trial et abonnement payant.
|
||||
- Quand l'éviter : si trial et abonnement payant sont modélisés par des entités distinctes.
|
||||
- Avantage :
|
||||
- évite les incohérences silencieuses dans les guards
|
||||
- rend les fixtures et mocks e2e cohérents avec la règle métier
|
||||
- Limites / vigilance :
|
||||
- toute logique `isActive` doit préciser si elle signifie "trial ou paid" ou "paid only"
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : Backend agnostique / modèle d'abonnement
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Un abonnement payant actif n'est pas seulement status = ACTIVE
|
||||
- Il doit aussi avoir trialEndsAt = null
|
||||
- Les fixtures et mocks e2e d'un abonnement payant fixent toujours trialEndsAt: null
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Règle métier explicitée
|
||||
- Guards alignés sur la sémantique choisie
|
||||
- Fixtures et seeds cohérents
|
||||
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