Le README documentait encore `GET /planning/current` (401 sans session, couvre "aujourd'hui"), une route qui n'existe plus — `planning.routes.ts` ne définit que `GET /planning?date=YYYY-MM-DD` depuis l'introduction de la grille de semaine complète. Sans session, `/planning/current` renvoie un 404 générique (route inexistante), pas le 401 documenté. Documente aussi POST/DELETE /planning/items au passage, absents jusqu'ici. Closes #55 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
403 lines
23 KiB
Markdown
403 lines
23 KiB
Markdown
# batchCooking
|
|
|
|
## Structure
|
|
|
|
Monorepo pnpm workspaces :
|
|
|
|
- `apps/api` — backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé, tests Mocha)
|
|
- `apps/web` — frontend React/Vite/TypeScript, prêt à être embarqué par Capacitor plus tard.
|
|
Page de connexion/inscription en place ; le reste est encore un squelette générique.
|
|
- `packages/shared` — code partagé entre `api` et `web` : schémas zod (`signupSchema`,
|
|
`loginSchema`), types (`SafeUserProfile`), et le contrat d'erreurs (`ErrorCode`
|
|
numérique, `ApiErrorResponse`, voir [specs/error-handling.md](specs/error-handling.md)) —
|
|
même règles des deux côtés, pas de risque de dérive entre front et back.
|
|
- `packages/error-tools` — gestion des erreurs, **indépendante de tout framework
|
|
HTTP** (n'importe pas `express`) : `HttpError`, `ErrorHandlerService`. Séparé
|
|
d'`express-tools` précisément parce que rien ici ne dépend d'Express. Détail :
|
|
[specs/error-handling.md](specs/error-handling.md).
|
|
- `packages/express-tools` — outillage Express générique et réutilisable : `ExpressServer`
|
|
(init serveur, routes, middlewares), `wrapAsyncHandler`, `createErrorMiddleware`
|
|
(adapte `ErrorHandlerService` de `error-tools` à Express) — séparé d'`apps/api`,
|
|
pas de logique métier. Détail : [specs/backend-architecture.md](specs/backend-architecture.md).
|
|
|
|
`packages/shared`, `packages/error-tools` et `packages/express-tools` ont un vrai
|
|
build (`tsc` → `dist/`, voir leur `package.json`) : consommés en JS compilé, pas en
|
|
TS brut — nécessaire pour un runtime Node pur (Docker, pas de transpilation à la
|
|
volée), voir la note dans
|
|
[specs/frontend-architecture.md](specs/frontend-architecture.md#note-sur-les-fichiers-dts).
|
|
|
|
## Prérequis
|
|
|
|
- Node.js 22 (voir `.nvmrc`)
|
|
- pnpm 10 (`corepack enable` puis `corepack use pnpm@10.12.4`, ou installation manuelle)
|
|
- Docker (pour Postgres en local)
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
pnpm install
|
|
cp .env.example .env
|
|
cp apps/api/.env.example apps/api/.env
|
|
cp apps/web/.env.example apps/web/.env
|
|
```
|
|
|
|
Puis **édite ces deux `.env`** pour renseigner de vrais `POSTGRES_USER`/`POSTGRES_PASSWORD`
|
|
(et la `DATABASE_URL` correspondante dans `apps/api/.env`) : les fichiers `.env.example`
|
|
ne contiennent volontairement aucun identifiant réel (juste `changeme`), et
|
|
`docker-compose.yml` refuse de démarrer tant que `POSTGRES_USER`/`PASSWORD`/`DB` ne
|
|
sont pas définis dans `.env` — pas de valeur par défaut en dur dans les fichiers commités.
|
|
Même règle pour `apps/api/.env` : `JWT_SECRET` est **requis, sans défaut** (génère le
|
|
tien, voir le commentaire dans `apps/api/.env.example`).
|
|
|
|
### Cypress : téléchargement du binaire
|
|
|
|
`pnpm install` installe le package `cypress` mais **pas forcément son binaire** (le
|
|
téléchargement du `.exe`/binaire natif peut être ignoré selon l'environnement où
|
|
`pnpm install` a été lancé — ex. un environnement sandboxé/CI dont le cache ne
|
|
correspond pas à celui de ta machine). Si `pnpm --filter web e2e` échoue avec une
|
|
erreur du type :
|
|
|
|
```
|
|
No version of Cypress is installed in: ...\AppData\Local\Cypress\Cache\...
|
|
Please reinstall Cypress by running: cypress install
|
|
```
|
|
|
|
lance simplement, depuis ta machine :
|
|
|
|
```bash
|
|
pnpm --filter web exec cypress install
|
|
```
|
|
|
|
(à faire une seule fois par machine ; le binaire est mis en cache localement,
|
|
hors du repo).
|
|
|
|
## Développement
|
|
|
|
```bash
|
|
# Base de données Postgres locale
|
|
docker compose up -d postgres
|
|
|
|
# Applique le schéma (première fois / après un changement de prisma/schema.prisma)
|
|
pnpm --filter api exec prisma migrate dev
|
|
|
|
# Backend (http://localhost:3000)
|
|
pnpm dev:api
|
|
|
|
# Frontend (http://localhost:5173)
|
|
pnpm dev:web
|
|
```
|
|
|
|
> **Conflit de port possible sur `5432`** : si tu as déjà un Postgres natif installé
|
|
> sur ta machine (service Windows, Homebrew, etc.), il peut occuper le port 5432 et
|
|
> intercepter les connexions à la place du conteneur Docker (symptôme : Prisma
|
|
> renvoie `P1000: Authentication failed` alors que les identifiants sont corrects).
|
|
> Dans ce cas, mets `POSTGRES_PORT=5433` (ou autre) dans ton `.env` **et** adapte le
|
|
> port dans la `DATABASE_URL` de `apps/api/.env`.
|
|
|
|
> **Toujours cibler `postgres`, jamais `docker compose up -d` tout court.** Le même
|
|
> `docker-compose.yml` définit aussi le service `app` (voir [Déploiement](#déploiement)) —
|
|
> celui que Portainer construit en production. Sans nom de service, `docker compose up -d`
|
|
> démarre les deux : ça déclenche un `pnpm install` sur tout le monorepo (donc aussi le
|
|
> `cypress` d'`apps/web`, avec son téléchargement de binaire) rien que pour builder une
|
|
> image dont le dev local n'a pas besoin (on sert le front/back directement via
|
|
> `pnpm dev:web`/`pnpm dev:api`, pas ce conteneur).
|
|
|
|
## Qualité / Tests
|
|
|
|
```bash
|
|
pnpm lint # Biome (lint + format check)
|
|
pnpm lint:fix # Biome --write
|
|
pnpm test # tests unitaires/intégration (Mocha, apps/api)
|
|
pnpm --filter web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement)
|
|
pnpm build # build de tous les workspaces
|
|
```
|
|
|
|
La CI GitHub Actions (`.github/workflows/ci.yml`) exécute quatre jobs indépendants (`lint`, `test`, `build`, `e2e`) en parallèle, sur chaque push (toutes branches) et sur chaque PR vers `main` — pas de chaînage entre eux, chacun apparaît comme son propre check. Voir aussi [Déploiement](#déploiement) pour le pipeline de release (`.github/workflows/release.yml`).
|
|
|
|
## Déploiement
|
|
|
|
Une seule image Docker (`apps/api/Dockerfile`) sert à la fois l'API et le frontend
|
|
buildé — plus de conteneur nginx séparé pour `apps/web`. Le stage `build` compile
|
|
`apps/api` **et** `apps/web` (`pnpm --filter web build`), le stage `runtime` copie
|
|
le résultat (`apps/web/dist`) à côté de l'API ; au démarrage, `apps/api/src/app.ts`
|
|
sert ce dossier statique (fallback SPA compris, pour le routing react-router côté
|
|
client) via `FRONTEND_DIST_DIR` — voir `packages/express-tools/src/express-server.ts`
|
|
(`serveStaticFrontend`). Cette variable n'est renseignée que dans l'image Docker :
|
|
en dev natif (`pnpm dev:api`), elle reste vide et `pnpm dev:web` continue de servir
|
|
le frontend via son propre serveur Vite (HMR), sur un port séparé, comme avant.
|
|
|
|
`docker-compose.yml` ne définit donc que deux services : `postgres` et `app` (un
|
|
seul port, `APP_PORT`, défaut `3000` — plus de `WEB_PORT`/`CORS_ORIGIN` à
|
|
coordonner entre deux origines, le frontend et l'API sont désormais servis depuis
|
|
la même origine).
|
|
|
|
**Pas de registre d'image** dans cette configuration : l'instance **Portainer** de
|
|
production est reliée directement au dépôt Git et reconstruit elle-même
|
|
`docker-compose.yml`/`apps/api/Dockerfile` à chaque déploiement — la CI ne pousse
|
|
donc aucune image nulle part.
|
|
|
|
### Release (`.github/workflows/release.yml`)
|
|
|
|
Déclenchée par un tag `vX.Y.Z` :
|
|
|
|
```bash
|
|
git tag vX.Y.Z
|
|
git push --tags
|
|
```
|
|
|
|
Le pipeline enchaîne trois jobs : `sanity-build` (build de l'image Docker sans
|
|
push, juste pour vérifier qu'elle build encore à ce tag avant de laisser Portainer
|
|
redéployer dessus), `github-release` (crée une Release GitHub avec changelog
|
|
auto-généré à partir des PRs mergées), puis `notify-portainer` — envoie une requête
|
|
au webhook de redeploy de Portainer si le secret de dépôt `PORTAINER_WEBHOOK_URL`
|
|
est configuré (sinon Portainer se resynchronise simplement à son prochain
|
|
polling Git). Pour l'activer : récupérer l'URL du webhook depuis les réglages du
|
|
stack Portainer, puis l'ajouter comme secret GitHub `PORTAINER_WEBHOOK_URL`.
|
|
|
|
## Auth (apps/api)
|
|
|
|
Inscription (création de profil + foyer) et connexion, JWT dans un cookie httpOnly.
|
|
|
|
- `POST /auth/signup` — `{ firstName, lastName, email, password }` → crée le foyer
|
|
(`house`) et le profil (`user_profiles`) en une transaction, pose le cookie de
|
|
session, renvoie le profil (201)
|
|
- `POST /auth/login` — `{ email, password }` → pose le cookie de session, renvoie le
|
|
profil (200) ; message d'erreur volontairement générique (401) que ce soit
|
|
l'email ou le mot de passe qui soit incorrect
|
|
- `POST /auth/logout` — efface le cookie (204)
|
|
- `GET /auth/me` — profil courant, nécessite le cookie de session (401 sinon)
|
|
|
|
Mots de passe hachés avec argon2. Le hash est indépendant du foyer : un profil crée
|
|
toujours son propre foyer à l'inscription (rejoindre un foyer existant n'est pas
|
|
encore implémenté).
|
|
|
|
> **argon2 : version pinnée à `0.31.2`, pas de `^`.** La version `0.45.1` (dernière au
|
|
> moment de l'écriture) segfault au runtime sur au moins une configuration Windows —
|
|
> reproduit de façon stable (bash sandboxé, bash non-sandboxé, PowerShell), alors que
|
|
> `0.31.2` fonctionne parfaitement avec la même API. Si tu montes la version, revérifie
|
|
> concrètement (`argon2.hash(...)` dans un `node -e`) avant de merger, un `pnpm build`
|
|
> qui passe ne suffit pas à détecter un crash runtime.
|
|
|
|
Les tests (Mocha) tournent avec un coût argon2 réduit
|
|
(`NODE_ENV=test`, voir `auth.service.ts`) — le coût par défaut est volontairement
|
|
élevé (sécurité), ce qui rendrait la suite de tests lente/instable sinon. La CI
|
|
provisionne un vrai Postgres de service (`.github/workflows/ci.yml`) et exécute
|
|
`prisma migrate deploy` avant les tests.
|
|
|
|
> **Les tests automatisés et `pnpm dev:api` partagent la même base Postgres locale.**
|
|
> Lancer `pnpm test` **vide `user_profiles`/`house`** (`TRUNCATE ... CASCADE`,
|
|
> voir `test-support/reset-db.ts`) — si tu es en train de tester manuellement à la main
|
|
> (via le navigateur ou curl) contre le serveur de dev, un run de tests en parallèle
|
|
> efface tes données de test sans prévenir. Pas un bug, juste à savoir.
|
|
|
|
## Planning (apps/api)
|
|
|
|
- `GET /planning?date=YYYY-MM-DD` — nécessite le cookie de session (401 sinon).
|
|
Renvoie le planning du foyer de l'utilisateur connecté qui couvre `date`
|
|
(`Planning` dont `start_date <= date <= finish_date`), items inclus avec leur
|
|
recette résolue en `{ id, name }` — ou `null` s'il n'y en a aucun (foyer sans
|
|
planning couvrant cette semaine, ou profil sans foyer). `null` est une
|
|
réponse **valide** (200), pas une erreur.
|
|
- `POST /planning/items` — ajoute une recette à un créneau (jour/repas) du
|
|
planning du foyer connecté, créant la semaine correspondante à la volée si
|
|
besoin.
|
|
- `DELETE /planning/items/:id` — retire un item du planning.
|
|
- Type de réponse partagé : `PlanningView`/`PlanningItemView`
|
|
(`packages/shared/src/types/planning.ts`), consommé tel quel par `apps/web`.
|
|
|
|
Détail de `AsyncRequestHandler`/`wrapAsyncHandler` (`packages/express-tools`) —
|
|
premier endpoint à combiner `requireAuth`/`AuthLocals` avec un handler async, ce qui
|
|
a mis au jour une contrainte générique trop stricte, corrigée à la source :
|
|
[specs/backend-architecture.md](specs/backend-architecture.md).
|
|
|
|
## Données de référence — régimes & allergènes (apps/api)
|
|
|
|
- `GET /reference/diets` — liste des régimes alimentaires (`Diet`, 5 valeurs seedées).
|
|
- `GET /reference/allergies` — liste des allergènes sélectionnables, `{ id, name }`
|
|
(le nom vient de `Category.name` — la table `allergy` elle-même ne porte pas de
|
|
nom, voir `schema.prisma` — chaque allergène = une `Category` + une unique
|
|
`Allergy` sous cette catégorie).
|
|
|
|
Les deux sont **publics** (pas de `requireAuth`) : ce sont des données de référence,
|
|
pas des données de foyer, et le wizard d'inscription doit pouvoir les lire avant
|
|
qu'un compte (donc une session) n'existe.
|
|
|
|
Données seedées via `apps/api/prisma/seed.ts` (`pnpm --filter api prisma:seed`, ou
|
|
automatiquement après `prisma migrate reset` — config `prisma.seed` dans
|
|
`package.json`). La logique réelle (listes + upsert idempotent) vit dans
|
|
`src/db/reference-seed-data.ts`, partagée avec `test-support/reset-db.ts` : chaque
|
|
test repart d'une base **avec** ces données de référence, pas de tables vides —
|
|
nécessaire pour tester `dietId`/`allergyIds` sur de vraies lignes.
|
|
|
|
`Diet.name` et `Category.name` sont `@unique` — ajouté à ce schéma (pas dans le doc
|
|
spec d'origine) précisément pour permettre cet upsert idempotent par nom.
|
|
|
|
Liste des 14 allergènes : ceux du règlement UE 1169/2011 (annexe II) — liste
|
|
standard, pas inventée.
|
|
|
|
**Allergies vs intolérances** (retour fonctionnel, pas dans le doc spec d'origine) :
|
|
`Category.kind` (`AllergenKind` — `ALLERGY` | `INTOLERANCE`) classe chaque allergène.
|
|
Seuls `Gluten` et `Sulfites` sont en `INTOLERANCE` (réaction non-immunitaire
|
|
documentée) ; les 12 autres en `ALLERGY` (réaction immunitaire classique). Classifié
|
|
par substance, pas par utilisateur — un même foyer ne peut pas déclarer "allergie au
|
|
lait" pour un membre et "intolérance au lait" pour un autre ; a suffi pour le besoin
|
|
exprimé, à revoir si ça devient un problème réel. `GET /reference/allergies` renvoie
|
|
`kind` dans chaque `AllergyView` ; `PATCH /profile/allergies` ne change pas (une
|
|
seule liste d'IDs, `kind` ne sert qu'à grouper l'affichage côté client).
|
|
|
|
## Foyer & profil — nom, régime, allergènes (apps/api)
|
|
|
|
Nécessitent tous une session (`requireAuth`) — contrairement aux endpoints de
|
|
référence ci-dessus, ce sont des données propres à l'utilisateur/au foyer.
|
|
|
|
- `GET`/`PATCH /house/current` — foyer de l'utilisateur connecté. `GET` renvoie
|
|
`null` si le profil n'a pas encore de foyer (cas théorique : le signup en crée
|
|
toujours un) ; `PATCH { name }` le renomme (`404 HOUSE_NOT_FOUND` si le profil
|
|
n'a pas de foyer).
|
|
- `PATCH /profile/diet { dietId: number | null }` — régime du profil connecté ;
|
|
`null` efface le régime (étape "skippable" du parcours). `404 DIET_NOT_FOUND` si
|
|
`dietId` ne correspond à aucun régime de référence.
|
|
- `GET`/`PATCH /profile/allergies` — allergènes/intolérances du profil connecté,
|
|
sous forme de liste d'IDs (`number[]`). `PATCH { allergyIds }` **remplace**
|
|
l'ensemble (pas une fusion — le client renvoie toujours la sélection complète,
|
|
cohérent avec un composant de multi-sélection). `404 ALLERGY_NOT_FOUND` si un ID
|
|
ne correspond à aucun allergène de référence.
|
|
|
|
`apps/api/src/lib/safe-profile.ts` centralise le retrait du `passwordHash`
|
|
(`toSafeProfile`), auparavant dupliqué dans `auth.service.ts` et
|
|
`require-auth.ts` — `profile.service.ts` le réutilise aussi.
|
|
|
|
## Page de connexion / inscription (apps/web)
|
|
|
|
- `src/api/client.ts` — `ApiClient` (classe, instance unique exportée `apiClient`) :
|
|
enveloppe `fetch` vers l'API (`credentials: "include"`, requis pour que le cookie
|
|
de session httpOnly parte/revienne — l'API et le front sont sur des origines
|
|
différentes). URL configurable via `VITE_API_URL` (voir `.env.example`).
|
|
- `src/features/auth/AuthContext.tsx` — état d'auth global ; appelle `GET /auth/me` au
|
|
chargement pour restaurer la session depuis le cookie.
|
|
- `src/features/auth/RequireAuth.tsx` / `RedirectIfAuthenticated.tsx` — gardes de route
|
|
(react-router-dom) : `/` exige d'être connecté, `/login` et `/signup` redirigent vers
|
|
`/` si on l'est déjà.
|
|
- `src/pages/{Login,Signup,Home}Page.tsx` — validation client instantanée via les
|
|
schémas zod partagés (`packages/shared`), erreurs API traduites via
|
|
`ErrorMessageService` (voir ci-dessous).
|
|
|
|
Détail de l'organisation complète (dossiers, routing, SCSS/theming) :
|
|
[specs/frontend-architecture.md](specs/frontend-architecture.md).
|
|
|
|
## Accueil, sidebar & sections (apps/web)
|
|
|
|
Une fois connecté, l'utilisateur atterrit sur `src/layouts/AppLayout.tsx` — sidebar
|
|
(nav Planning/Recettes/Liste de courses/Foyer & profil + nom/déconnexion en pied) et
|
|
`<Outlet />` pour la route active — montée une seule fois comme route parente de tout
|
|
l'espace authentifié (`App.tsx`), pas dupliquée par page. `src/pages/HomePage.tsx`
|
|
(routée sur `/`) affiche le planning de la semaine du foyer (`GET /planning?date=`,
|
|
voir plus haut) avec ses états chargement/erreur/vide/rempli ; `Recettes` et `Liste de
|
|
courses` n'ont pas encore de backend dédié et rendent pour l'instant le même
|
|
composant `ComingSoonPage` — `Foyer & profil` (`src/pages/HouseholdPage.tsx`), lui,
|
|
est une vraie page (voir section suivante). Détail complet (pourquoi une seule route
|
|
parente, pourquoi un composant stub partagé) :
|
|
[specs/frontend-architecture.md](specs/frontend-architecture.md#applayout--sidebar-commune-à-lespace-connecté).
|
|
|
|
## Parcours profil — foyer, régime, allergènes (apps/web)
|
|
|
|
- `src/features/profile/` — `HouseNameField`, `DietSelect`, `AllergySelect` : champs
|
|
contrôlés et "dumb" (reçoivent leurs données en props, ne fetchent rien
|
|
eux-mêmes), partagés par les deux surfaces ci-dessous. `AllergySelect` utilise une
|
|
grille de cases à cocher dans un `<fieldset>`/`<legend>` plutôt qu'un
|
|
`<select multiple>` — bien plus repérable/tapable, notamment sur mobile. Prend un
|
|
`legend` en prop (pas un libellé fixe interne) : le même composant est rendu
|
|
**deux fois** par chaque page consommatrice — une fois pour les allergies
|
|
(`AllergyView.kind === "ALLERGY"`), une fois pour les intolérances
|
|
(`"INTOLERANCE"`) — les deux listes filtrées côté client à partir d'un seul
|
|
`GET /reference/allergies`, mais la sélection (`allergyIds`) reste une seule
|
|
liste d'IDs partagée entre les deux groupes (une seule `PATCH /profile/allergies`).
|
|
- `src/pages/onboarding/` — wizard de 3 écrans lancé une fois juste après
|
|
l'inscription (`OnboardingHouseholdPage` → `OnboardingDietPage` →
|
|
`OnboardingAllergensPage`, routes `/onboarding/{foyer,regime,allergenes}`).
|
|
Chaque étape a un unique bouton "Continuer" qui envoie la valeur courante (y
|
|
compris "aucune" pour régime/allergènes) — pas de bouton "Passer" séparé, skip
|
|
implicite. Routes top-level `RequireAuth`, **pas** nichées sous `AppLayout` :
|
|
wizard plein écran sans sidebar, même langage visuel que `/login`/`/signup`.
|
|
- `src/pages/HouseholdPage.tsx` (routée sur `/foyer`) — mêmes réglages, modifiables
|
|
à tout moment. **Hot saving** (retour fonctionnel) : pas de bouton "Enregistrer",
|
|
chaque section sauvegarde automatiquement peu après la dernière modification —
|
|
nom du foyer et allergènes/intolérances debouncés (respectivement 600ms/500ms,
|
|
pour ne pas spammer l'API à chaque frappe/case cochée), régime sauvegardé
|
|
immédiatement (sélection discrète, pas de saisie continue). Déclenché depuis le
|
|
handler `onChange` de chaque champ, jamais depuis un `useEffect` générique qui
|
|
observerait la valeur — un tel effect se déclencherait aussi au chargement
|
|
initial (quand le `GET` peuple le même state), sans moyen propre de distinguer
|
|
"vient d'être chargé" de "vient d'être modifié par l'utilisateur".
|
|
|
|
**Piège trouvé en testant dans le navigateur** : `RedirectIfAuthenticated` (garde de
|
|
`/login`/`/signup`) réagissait à *chaque* changement de `user`, pas seulement à la
|
|
vérification initiale — un `navigate()` explicite dans le gestionnaire de soumission
|
|
d'un formulaire qu'elle protège (ex. `SignupPage` après `signup()`, qui met `user` à
|
|
jour) entre alors en course avec le propre `<Navigate>` de la garde. Invisible tant
|
|
que les deux ciblaient "/", devenu un vrai bug dès que `SignupPage` a dû rediriger
|
|
ailleurs (`/onboarding/foyer`). Fix : la décision de redirection est verrouillée une
|
|
seule fois, au moment où `isLoading` passe à `false`, plus jamais réévaluée après.
|
|
|
|
**Autre piège, même méthode** : `HouseholdPage` initialisait le régime affiché depuis
|
|
`useAuth().user.dietId` (un instantané jamais rafraîchi après une modification faite
|
|
directement via `apiClient`, qui ne touche pas `AuthContext`) — revenait à l'ancienne
|
|
valeur après un aller-retour de navigation SPA sans rechargement complet. Fix : la
|
|
page fetch son propre profil frais (`apiClient.me()`) au montage, et
|
|
`AuthContext.refreshUser()` (nouveau) est appelé après une sauvegarde réussie du
|
|
régime pour que le reste de l'app reste cohérent aussi.
|
|
|
|
Tests Cypress (`apps/web/cypress/e2e/*.cy.ts`) : mockent l'API via `cy.intercept`
|
|
plutôt que de dépendre d'un vrai backend — le job e2e de la CI ne provisionne pas de
|
|
Postgres/API, seulement le serveur de dev Vite. Le comportement réel de l'API est
|
|
couvert par la suite Mocha d'`apps/api` (contre une vraie base).
|
|
|
|
> **Cypress ne peut pas tourner en local dans un environnement Windows sandboxé** :
|
|
> Chromium/Electron headless plante au lancement du process GPU
|
|
> (`GPU process isn't usable`), reproductible sur `main` aussi bien que sur une
|
|
> branche de feature — pas un problème introduit par une modification du code.
|
|
> `pnpm --filter web e2e` fonctionne normalement en CI (GitHub Actions) et sur une
|
|
> machine de dev classique ; dans cet environnement précis, vérifier manuellement via
|
|
> le serveur de dev (`pnpm dev:web` + `pnpm dev:api` en local, pas le conteneur
|
|
> Docker — voir [Déploiement](#déploiement) — qui sert le frontend buildé, pas le
|
|
> serveur de dev Vite).
|
|
|
|
## Gestion des erreurs (API ↔ web)
|
|
|
|
Contrat d'erreurs partagé via `packages/shared` (`ErrorCode`, énumération
|
|
**numérique** groupée par famille — `4000` validation, `401x` auth, `404x` not
|
|
found, `500x` interne — et `ApiErrorResponse`) : l'API renvoie toujours
|
|
`{ code, message, details? }` (message en anglais, dev-facing — jamais affiché tel
|
|
quel), et le client traduit `code` en libellé français via **i18next**
|
|
(`ErrorMessageService`, `apps/web/src/services/error-message.service.ts` →
|
|
`apps/web/src/locales/fr/translation.json`). Côté API, `ErrorHandlerService`
|
|
(`packages/error-tools`) et `createErrorMiddleware` (`packages/express-tools`)
|
|
centralisent la transformation de toute erreur levée en réponse HTTP conforme —
|
|
aucune valeur `ErrorCode` codée en dur nulle part (toujours `ErrorCode.XXX`, y
|
|
compris dans les mocks Cypress).
|
|
|
|
Détail complet (schéma, exemples, comment ajouter un nouveau code d'erreur) :
|
|
[specs/error-handling.md](specs/error-handling.md).
|
|
|
|
Le profil authentifié (`requireAuth`) passe par `res.locals.userProfile`
|
|
(typé via `AuthLocals`), pas par une augmentation du namespace global Express —
|
|
voir [specs/backend-architecture.md](specs/backend-architecture.md) pour le détail
|
|
et le pourquoi.
|
|
|
|
`packages/shared` fournit aussi `assertIsNever` (vérification d'exhaustivité de
|
|
switch/if-chain sur une union, erreur de **compilation** si un cas est oublié) —
|
|
voir [specs/backend-architecture.md](specs/backend-architecture.md#packagesshared--assertisnever).
|
|
|
|
## i18n
|
|
|
|
**i18next** + **react-i18next** — tout le texte affiché (formulaires, boutons,
|
|
erreurs) vient de fichiers de locale JSON (`apps/web/src/locales/<lng>/translation.json`),
|
|
jamais codé en dur dans un composant. Une seule langue existe aujourd'hui (`fr`) ;
|
|
en ajouter une est une question de fichier de locale, pas de code. Détail :
|
|
[specs/frontend-architecture.md](specs/frontend-architecture.md#i18n-internationalisation).
|
|
|
|
## Données de test (faker.js)
|
|
|
|
`apps/api` utilise [`@faker-js/faker`](https://fakerjs.dev/) pour toutes les données
|
|
de test dans `test/*.test.ts` (Mocha) — jamais de nom/email qui ressemble à une
|
|
vraie personne en dur dans un fixture.
|