freedge/README.md
ordinarthur 64d2bf4506 feat(stripe): idempotent setup script that provisions products + prices
New script backend/scripts/setup-stripe.ts that:
- Reads STRIPE_SECRET_KEY from .env
- Detects test vs live mode and warns + 5s delay for live
- For each plan (Essentiel 3EUR/mo, Premium 5EUR/mo):
  - Looks up existing price by lookup_key (freedge_essential_monthly,
    freedge_premium_monthly) — idempotent, safe to re-run
  - If missing, creates the product then the recurring price with the
    lookup_key and nickname for clarity
- Prints the resulting price IDs with their env var names
- With --write-env flag, automatically upserts the values into
  backend/.env preserving other lines
- Points to Customer Portal settings and stripe listen command as
  next steps

npm scripts added:
- npm run stripe:setup        # dry run, just print IDs
- npm run stripe:setup:write  # update .env automatically
- npm run stripe:listen       # shortcut for stripe CLI webhook forward

Updated README to show the script as the recommended path for step 1,
keeping the manual dashboard instructions as a fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:58:26 +02:00

257 lines
10 KiB
Markdown

# Freedge
Freedge génère des recettes personnalisées à partir des ingrédients dictés à l'oral. Le son est transcrit (Whisper), une recette est générée (GPT-4o-mini) et une image optionnelle est produite (DALL-E 3).
## Stack
- **Frontend** : React 19 + Vite + TailwindCSS + ShadCN/UI + React Router
- **Backend** : Fastify 4 + TypeScript + Prisma 5 + SQLite
- **IA** : OpenAI (gpt-4o-mini-transcribe, gpt-4o-mini avec Structured Outputs, gpt-image-1)
- **Stockage** : MinIO (S3-compatible) avec fallback local
- **Paiement** : Stripe Checkout hébergé + Customer Portal + webhooks signés
- **Auth** : JWT + Google OAuth
## Structure
```
freedge/
├── backend/ # TypeScript
│ ├── prisma/ # Schéma + migrations SQLite
│ ├── tsconfig.json
│ └── src/
│ ├── ai/ # prompts, recipe-generator, image-generator,
│ │ # transcriber, cost (logging tokens/USD)
│ ├── plugins/ # auth, ai, stripe, google-auth
│ ├── routes/ # auth, recipes, users
│ ├── types/ # fastify.d.ts (augmentation décorateurs)
│ ├── utils/ # env, storage (MinIO), email
│ └── server.ts
└── frontend/
└── src/
├── api/ # Clients HTTP (auth, user, recipe)
├── components/ # UI shadcn + composants métier
├── hooks/ # useAuth, useMobile, useAudioRecorder
├── layouts/ # MainLayout
└── pages/ # Home, Auth/*, Recipes/*, Profile, ResetPassword
```
## Prérequis
- Node.js ≥ 18
- pnpm (recommandé) ou npm
- Une clé API OpenAI
- (Optionnel) Un serveur MinIO pour le stockage images/audio
- (Optionnel) Un compte Resend pour les emails
## Démarrage
```bash
# 1. Services (MinIO) — depuis la racine du projet
docker compose up -d
# → MinIO sur :9000 (API) + :9001 (console web, user freedge / pass freedge123)
# → le bucket 'freedge' est créé automatiquement et rendu lisible publiquement
# 2. Installation
cd backend && npm install
cd ../frontend && pnpm install
# 3. Variables d'environnement backend (.env dans backend/)
cp .env.example .env # puis éditer OPENAI_API_KEY et JWT_SECRET
# Les défauts MinIO du .env.example pointent sur la stack docker-compose
# 4. Base de données
cd backend
npx prisma migrate dev
npx prisma generate
# 5. Lancement
npm run dev # backend (tsx watch) sur :3000
cd ../frontend && pnpm dev # frontend sur :5173
# Build production backend
cd backend && npm run build && npm start # compile vers dist/ puis lance node dist/server.js
npm run typecheck # vérification TS sans build
```
## Variables d'environnement backend
| Variable | Requis | Description |
|---|---|---|
| `DATABASE_URL` | ✅ | URL Prisma (ex: `file:./prisma/dev.db`) |
| `JWT_SECRET` | ✅ | Clé JWT (≥ 32 caractères recommandé) |
| `OPENAI_API_KEY` | ✅ | Clé API OpenAI |
| `PORT` | ❌ | Port du serveur (défaut 3000) |
| `CORS_ORIGINS` | ❌ | Origines autorisées, séparées par virgule |
| `FRONTEND_URL` | ❌ | URL frontend pour les emails de reset |
| `OPENAI_TEXT_MODEL` | ❌ | Défaut `gpt-4o-mini` (recette) |
| `OPENAI_TRANSCRIBE_MODEL` | ❌ | Défaut `gpt-4o-mini-transcribe` |
| `OPENAI_IMAGE_MODEL` | ❌ | Défaut `gpt-image-1` (requiert org vérifiée sur OpenAI) |
| `OPENAI_IMAGE_FALLBACK_MODEL` | ❌ | Défaut `dall-e-3`, utilisé si le principal échoue |
| `OPENAI_IMAGE_QUALITY` | ❌ | `low` / `medium` / `high` (défaut `medium`) |
| `OPENAI_IMAGE_SIZE` | ❌ | Défaut `1024x1024` |
| `ENABLE_IMAGE_GENERATION` | ❌ | `false` pour désactiver totalement la génération d'image |
| `OPENAI_MAX_RETRIES` | ❌ | Retries SDK OpenAI (défaut `3`) |
| `OPENAI_TIMEOUT_MS` | ❌ | Timeout par requête (défaut `60000`) |
| `STRIPE_SECRET_KEY` | ❌ | Clé Stripe (pour créer les customers) |
| `MINIO_ENDPOINT` / `MINIO_PORT` / `MINIO_USE_SSL` / `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` / `MINIO_BUCKET` | ❌ | Config MinIO ; fallback local `./uploads` sinon |
| `PUBLIC_BASE_URL` | ❌ | URL publique du backend pour les fichiers locaux (défaut `http://localhost:3000`) |
| `MINIO_ALLOW_SELF_SIGNED` | ❌ | `true` pour autoriser TLS auto-signé (DEV uniquement) |
| `RESEND_API_KEY` | ❌ | Clé Resend pour les emails |
## Routes API (préfixe `/`)
### Auth (`/auth`)
- `POST /auth/register` — Inscription email + mot de passe
- `POST /auth/login` — Connexion
- `POST /auth/google-auth` — Connexion/inscription via Google OAuth
### Utilisateurs (`/users`)
- `GET /users/profile` — Profil courant (🔒)
- `PUT /users/profile` — Mise à jour du nom (🔒)
- `PUT /users/change-password` — Changement de mot de passe (🔒)
- `PUT /users/change-email` — Changement d'email (🔒)
- `POST /users/forgot-password` — Demande de réinitialisation
- `POST /users/reset-password` — Réinitialisation avec token
- `DELETE /users/account` — Suppression du compte (🔒)
### Recettes (`/recipes`) — toutes 🔒
- `POST /recipes/create` — Upload audio + transcription + génération
- `POST /recipes/create-stream` — Version streaming SSE
- `GET /recipes/list` — Liste les recettes de l'utilisateur
- `GET /recipes/:id` — Détail d'une recette
- `DELETE /recipes/:id` — Supprime une recette
### Stripe (`/stripe`)
- `GET /stripe/plans` — Liste publique des plans disponibles
- `GET /stripe/subscription` — Statut d'abonnement de l'utilisateur 🔒
- `POST /stripe/checkout` — Crée une Checkout Session (body: `{ plan }`) 🔒
- `POST /stripe/portal` — Ouvre le Customer Portal 🔒
- `POST /stripe/webhook` — Receiver d'événements Stripe (signature vérifiée)
### Divers
- `GET /health` — Healthcheck
🔒 = nécessite un JWT `Authorization: Bearer <token>`
## Stockage des fichiers (images & audio)
Deux modes, avec fallback automatique :
1. **MinIO** (recommandé, lancé via `docker compose up -d`)
- Bucket S3-compatible, accessible en lecture publique
- Console web : http://localhost:9001 (user `freedge`, pass `freedge123`)
- API S3 : http://localhost:9000
2. **Local** (fallback si MinIO est indisponible)
- Les fichiers sont écrits dans `backend/uploads/{audio,recipes}/`
- Servis par Fastify sur `GET /uploads/*`
- L'URL publique retournée au frontend est `${PUBLIC_BASE_URL}/uploads/recipes/xxx.png`
Le frontend n'a pas à se soucier du mode : il reçoit une URL publique dans tous les cas.
## Pipeline IA
Le module `src/ai/` est isolé du reste du backend pour faciliter les itérations.
```
audio (multipart)
└─→ saveAudioFile (MinIO ou local)
└─→ transcribeAudio (gpt-4o-mini-transcribe, ~3s)
└─→ generateRecipe (gpt-4o-mini, json_schema strict, ~3-5s)
├─→ Recette structurée riche (titre, description,
│ inspiration, ingrédients avec notes, étapes
│ chronométrées, accord boisson, conseils)
└─→ generateRecipeImage (gpt-image-1 medium, ~6s)
(parallélisée avec la sérialisation JSON)
```
**Caractéristiques :**
- **Structured Outputs strict** : la réponse JSON est validée côté OpenAI
via `json_schema`. Plus jamais de `JSON.parse` qui casse.
- **Prompt système long et stable** (~1500 tokens, persona "chef Antoine"
avec règles d'originalité, de précision technique, et exemples few-shot).
→ Bénéficie du **prompt caching automatique d'OpenAI** (-50% sur la portion
cachée à partir du 2ᵉ appel).
- **Retries + timeout** au niveau du SDK OpenAI (configurables via env).
- **Logs de coût** : chaque appel OpenAI émet un log structuré avec
`model`, `durationMs`, `costUsd`, `usage` et `cacheHit`. Permet de suivre
les coûts par utilisateur et de mesurer l'effet du cache.
- **Image best-effort** : un échec de génération d'image ne casse pas la
création de recette.
## Stripe — configuration
### 1. Créer les produits et prix
**Option A — script automatique (recommandé)**
```bash
cd backend
# Mets juste STRIPE_SECRET_KEY dans .env, puis :
npm run stripe:setup # affiche les IDs à copier
# ou :
npm run stripe:setup:write # écrit directement dans backend/.env
```
Le script est idempotent (utilise des `lookup_key` sur les prix) : tu peux le relancer autant de fois que tu veux, il ne créera pas de doublons.
**Option B — manuel, via le [dashboard Stripe](https://dashboard.stripe.com/test/products)**
1. Crée deux produits récurrents :
- **Essentiel** — 3€/mois
- **Premium** — 5€/mois
2. Note les **Price IDs** (commencent par `price_...`) et colle-les dans `backend/.env`.
### 2. Configurer le Customer Portal
Dans [Settings → Billing → Customer Portal](https://dashboard.stripe.com/test/settings/billing/portal) :
- Active **Cancel subscriptions** (pour permettre l'annulation)
- Active **Switch plans** et ajoute les 2 produits (pour permettre le changement)
- Active **Update payment methods**
- Dans "Branding", renseigne le nom Freedge et l'URL de support
### 3. Variables d'environnement backend
```bash
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID_ESSENTIAL=price_...
STRIPE_PRICE_ID_PREMIUM=price_...
```
### 4. Tester les webhooks en local
Installe la CLI Stripe : `brew install stripe/stripe-cli/stripe`.
```bash
stripe login
stripe listen --forward-to localhost:3000/stripe/webhook
# → affiche "whsec_..." à coller dans STRIPE_WEBHOOK_SECRET
```
Dans un autre terminal, tu peux simuler un événement :
```bash
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deleted
```
### 5. Cartes de test
Utilise les numéros fournis par Stripe dans le [dashboard test](https://docs.stripe.com/testing) :
- `4242 4242 4242 4242` → paiement réussi
- `4000 0000 0000 9995` → carte déclinée
## Sécurité
- Helmet + rate-limit (100 req/min) activés
- CORS whitelisté via `CORS_ORIGINS`
- JWT signé, expiration 7 jours
- Bcrypt (10 rounds) pour les mots de passe
- Validation des variables d'environnement au démarrage
## Licence
MIT