## Error handling
Requested: a centralized error-handling service on the API, custom error
codes shared across apps, and a client-side error service for i18n labels.
- packages/shared/src/errors/error-codes.ts — ErrorCode enum + ApiErrorResponse
contract. Single source of truth: neither side hardcodes a raw error string
the other has to guess at.
- apps/api: HttpError now carries an ErrorCode (not just a message).
ErrorHandlerService (new) centralizes every "how do we turn a thrown error
into an HTTP response" decision — app.ts's error middleware is now a thin
adapter calling into it. API messages reverted to English/dev-facing (they
were French from an earlier pass) since user-facing text is now generated
client-side from the code.
- apps/web: ApiClient (class, singleton instance) throws ApiError carrying
the code. ErrorMessageService (new) maps every ErrorCode to a localized
label, structured with a Locale type from the start (only "fr" exists, but
adding a language later is "add a locale to the map", not "hunt down every
hardcoded string"). LoginPage/SignupPage now display
errorMessageService.getLabel(err.code), never err.message directly.
- Tests strengthened to assert on `code`, not just HTTP status (Mocha +
Cucumber, new "the response error code should be" step). Cypress mocks
updated to the new {code, message} response shape.
## Code quality pass
Per explicit feedback: heavy JSDoc on every interface/type/class/function/
method/member touched in this PR, explicit public/private visibility on
every class member (ApiClient, ErrorMessageService, ErrorHandlerService,
HttpError), no HTML/logic mixing (styling extracted out of components
entirely, never inline).
ApiClient/ErrorMessageService were initially written as static-only classes;
switched to instance-based singletons (matching ErrorHandlerService's
existing pattern) after Biome's noStaticOnlyClass rule flagged the
static-only shape as an anti-pattern — same "class with visibility
modifiers" outcome, without fighting the linter.
## SCSS + theming
- apps/web/src/styles/_theme.scss — design tokens as CSS custom properties
on :root (colors, spacing, typography), not plain Sass variables — makes
them available at runtime, not just compile time, so a future theme
switch (e.g. dark mode) is "redefine these variables" rather than
rebuilding stylesheets.
- apps/web/src/styles/global.scss replaces the old single index.css:
reset + theme import only, loaded once from main.tsx.
- Per-page/component styles colocated (HomePage.tsx + HomePage.scss);
styles shared by multiple pages within one feature live in that feature's
folder (features/auth/auth-form.scss, used by both Login/SignupPage) —
not duplicated per page, not dumped in the global stylesheet either.
- Component-level .scss files intentionally don't `@use` the theme
partial: they only consume CSS custom properties (global at runtime via
global.scss), not Sass-level symbols, so importing it would do nothing —
documented inline rather than left as a silently-redundant import.
- vite.config.ts opts into Sass's modern compiler API to silence a
legacy-js-api deprecation warning on every build.
## specs/ updates
- New specs/error-handling.md — the ErrorCode/ApiErrorResponse contract,
both services, with a flow diagram.
- New specs/frontend-architecture.md — apps/web folder structure, routing/
auth-guard flow, SCSS/theming conventions.
- specs/batch-cooking-architecture.md links to both (original doc content
otherwise untouched — it's the user's own hand-authored source doc).
## Verification
Full lint/mocha/cucumber/build green. Manually re-verified the whole auth
flow in a real browser against native dev servers (not just the automated
suites): signup, the EMAIL_ALREADY_IN_USE → "Cet email est déjà utilisé"
translation end-to-end (confirmed the raw API response carries the English
dev message + code, and the UI shows the French label), wrong-password
INVALID_CREDENTIALS → its label, and confirmed the theme tokens actually
apply (computed button background-color matches --color-primary, card
max-width matches the token value) rather than trusting the build succeeding.
193 lines
9.3 KiB
Markdown
193 lines
9.3 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 + Cucumber/BDD)
|
|
- `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`,
|
|
`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. A un vrai
|
|
build (`tsc` → `dist/`, voir son `package.json`) : consommé en JS compilé par
|
|
l'API (runtime Node pur, pas de transpilation à la volée) comme par le web (Vite).
|
|
|
|
## 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
|
|
|
|
# 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`.
|
|
|
|
## 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 api test:bdd # tests d'intégration BDD (Cucumber/Gherkin, 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 lint + tests + build sur chaque push/PR vers `main`, puis les tests e2e Cypress.
|
|
|
|
### Cucumber (apps/api)
|
|
|
|
Tests d'intégration lisibles en Gherkin, en complément de Mocha (qui reste pour les
|
|
tests unitaires purs) :
|
|
|
|
- `apps/api/features/*.feature` — scénarios en Given/When/Then (`health.feature` sert
|
|
d'exemple)
|
|
- `apps/api/features/step-definitions/*.steps.ts` — implémentation des steps
|
|
- `apps/api/features/support/world.ts` — contexte partagé entre les steps d'un
|
|
scénario (instancie l'app Express in-process via `createApp()`, comme le fait déjà
|
|
supertest côté Mocha — pas besoin de lancer un vrai serveur)
|
|
- `apps/api/cucumber.cjs` — config (extension `.cjs` volontaire, voir la remarque
|
|
TypeScript/ESM ci-dessous)
|
|
|
|
Pour ajouter un scénario : écrire le `.feature`, lancer `pnpm --filter api test:bdd`,
|
|
implémenter les steps manquants (Cucumber affiche des snippets tout prêts pour ceux
|
|
qui n'existent pas encore).
|
|
|
|
> **Piège TypeScript/ESM à connaître** (déjà rencontré avec `cypress.config.ts`) :
|
|
> les fichiers de config d'outils tiers qui font du chargement dynamique de TS
|
|
> (`cucumber.cjs`, `cypress.config.ts`…) sont sensibles au `"type": "module"` du
|
|
> `package.json`. `cucumber.cjs` évite le problème *pour sa propre config* en étant
|
|
> explicitement CommonJS ; les steps/world restent en `.ts` ESM classique et sont
|
|
> chargés via `tsx` (`NODE_OPTIONS=--import=tsx`, voir le script `test:bdd`).
|
|
|
|
## 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 + Cucumber) 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`/`test:bdd` **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.
|
|
|
|
## 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).
|
|
|
|
Tests Cypress (`apps/web/cypress/e2e/`) : `smoke.cy.ts` + `auth.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 les suites Mocha/Cucumber d'`apps/api` (contre une vraie
|
|
base).
|
|
|
|
## Gestion des erreurs (API ↔ web)
|
|
|
|
Contrat d'erreurs partagé via `packages/shared` (`ErrorCode`, `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 `ErrorMessageService` (`apps/web/src/services/error-message.service.ts`).
|
|
Côté API, `ErrorHandlerService` (`apps/api/src/services/error-handler.service.ts`)
|
|
centralise la transformation de toute erreur levée en réponse HTTP conforme.
|
|
|
|
Détail complet (schéma, exemples, comment ajouter un nouveau code d'erreur) :
|
|
[specs/error-handling.md](specs/error-handling.md).
|