Application web de suivi thyroïdien, inspirée de l'app Clue.
Stack : TypeScript · Express · Prisma · PostgreSQL · React · Recharts · Docker
- Fonctionnalités
- Architecture
- Démarrage rapide (local)
- Déploiement
- Structure du projet
- Schéma de la base de données
- API Endpoints
- Design System
- Stack technique
- Contribuer
- Changelog
- Licence
| Module | Détail |
|---|---|
| Journal quotidien | Énergie, humeur, anxiété, brouillard mental, 11 symptômes thyroïdiens, médicament pris, mesures physiques (poids/FC/sommeil synchronisables via Google Health, ex: Pixel Watch) |
| Analyses sanguines | TSH, FT4, FT3, Anti-TPO, Anti-TG, carences (Ferritine, Vit D, B12…) avec graphiques d'évolution |
| Médicaments | Gestion du traitement (Levothyrox, etc.), dosage, fréquence, observance |
| Rendez-vous | Agenda médical avec rappels, statuts, types spécialisés |
| Tableau de bord | Streak médicament, observance, moyennes, prochain RDV, historique TSH |
| Profil | Diagnostic, état thyroïde, plages TSH/FT4/FT3 personnalisées par votre médecin |
flowchart LR
UI["Navigateur"]
subgraph Host["Hôte Docker (docker-compose.yml)"]
FE["frontend<br/>nginx + build React/Vite<br/>:8082 → :80"]
BE["backend<br/>Express + TypeScript<br/>:3001"]
DB[("postgres<br/>PostgreSQL 16")]
FE -- "proxy /api/*" --> BE
BE --> DB
end
EXT["Google OAuth · Resend"]
UI -- "HTTPS :8082" --> FE
BE -. "OIDC (connexion Google) / envoi d'email" .-> EXT
Le conteneur frontend ne sert que des fichiers statiques (nginx) ; toutes les requêtes
/api/* sont proxyfiées vers backend (voir frontend/nginx.conf), qui est seul à parler à
PostgreSQL via Prisma. Le navigateur ne voit donc qu'une seule origine (:8082), ce qui évite
toute configuration CORS côté client en production — CORS_ORIGIN/FRONTEND_URL restent un
garde-fou si le backend est appelé directement.
- Node.js 20+
- PostgreSQL (ou Docker)
git clone <url>
cd thyro-track
cd backend && npm install && cd ..
cd frontend && npm install && cd ..cp backend/.env.example backend/.env
# Éditer backend/.env avec votre DATABASE_URL et JWT_SECRETSans GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET, GET /api/auth/oidc/google répond simplement 501 et le bouton Google reste inutile — le reste de l'app fonctionne normalement. Pour l'activer :
- Sur Google Cloud Console, créez (ou sélectionnez) un projet.
- APIs & Services → OAuth consent screen : type External, renseignez le nom de l'app et un email de support, ajoutez les scopes
openid,email,profile. - APIs & Services → Credentials → Create Credentials → OAuth client ID, type Web application.
- Authorized JavaScript origins :
http://localhost:5173(URL du frontend). - Authorized redirect URIs :
http://localhost:3001/api/auth/oidc/google/callback(doit correspondre exactement àGOOGLE_REDIRECT_URI, c'est l'URL du backend, pas du frontend). - Copiez le Client ID et le Client Secret générés dans
backend/.env:GOOGLE_CLIENT_ID="xxxxxxxx.apps.googleusercontent.com" GOOGLE_CLIENT_SECRET="xxxxxxxx"
En production, mettez à jour les origines/redirect URIs avec le domaine réel et ajustez GOOGLE_REDIRECT_URI (+ APP_URL) en conséquence.
Fonctionnalité distincte de "Se connecter avec Google" ci-dessus : elle relie un appareil
connecté (ex: Pixel Watch) via la Google Health API
pour pré-remplir automatiquement Poids, Rythme cardiaque et Heures de sommeil dans le Journal
(un badge ⌚ indique une valeur synchronisée). Sans GOOGLE_HEALTH_CLIENT_ID, la carte
"Google Health" du Profil répond 501 et reste inactive.
Deux flux d'authentification distincts sont en jeu :
- OAuth utilisateur (
GOOGLE_HEALTH_CLIENT_ID/SECRET) : chaque utilisateur autorise ThyroTrack à lire ses données santé — c'est le bouton "Lier" du Profil. - Compte de service IAM Google Cloud (
GOOGLE_HEALTH_SERVICE_ACCOUNT_KEY) : gère un unique abonné webhook au niveau du projet (pas par utilisateur), créé une fois au démarrage du backend. Avec une politiqueAUTOMATIC, Google route ensuite automatiquement les notifications de tout utilisateur consentant vers cet abonné — aucun abonnement individuel n'est nécessaire.
⚠️ fetchDailyMetrics(lecture des mesures) reste partiellement une best-effort. Le host, la version (health.googleapis.com/v4), les scopes OAuth et tout le modèle d'abonnement webhook ont été confirmés par lecture directe de la documentation officielle. En revanche, la forme exacte du corps JSON renvoyé par les endpointsdataTypes/{type}/dataPoints(utilisés pour lire les valeurs) n'a pas pu être vérifiée — le parsing dansbackend/src/lib/googleHealth.ts#fetchDailyMetricsest une meilleure hypothèse, à ajuster si besoin une fois des données réelles observées.
1. Projet et API
Sur Google Cloud Console, activez la Google Health API
sur le projet (le même que "Se connecter avec Google" ou un projet dédié). Notez le numéro
du projet (visible sur la page d'accueil du projet — pas son ID textuel, Google renvoie une
erreur 400/403 sinon) pour GOOGLE_HEALTH_PROJECT_NUMBER.
2. Client OAuth (connexion utilisateur)
- APIs & Services → Credentials → Create Credentials → OAuth client ID, type Web application — des credentials séparées de celles du login, car les scopes santé sont sensibles.
- Authorized redirect URIs :
http://localhost:3001/api/integrations/google-health/callback(doit correspondre àGOOGLE_HEALTH_REDIRECT_URI). - Copiez le Client ID/Client Secret dans
backend/.env(GOOGLE_HEALTH_CLIENT_ID,GOOGLE_HEALTH_CLIENT_SECRET), et générez une clé de chiffrement pour les tokens stockés :à mettre dansnode -e "console.log(require('crypto').randomBytes(32).toString('hex'))"TOKEN_ENCRYPTION_KEY(requise dès queGOOGLE_HEALTH_CLIENT_IDest renseigné, le serveur refuse de démarrer en production sinon). - Ajoutez votre compte Google comme testeur : Google Auth Platform → Audience → Utilisateurs tests → Add users (nécessaire tant que l'app n'est pas publiée/vérifiée par Google — largement suffisant pour un usage personnel).
3. Compte de service (abonné webhook au niveau du projet)
- IAM et administration → Comptes de service → Créer un compte de service.
- Attribuez-lui le rôle "Éditeur de l'API Google Health" (ou Administrateur, selon vos besoins).
- Générez une clé JSON pour ce compte de service (onglet Clés → Ajouter une clé → JSON) et
collez le contenu complet du fichier téléchargé dans
GOOGLE_HEALTH_SERVICE_ACCOUNT_KEY(pas un chemin de fichier — la variable d'env porte le JSON lui-même). - Choisissez un secret et mettez-le dans
GOOGLE_HEALTH_WEBHOOK_SECRET:Ce secret est envoyé à Google à la création de l'abonné et renvoyé tel quel par Google dans chaque notification — c'est ce qui permet au backend de vérifier leur authenticité.node -e "console.log('Bearer ' + require('crypto').randomBytes(24).toString('hex'))" - Renseignez
GOOGLE_HEALTH_PROJECT_NUMBER(voir étape 1).
4. Important : HTTPS public
POST /api/webhooks/google-health doit être joignable par Google en HTTPS public — ça ne
fonctionnera pas avec localhost. Un vrai domaine déployé est nécessaire pour que l'abonné
se crée avec succès : Google effectue une double vérification synchrone de l'endpoint (une
requête authentifiée qui doit répondre 200/201, une non authentifiée qui doit répondre
401/403) au moment de la création, et la création de l'abonné échoue si l'une des deux rate.
Une fois tout renseigné, redémarrez le backend : il crée l'abonné automatiquement au démarrage
(voir les logs pour confirmer Abonné webhook Google Health "thyrotrack-webhook" créé.).
En production, mettez à jour GOOGLE_HEALTH_REDIRECT_URI avec le domaine réel.
cd backend
npx prisma migrate dev --name init
npx prisma generate
npm run db:seed # Crée un compte démo: demo@thyrotrack.com / demo1234Dans deux terminaux séparés :
cd backend && npm run dev # http://localhost:3001cd frontend && npm run dev # http://localhost:5173Le déploiement réel de ce projet passe par docker-compose.yml à la racine : trois services (PostgreSQL, backend Express, frontend servi par nginx) construits depuis backend/Dockerfile et frontend/Dockerfile.
cp .env.example .env
# Renseigner un POSTGRES_PASSWORD fort (ex: openssl rand -hex 24)
cp backend/.env.example backend/.env
# Renseigner JWT_SECRET (32+ caractères), RESEND_API_KEY, et
# GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET si "Se connecter avec Google" est utilisé.
# DATABASE_URL et FRONTEND_URL sont déjà fixés dans docker-compose.yml —
# adaptez-y votre propre domaine avant de déployer.docker compose up -d --buildLe conteneur backend exécute automatiquement prisma migrate deploy au démarrage (voir backend/Dockerfile). Le frontend est disponible sur le port 8082 (voir docker-compose.yml), le backend en interne sur 3001.
docker compose exec backend npm run db:seedGénérer un JWT_SECRET :
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
thyro-track/
├── backend/
│ ├── prisma/
│ │ ├── schema.prisma # Modèles de données complets
│ │ └── seed.ts # Données de démonstration
│ ├── src/
│ │ ├── index.ts # Entrée Express
│ │ ├── lib/ # Prisma client, i18n, logger, email (Resend), OIDC
│ │ ├── middleware/
│ │ │ ├── auth.ts # JWT middleware
│ │ │ ├── admin.ts
│ │ │ ├── asyncHandler.ts
│ │ │ └── errorHandler.ts
│ │ ├── routers/ # Déclaration des routes Express (*.router.ts)
│ │ └── controllers/ # Logique métier + validation Zod (*.controller.ts)
│ ├── Dockerfile
│ └── package.json
│
├── frontend/
│ ├── src/
│ │ ├── pages/
│ │ │ ├── DashboardPage # Vue d'ensemble
│ │ │ ├── LogPage # Journal quotidien (style Clue)
│ │ │ ├── LabResultsPage # Analyses + graphiques
│ │ │ ├── MedicationsPage # Traitements
│ │ │ ├── AppointmentsPage
│ │ │ └── ProfilePage
│ │ ├── lib/
│ │ │ ├── api.ts # Client axios typé
│ │ │ ├── store.ts # Auth state (Zustand)
│ │ │ └── utils.ts # Helpers date, couleurs
│ │ └── types/index.ts # Types partagés + constantes
│ ├── Dockerfile
│ └── package.json
│
├── docker-compose.yml # Backend + Frontend (nginx) + PostgreSQL
├── .env.example # Variables lues par docker-compose.yml
└── LICENSE
User ──┬── UserProfile (diagnostic, plages cibles)
├── DailyEntry[] (journal quotidien — poids/FC/sommeil avec provenance MANUAL|GOOGLE_HEALTH)
│ └── SymptomLog[] (symptômes personnalisés)
├── LabResult[] (TSH, FT4, FT3, anticorps, carences)
├── Medication[] (traitements)
├── Appointment[] (rendez-vous médicaux)
├── GoogleHealthConnection (tokens chiffrés, synchro Pixel Watch...)
└── NotificationSetting
POST /api/auth/register
POST /api/auth/login
GET /api/auth/me
GET /api/auth/oidc/google (redirige vers Google — OAuth2 + OpenID Connect)
GET /api/auth/oidc/google/callback
GET /api/entries?from=&to=
GET /api/entries/:date
POST /api/entries (upsert par date)
DELETE /api/entries/:date
GET /api/lab-results
POST /api/lab-results
PUT /api/lab-results/:id
DELETE /api/lab-results/:id
GET /api/medications
POST /api/medications
PUT /api/medications/:id
DELETE /api/medications/:id
GET /api/appointments
POST /api/appointments
PUT /api/appointments/:id
DELETE /api/appointments/:id
GET /api/profile
PUT /api/profile
GET /api/analytics/overview?days=90
GET /api/analytics/symptoms?days=30
POST /api/integrations/google-health/link (démarre la connexion Pixel Watch/Google Health)
GET /api/integrations/google-health/callback
DELETE /api/integrations/google-health/link
POST /api/webhooks/google-health (notifications Google + négociation de validation de l'abonné)
- Palette : fond sombre (#0b0d14), accent violet (#7b61ff), teal (#00d4b4), rose (#ff6b8a)
- Typographie : DM Serif Display (titres) + DM Sans (corps)
- UI : CSS Modules, responsive mobile avec navigation bas de page
| Couche | Technologie |
|---|---|
| Runtime | Node.js 20 |
| API | Express 4 + TypeScript |
| ORM | Prisma 5 |
| BDD | PostgreSQL |
| Auth | JWT (jsonwebtoken) + bcryptjs, OAuth2 + OpenID Connect (Google, via openid-client) |
| Validation | Zod |
| Frontend | React 18 + Vite |
| État | Zustand + TanStack Query |
| Graphiques | Recharts |
| Routing | React Router 6 |
| Déploiement | Docker Compose (self-hosted) |
Voir CONTRIBUTING.md pour l'environnement de développement, comment
reproduire la CI en local, et le format des PR. En cas de problème, TROUBLESHOOTING.md
documente les incidents déjà rencontrés (et leur diagnostic) sur ce projet.
Les changements notables sont documentés dans CHANGELOG.md.
Projet privé — voir LICENSE.