mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Capitalisation complète — app-alexandrie & app-template-resto (23-03-2026)
Intègre ~50 entrées depuis 95_a_capitaliser.md vers les fichiers validés :
- backend risques : +15 (GET sans authz, TOCTOU tenantId, TTL UTC, AdminRoleGuard, P3014...)
- backend patterns : P2002 amendé (create+update) + 10 nouveaux (Decimal, URL safe, EN enforcement...)
- frontend risques : +21 (defaultValue/key, useTransition global, consent state, Tailwind invalide...)
- frontend patterns : +6 (click-to-load, toggle optimiste, Server Action retourne entité...)
- debug/postmortem : export{fn} ne crée pas de binding local
95_a_capitaliser.md remis à l'état initial vide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ Ce fichier recense des risques back-end susceptibles de provoquer :
|
||||
- régressions coûteuses,
|
||||
- incohérences de données.
|
||||
|
||||
Dernière mise à jour : 20-03-2026
|
||||
Dernière mise à jour : 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
@@ -49,6 +49,20 @@ Dernière mise à jour : 20-03-2026
|
||||
- [NestJS 11 — `TooManyRequestsException` inexistante](#risque-nestjs-toomanyrequest)
|
||||
- [`ForbiddenException` utilisé pour des erreurs de validation](#risque-forbidden-pour-validation)
|
||||
- [PrismaService — getter explicite manquant sur nouveau modèle](#risque-prismaservice-getter-manquant)
|
||||
- [Endpoints GET sans contrôle d'accès sur ressource protégée](#risque-get-sans-controle-acces)
|
||||
- [Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)](#risque-schema-divergence-spec-story)
|
||||
- [Prisma initialisé au chargement de module — casse le build Next.js](#risque-prisma-init-module-build)
|
||||
- [`server-only` dans les repositories — bloque les tests unitaires](#risque-server-only-repositories-tests)
|
||||
- [Controller NestJS corrompu par insertions multiples](#risque-controller-corrompu-insertions)
|
||||
- [TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)](#risque-ttl-redis-heure-locale)
|
||||
- [Story "completed" avec tâches ❌ auto-déclarées](#risque-story-completed-taches-echec)
|
||||
- [Story "done" sans aucun fichier source dans la File List](#risque-story-done-file-list-vide)
|
||||
- [Prisma `$transaction` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU)](#risque-prisma-transaction-toctou-tenantid)
|
||||
- [Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système](#risque-prisma-or-tenantid-null)
|
||||
- [Calcul de `nextOrder` hors transaction (race condition `sortOrder`)](#risque-nextorder-hors-transaction)
|
||||
- [Redirect vers la page désactivée elle-même (boucle infinie feature flags)](#risque-redirect-boucle-infinie)
|
||||
- [Champ `tenantId` sans FK ni relation Prisma vers `Tenant`](#risque-tenantid-sans-fk-relation)
|
||||
- [NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert](#risque-adminroleguard-sans-decorateur)
|
||||
|
||||
---
|
||||
|
||||
@@ -643,3 +657,378 @@ get 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-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-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-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-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-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
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-story-completed-taches-echec"></a>
|
||||
## Story "completed" avec tâches ❌ auto-déclarées
|
||||
|
||||
### Risques
|
||||
|
||||
- Un agent sette `Status: completed` alors que son propre Dev Agent Record liste des items ❌ non implémentés
|
||||
- Le store mobile, service ou tests peuvent être déclarés manquants par l'agent lui-même mais la story semble terminée
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Dev Agent Record contient `❌ store mobile non implémenté` mais `Status: completed`
|
||||
- Code review découvre des ACs non satisfaits
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Avant de setter `Status: completed`, vérifier que le Dev Agent Record ne contient aucun ❌
|
||||
- En cas de doute ou d'item manquant, setter `Status: review` pour déclencher la code review
|
||||
- **Règle** : `Status: completed` = zéro ❌ auto-déclaré dans le Dev Agent Record
|
||||
|
||||
- Contexte technique : BMAD / workflow agent — app-alexandrie 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-story-done-file-list-vide"></a>
|
||||
## Story "done" sans aucun fichier source dans la File List
|
||||
|
||||
### Risques
|
||||
|
||||
- Un agent peut halluciner la completion d'une story en produisant une note générique sans écrire de code
|
||||
- La File List ne contient que des fichiers `_bmad-output/` mais aucun `src/`, `prisma/`, `tests/`
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Completion note générique du type "Ultimate context engine analysis completed"
|
||||
- File List réduite à 2 fichiers meta (story file, sprint-status)
|
||||
- `git log --follow src/` ne montre aucun commit lié à la story
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Lors d'une code review, si la File List ne contient aucun fichier source : traiter comme non implémentée
|
||||
- Vérifier avec `git log --follow src/` avant d'accepter le `Status: done`
|
||||
- Ne pas faire confiance au status `done` sans preuve dans le code
|
||||
|
||||
- Contexte technique : BMAD / agent Codex — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-prisma-transaction-toctou-tenantid"></a>
|
||||
## Prisma `$transaction` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU)
|
||||
|
||||
### Risques
|
||||
|
||||
- Un pre-check d'appartenance tenant + une `$transaction` avec `update({ where: { id } })` sans `tenantId` crée une fenêtre TOCTOU
|
||||
- Un bug upstream qui laisse passer un id cross-tenant peut contourner l'isolation
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Vérification préalable OK mais écriture sur une ressource d'un autre tenant possible en race condition
|
||||
- Le guard applicatif est passé mais la DB n'enforce pas au niveau de l'écriture
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```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` (pas `update`/`delete`) pour inclure `tenantId` sans exception si 0 lignes
|
||||
|
||||
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-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-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
|
||||
|
||||
---
|
||||
|
||||
<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-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
|
||||
|
||||
Reference in New Issue
Block a user