From 9a9401ca8d9b9ea0c27f6210f907eee59605104e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 29 Aug 2026 21:21:37 +0200 Subject: [PATCH] docs: reference unique de toutes les variables d'environnement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute specs/environment.md : liste et explique chaque variable d'env de l'appli (API, front, service NLP, worker LLM, Postgres, Compose) — obligatoire ou non, defaut, fichier ou la poser, role, et la matrice des secrets partages qui doivent correspondre entre deux bouts. Resume les schemas de validation (apps/api/src/config/env.ts, services/tech-step-llm-worker/src/config.ts, tech-step-intent-service/config.py) qui restent la source de verite, plus les 7 fichiers *.env.example et docker-compose.yml. README : pointeur vers ce document depuis la section Installation, et mention explicite des .env des deux services. Co-Authored-By: Claude Sonnet 5 --- README.md | 9 +- specs/environment.md | 249 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 specs/environment.md diff --git a/README.md b/README.md index 54864e9..27cbd37 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ 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` +Puis **édite ces `.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 @@ -64,6 +64,13 @@ sont pas définis dans `.env` — pas de valeur par défaut en dur dans les fich 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`). +> **Toutes les variables d'environnement** (lesquelles poser, dans quel fichier, +> obligatoire ou non, à quoi elles servent, quels secrets doivent correspondre) +> sont listées et expliquées dans **[specs/environment.md](specs/environment.md)**. +> Le service NLP (`services/tech-step-intent-service`) et, si tu le lances, le +> worker LLM (`services/tech-step-llm-worker`) ont chacun leur propre `.env` à +> copier — voir ce document. + Si tu comptes lancer `pnpm --filter api test` (voir [Qualité / Tests](#qualité--tests)), crée aussi `apps/api/.env.test` — voir la section dédiée plus bas. diff --git a/specs/environment.md b/specs/environment.md new file mode 100644 index 0000000..47cf85d --- /dev/null +++ b/specs/environment.md @@ -0,0 +1,249 @@ +# Variables d'environnement + +Référence unique de **toutes** les variables d'environnement de l'appli : +lesquelles poser, dans quel fichier, obligatoire ou non, et à quoi elles +servent. + +La **source de vérité** de chaque variable reste son schéma de validation +(`apps/api/src/config/env.ts`, `services/tech-step-llm-worker/src/config.ts`, +`services/tech-step-intent-service/intent_service/config.py`) : au démarrage, +un secret requis manquant fait **échouer immédiatement** le process plutôt que +de laisser une erreur obscure survenir plus tard. Ce document résume ces +schémas et les relie entre eux. + +--- + +## Principes + +- **Aucun secret n'est commité.** Les fichiers `*.env.example` ne contiennent + que des `changeme` ; les vrais `.env` sont git-ignorés et à créer par copie. +- **Générer un secret** (32 caractères minimum) : + ```bash + node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" + ``` +- **Deux workflows, deux jeux de fichiers :** + + | Workflow | Fichiers lus | + |---|---| + | Dev natif (`pnpm dev:api` / `pnpm dev:web` + services lancés à la main) | `apps/api/.env`, `apps/web/.env`, `services/*/.env` | + | `pnpm --filter api test` | `apps/api/.env.test` (chargé à la place de `.env` via `NODE_ENV=test`) | + | Docker Compose (`docker compose …`, prod/review) | `.env` à la racine uniquement — Compose injecte le reste en `environment:` | + +- **Secrets partagés :** plusieurs variables portent le même secret des deux + côtés d'un lien (API ↔ service). Elles **doivent avoir la même valeur** — + voir [Secrets à faire correspondre](#secrets-à-faire-correspondre). + +--- + +## Fichiers `.env` + +| Fichier | Créé par | Utilisé pour | Modèle | +|---|---|---|---| +| `.env` (racine) | `cp .env.example .env` | **Docker Compose uniquement** (provisionne Postgres + injecte les vars des services) | `.env.example` | +| `apps/api/.env` | `cp apps/api/.env.example apps/api/.env` | API en dev natif (`pnpm dev:api`) | `apps/api/.env.example` | +| `apps/api/.env.test` | `cp apps/api/.env.test.example apps/api/.env.test` | `pnpm --filter api test` (base **séparée** de la dev) | `apps/api/.env.test.example` | +| `apps/web/.env` | `cp apps/web/.env.example apps/web/.env` | Front en dev natif — lu **au build** par Vite | `apps/web/.env.example` | +| `services/tech-step-intent-service/.env` | `cp …/.env.example …/.env` | Service NLP en dev natif | idem | +| `services/tech-step-llm-worker/.env` | `cp …/.env.example …/.env` | Worker LLM en dev natif (**optionnel**) | idem | +| `services/tech-step-llm-worker/.env.test` | `cp …/.env.test.example …/.env.test` | Tests Mocha du worker | idem | + +--- + +## Postgres — `.env` racine (Docker Compose) + +Provisionnent le conteneur `postgres` de `docker-compose.yml`. En dev natif, +ces valeurs ne sont lues que par Compose pour lancer la base ; l'API, elle, +lit `DATABASE_URL` dans `apps/api/.env` (qui doit refléter ces identifiants). + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `POSTGRES_USER` | **oui** (Compose refuse de démarrer sans) | — | Utilisateur du conteneur Postgres | +| `POSTGRES_PASSWORD` | **oui** | — | Mot de passe | +| `POSTGRES_DB` | **oui** | — | Nom de la base | +| `POSTGRES_PORT` | non | `5432` | Port hôte exposé. Passer à `5433` si un Postgres natif occupe déjà `5432` (adapter aussi `DATABASE_URL`) | + +--- + +## API — `apps/api/.env` (dev natif) / injecté par Compose + +Schéma : `apps/api/src/config/env.ts`. + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `NODE_ENV` | non | `development` | `development` \| `test` \| `production`. `test` déclenche des comportements dédiés (coût argon2 réduit, chargement de `.env.test`). | +| `PORT` | non | `3000` | Port d'écoute HTTP de l'API. | +| `DATABASE_URL` | **de fait oui** (Prisma en a besoin) | — | Chaîne de connexion Postgres. Doit refléter les identifiants du `.env` racine. En Compose elle est construite automatiquement et cible l'hôte `postgres:5432`. | +| `JWT_SECRET` | **oui, sans défaut** | — | Signe/vérifie le JWT de session utilisateur. ≥ 32 caractères. | +| `JWT_EXPIRES_IN` | non | `7d` | Durée de validité du JWT (format `jsonwebtoken`). | +| `AUTH_COOKIE_NAME` | non | `session` | Nom du cookie httpOnly de session utilisateur. | +| `CORS_ORIGIN` | non | `http://localhost:5173` | Origine autorisée par CORS — doit correspondre à l'URL de `apps/web`. En Compose, le front est servi par le **même** conteneur : pas de CORS cross-origin. | +| `COOKIE_SECURE` | non | *(non posé → `NODE_ENV === "production"`)* | Force l'attribut `Secure` du cookie de session. Mettre `false` **uniquement** si le déploiement est en HTTP nu (sans TLS devant) : sinon le cookie n'est jamais renvoyé et toute requête authentifiée renvoie 401 après un login pourtant réussi. | +| `FRONTEND_DIST_DIR` | non | — | Chemin absolu vers `apps/web/dist` à servir avec l'API. Posé **uniquement** dans l'image Docker de prod ; laissé vide en dev natif (c'est le serveur Vite de `pnpm dev:web` qui sert le front). | + +### Lien API ↔ service NLP (`tech-step-intent-service`) + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `INTENT_SERVICE_BASE_URL` | non | `http://localhost:8000` | URL du service NLP. Compose la remplace par le nom de service réseau (`http://tech-step-intent-service:8000`). | +| `INTENT_SERVICE_SECRET` | **oui, sans défaut** | — | Secret partagé (header `X-Intent-Service-Secret`). **Dépendance cœur** : sans le service NLP joignable, aucune technique de cuisine n'est détectée à l'enregistrement/l'aperçu d'une recette. Doit être **identique** à celui du service ([voir plus bas](#service-nlp--servicestech-step-intent-serviceenv)). | + +### Surface d'administration `/admin/*` (optionnelle) + +L'UI admin est servie par `apps/web` sous `/admin/*` (même origine que le +reste du front — pas de CORS dédié), mais avec une **auth totalement +séparée** de celle des utilisateurs. + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `ADMIN_JWT_SECRET` | non *(mais `/admin/*` renvoie 401 tant qu'absent)* | — | Signe/vérifie le JWT de session admin. **Doit être ≠ `JWT_SECRET`** pour qu'un token utilisateur ne puisse jamais être rejoué contre `/admin/*`. ≥ 32 caractères. | +| `ADMIN_COOKIE_NAME` | non | `admin_session` | Nom du cookie httpOnly de session admin — doit différer de `AUTH_COOKIE_NAME` pour que les deux sessions coexistent dans un même navigateur. | +| `ADMIN_INITIAL_EMAIL` | non | — | Valeurs lues **uniquement** par `apps/api/src/scripts/create-admin.ts` quand ses flags `--email` / `--password` / `--name` sont omis (bootstrap du premier admin). Jamais lues par le serveur. | +| `ADMIN_INITIAL_PASSWORD` | non | — | idem | +| `ADMIN_INITIAL_NAME` | non | — | idem | + +### Lien API ↔ worker LLM (`tech-step-llm-worker`, optionnel) + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `INTERNAL_WORKER_SECRET` | non *(mais `/internal/tech-steps/*` renvoie 401 tant qu'absent)* | — | Secret partagé (header `X-Internal-Worker-Secret`) pour les appels du worker LLM vers l'API. ≥ 32 caractères. Job de fond **optionnel** : un déploiement qui ne lance pas le worker n'en a pas besoin. Doit être **identique** à celui du worker. | + +--- + +## Front — `apps/web/.env` + +Vite n'expose au code client **que** les variables préfixées `VITE_`, et les +inline **au build** (pas de lecture à l'exécution). + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `VITE_API_URL` | non | `""` (même origine) | Base URL de l'API pour `fetch` (`apiClient` **et** `adminApiClient`). En dev natif : `http://localhost:3000`. Vide = même origine, correct derrière un reverse-proxy partagé ou dans l'image Docker mono-conteneur. | + +*(La version affichée dans l'UI (`__APP_VERSION__`) vient de `package.json` +via `vite.config.ts`, ce n'est pas une variable d'environnement.)* + +--- + +## Service NLP — `services/tech-step-intent-service/.env` + +Schéma : `intent_service/config.py` (`pydantic-settings`). En Docker, Compose +injecte les variables ; en dev natif, elles viennent de ce `.env`. + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `INTENT_SERVICE_SECRET` | **oui, sans défaut** (le service refuse de démarrer sans) | — | Secret attendu sur `X-Intent-Service-Secret` de chaque requête (sauf `GET /health`). Doit être **identique** à `INTENT_SERVICE_SECRET` côté API. | +| `LOG_LEVEL` | non | `INFO` | Niveau du logging JSON structuré. À `INFO`, chaque appel `/v1/process` journalise son input/output (locale, texte, entités, intent, score). | + +Le **port** n'est pas une variable d'env : il est passé à `uvicorn` en +argument (`--port 8000`). + +--- + +## Worker LLM — `services/tech-step-llm-worker/.env` (optionnel) + +Schéma : `src/config.ts`. Nécessaire **uniquement** hors Docker Compose +(Compose pose lui-même `API_BASE_URL` / `INTERNAL_WORKER_SECRET`). + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `INTERNAL_WORKER_SECRET` | **oui, sans défaut** | — | Doit être **identique** à `INTERNAL_WORKER_SECRET` côté API. ≥ 32 caractères. | +| `API_BASE_URL` | non | `http://app:3000` | Base URL de l'API. Le défaut est le nom d'hôte Compose du service `app` ; à surcharger (`http://localhost:3000`) pour un `pnpm dev:api` local. | +| `TECH_STEP_WORKER_CRON` | non | `0 3 * * 0` (dimanche 03:00) | Expression cron (syntaxe `node-cron`) de réveil du worker. Valeur provisoire à calibrer une fois déployé. | +| `TECH_STEP_LLM_MODEL_URI` | non | `hf:Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M` | URI `hf::` du modèle GGUF à télécharger. | +| `TECH_STEP_LLM_MODEL_PATH` | non | — | Chemin local explicite vers un GGUF, court-circuite le téléchargement HF ci-dessus (offline / éviter un download en plein déploiement). | +| `TECH_STEP_WORKER_LOCALE` | non | `fr` | Locale échantillonnée par le job `audit-low-confidence`. | +| `TECH_STEP_WORKER_BATCH_LIMIT` | non | `50` | Borne (`?limit=`) sur le nombre d'items traités par run — plafonne le coût d'inférence LLM d'une exécution planifiée. | +| `RUN_ONCE` | non | *(non posé → `false`)* | `true` = lance les deux jobs une fois puis quitte, au lieu de démarrer la boucle cron (run manuel / CI). Comparaison de chaîne stricte : `"false"` vaut bien `false`. | + +`services/tech-step-llm-worker/.env.test` ne contient qu'un +`INTERNAL_WORKER_SECRET` bidon (les tests du worker mockent tous les appels +HTTP vers l'API) — juste là pour satisfaire le schéma à l'import. + +--- + +## Compose — `.env` racine, variables spécifiques + +| Variable | Obligatoire | Défaut | Rôle | +|---|---|---|---| +| `APP_PORT` | non | `3000` | Port hôte du conteneur `app` (API + front construit) dans `docker-compose.yml`. | +| `COOKIE_SECURE` | non | *(non posé)* | Voir la table API — `${COOKIE_SECURE:-}` dans Compose, à mettre à `false` seulement pour un déploiement HTTP nu. | +| `TECH_STEP_WORKER_CRON` | non | `0 3 * * 0` | Passé au service `tech-step-llm-worker` de Compose. | + +Le `.env` racine porte aussi `JWT_SECRET`, `INTENT_SERVICE_SECRET`, +`INTERNAL_WORKER_SECRET`, `ADMIN_JWT_SECRET`, `ADMIN_INITIAL_*` : en Compose, +c'est **là** qu'ils sont posés (et non dans `apps/api/.env`, jamais copié dans +l'image). + +--- + +## Secrets à faire correspondre + +Un même secret doit porter la **même valeur** aux deux bouts : + +| Secret | Bout A | Bout B | Si absent / divergent | +|---|---|---|---| +| `INTENT_SERVICE_SECRET` | `apps/api/.env` | `services/tech-step-intent-service/.env` | Le service NLP refuse chaque requête → **plus aucune détection de technique** (dépendance cœur). | +| `INTERNAL_WORKER_SECRET` | `apps/api/.env` | `services/tech-step-llm-worker/.env` | `/internal/tech-steps/*` renvoie 401 → le worker LLM ne peut rien faire (job optionnel). | +| Identifiants Postgres | `.env` racine (`POSTGRES_*`) | `apps/api/.env` (`DATABASE_URL`) | `P1000 Authentication failed`. | + +Contraintes supplémentaires : + +- `ADMIN_JWT_SECRET` **≠** `JWT_SECRET` (isolation des sessions admin/utilisateur). +- `ADMIN_COOKIE_NAME` **≠** `AUTH_COOKIE_NAME`. +- `apps/api/.env.test` → `DATABASE_URL` doit pointer une base **différente** de + `apps/api/.env` (la suite `TRUNCATE` tout avant chaque test ; un garde-fou + refuse de tourner si l'URL ne contient ni `test` ni `ci`). + +--- + +## Mise en route rapide + +### Dev natif + +```bash +cp .env.example .env # Postgres (Compose) +cp apps/api/.env.example apps/api/.env # API +cp apps/web/.env.example apps/web/.env # front +cp services/tech-step-intent-service/.env.example \ + services/tech-step-intent-service/.env # service NLP +``` + +Puis éditer : + +1. `.env` — vrais `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB`. +2. `apps/api/.env` — `DATABASE_URL` avec ces mêmes identifiants ; un + `JWT_SECRET` généré ; le **même** `INTENT_SERVICE_SECRET` que… +3. `services/tech-step-intent-service/.env` — …ce fichier. +4. `apps/web/.env` — `VITE_API_URL=http://localhost:3000` (déjà le défaut du + modèle). + +Optionnel — pour l'admin : ajouter `ADMIN_JWT_SECRET` (≥ 32 c, ≠ `JWT_SECRET`) +dans `apps/api/.env`, puis créer un admin : + +```bash +pnpm --filter api exec tsx src/scripts/create-admin.ts \ + --email=ops@example.com --password='…' --name='Ops' +``` + +Optionnel — pour le worker LLM : ajouter le **même** `INTERNAL_WORKER_SECRET` +dans `apps/api/.env` **et** `services/tech-step-llm-worker/.env`. + +### Tests `apps/api` + +```bash +cp apps/api/.env.test.example apps/api/.env.test +``` + +Éditer `DATABASE_URL` → base **dédiée** (ex. `batchcooking_test`), même +`JWT_SECRET` / `INTENT_SERVICE_SECRET` que d'habitude. Le service NLP doit +tourner. Détails : `apps/api/.env.test.example`. + +### Docker Compose + +Un seul fichier : `.env` à la racine. Y renseigner `POSTGRES_*`, `JWT_SECRET`, +`INTENT_SERVICE_SECRET`, et — si besoin — `ADMIN_JWT_SECRET`, +`INTERNAL_WORKER_SECRET`, `COOKIE_SECURE=false` (HTTP nu), `APP_PORT`. + +```bash +docker compose up -d postgres # dev : Postgres seul +docker compose up -d --build # stack complète (review/prod) +```