ordinarthur 1b3f53c086 fix(profile): force Stripe sync on mount + on window focus
Problem reported: user cancelled their subscription via the generic
'Gérer' portal, but the backend still showed the old subscription.
Root cause: stripe listen wasn't running, so the
customer.subscription.updated webhook never reached the server. The
Profile page was only reading from the DB (getSubscription), so it
stayed stale forever.

Fix: call stripeService.syncSubscription() alongside getSubscription()
at mount time. The fast DB read still happens first (instant display),
then the Stripe API call updates the state if anything has drifted.
Also add a window.addEventListener('focus', ...) listener that re-syncs
every time the user tabs back to the app — handles the common pattern
of opening Stripe portal in a new tab, doing something, then coming
back.

This makes the Profile self-healing even without a webhook setup in dev.
Production should still run the webhook for other apps/users, but this
fallback ensures individual users see the truth on their next visit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:18:12 +02:00
2025-03-10 00:24:26 +01:00

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

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

  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é)

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

  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 :

  • 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

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.

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 :

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 :

  • 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

Description
No description provided
Readme 2.3 MiB
Languages
TypeScript 96.6%
CSS 1.7%
Shell 0.6%
Dockerfile 0.6%
JavaScript 0.3%
Other 0.2%