# 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 ` ## 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