mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 13:31:43 +02:00
capitalisation: TOCTOU Prisma (fusion + généralisation), Contracts schema orphelin, Zustand optimistic update sous-listes
- Fusion entrée TOCTOU : étend l'entrée multi-tenant existante avec le cas général "idempotence / plafond" (check métier hors transaction) — app-alexandrie story 4.6 - Nouvelle entrée : Contracts schema orphelin / type de retour désynchronisé (RequestSchema non importé, type inline au lieu du type contracts) - Nouvelle entrée : Zustand optimistic update sur item absent de la liste principale (fallback sur pinnedThreads / showcasedThreads) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,8 @@ Dernière mise à jour : 23-03-2026
|
||||
- [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 `$transaction` : fenêtres TOCTOU (check hors transaction)](#risque-prisma-transaction-toctou-tenantid)
|
||||
- [Contracts : schema orphelin / type de retour désynchronisé](#risque-contracts-schema-orphelin)
|
||||
- [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)
|
||||
@@ -853,20 +854,23 @@ endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
|
||||
---
|
||||
|
||||
<a id="risque-prisma-transaction-toctou-tenantid"></a>
|
||||
## Prisma `$transaction` multi-tenant : écriture sans `tenantId` dans le WHERE (TOCTOU)
|
||||
## Prisma `$transaction` : fenêtres TOCTOU (check hors transaction)
|
||||
|
||||
### 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
|
||||
- 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
|
||||
|
||||
- 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
|
||||
- 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 } });
|
||||
@@ -881,9 +885,31 @@ await prisma.$transaction(
|
||||
```
|
||||
|
||||
- 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
|
||||
- Utiliser `updateMany`/`deleteMany` pour inclure `tenantId` sans exception si 0 lignes
|
||||
|
||||
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
@@ -1032,3 +1058,38 @@ async createRessource(...) {}
|
||||
- **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-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
|
||||
|
||||
@@ -59,6 +59,7 @@ Dernière mise à jour : 23-03-2026
|
||||
- [`useTransition` global pour des listes d'items interactifs](#risque-usetransition-global-liste-items)
|
||||
- [`useCallback` inutile quand le callback est wrappé en inline au render](#risque-usecallback-inutile-inline)
|
||||
- [Formulaire React avec `defaultValue` sans `key` prop](#risque-formulaire-defaultvalue-sans-key)
|
||||
- [Zustand : optimistic update sur item absent de la liste principale](#risque-zustand-optimistic-update-sous-listes)
|
||||
|
||||
---
|
||||
|
||||
@@ -1010,3 +1011,37 @@ const handleToggle = useCallback((id: string) => { ... }, []); // stable ✓
|
||||
- **Règle** : tout formulaire d'édition réutilisé pour plusieurs entités doit avoir une `key` distincte par entité
|
||||
|
||||
- Contexte technique : React / Next.js — app-template-resto 21-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-zustand-optimistic-update-sous-listes"></a>
|
||||
## Zustand : optimistic update sur item absent de la liste principale
|
||||
|
||||
### Risques
|
||||
|
||||
- Une action admin qui cherche l'item uniquement dans `state.threads` (liste paginée principale) manque les items présents exclusivement dans `state.pinnedThreads` ou `state.showcasedThreads`
|
||||
- L'optimistic update ne se reflète pas visuellement même si l'appel API a réussi
|
||||
|
||||
### Symptômes
|
||||
|
||||
- L'item mis à jour par une action admin n'apparaît pas dans la nouvelle sous-liste après l'action
|
||||
- Bug reproductible uniquement quand l'item est épinglé / en vitrine mais pas dans la page courante du flux principal
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Anti-pattern : cherche uniquement dans la liste principale paginée
|
||||
const target = state.threads.find((t) => t.id === threadId);
|
||||
// → manque les items présents uniquement dans pinnedThreads / showcasedThreads
|
||||
|
||||
// ✅ Pattern correct : fallback sur toutes les sous-listes du store
|
||||
const target =
|
||||
state.threads.find((t) => t.id === threadId) ??
|
||||
state.pinnedThreads.find((t) => t.id === threadId) ??
|
||||
state.showcasedThreads.find((t) => t.id === threadId);
|
||||
```
|
||||
|
||||
- **Règle** : toute action qui opère sur un item pouvant être présent dans plusieurs sous-listes doit chercher dans l'ensemble de ces listes
|
||||
- Règle complémentaire : ne pas mettre à jour une sous-liste (ex: `pinnedThreads`) lors d'une action qui n'y a pas de rapport (ex: mise en vitrine ne touche pas `pinnedThreads`)
|
||||
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie 23-03-2026
|
||||
|
||||
Reference in New Issue
Block a user