mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Purge _ A capitaliser
This commit is contained in:
@@ -8,7 +8,7 @@ Ce fichier contient **uniquement** des patterns back-end :
|
||||
|
||||
Objectif : éviter de réinventer la roue et réduire le temps de debug.
|
||||
|
||||
Dernière mise à jour : 19-03-2026
|
||||
Dernière mise à jour : 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
@@ -38,6 +38,10 @@ Dernière mise à jour : 19-03-2026
|
||||
- [Guardrails multi-tenant — 403 vs 404 selon la sémantique](#pattern-guardrails-multi-tenant-403-404)
|
||||
- [Repository tenant-aware — `tenantId` obligatoire dans la signature](#pattern-repository-tenant-aware)
|
||||
- [Défense en profondeur — inclure `tenantId` dans les updates](#pattern-tenantid-dans-updates)
|
||||
- [Next.js server-only & Server Actions — règles d'isolation](#pattern-nextjs-server-only-isolation)
|
||||
- [Opérations auth sensibles — atomiques, idempotentes et cohérentes](#pattern-auth-operations-atomiques)
|
||||
- [Réponse HTTP 200 avec payload métier pour les états d'accès](#pattern-http-200-payload-metier)
|
||||
- [Quota journalier Redis atomique (INCR + EXPIREAT pipeline)](#pattern-quota-redis-atomique)
|
||||
|
||||
---
|
||||
|
||||
@@ -965,3 +969,183 @@ handlePackWebhookEvent(event): PackWebhookResult | null
|
||||
- `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-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-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
|
||||
|
||||
---
|
||||
|
||||
<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
|
||||
|
||||
---
|
||||
|
||||
<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)
|
||||
|
||||
Reference in New Issue
Block a user