Aller au contenu principal

Développement

Prérequis​

  • Node.js 22 (l'image de production est node:22-alpine ; la branche 20 est sortie de maintenance en avril 2026). La CI, elle, tourne sur Node 24 — voir Déploiement.
  • Docker (pour PostgreSQL local)
  • Git
  • Accès repo GitHub : https://github.com/fresquesystemique/LeHub

Installation locale​

# 1. Clone le repo
git clone https://github.com/fresquesystemique/LeHub.git
cd LeHub

# 2. Copy .env.example et remplir variables
cp .env.example .env.local

# 3. Install dépendances
npm install

# 4. Lancer PostgreSQL en Docker
docker compose up -d postgres

# 5. Créer et appliquer les migrations (génère aussi le client dans generated/prisma)
npx prisma migrate dev

# 6. (Optionnel) Seeder les données de dev
npm run db:seed

# 7. Lancer le serveur Next.js dev
npm run dev

L'app est accessible sur http://localhost:3000.

Variables d'environnement​

Beaucoup de réglages qui ressemblent à des secrets d'environnement vivent en réalité dans la table Settings et s'éditent dans /admin/settings : identifiants HelloAsso, clé Resend et adresses expéditrices, bot Telegram, clés Logo.dev, réglages SEO. Le .env.example en garde des traces (HELLOASSO_CLIENT_ID, RESEND_API_KEY…) que le code ne lit plus. La liste ci-dessous est celle des variables réellement lues dans process.env.

Authentification​

NomRôleExemple
NEXTAUTH_SECRETSecret NextAuth (min 32 caractères)Générer avec openssl rand -base64 32
NEXTAUTH_URLURL de l'apphttp://localhost:3000
AUTH_COOKIE_DOMAINDomaine partagé (prod seulement).fresquesystemique.org
AUTH_SESSION_COOKIE_NAMENom du cookie de session__Secure-fresque.session-token
LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRETOAuth LinkedInDepuis l'espace développeur LinkedIn
MOBILE_JWT_SECRETSignature des jetons de l'API mobileopenssl rand -base64 32

Base de données​

NomRôleExemple
DATABASE_URLURL PostgreSQL, lue par prisma.config.ts (CLI) et lib/prisma.ts (adaptateur pg)postgresql://<utilisateur>:<mot_de_passe>@localhost:5432/fresquesystemique
POSTGRES_PASSWORDMot de passe Postgres du conteneur localDéfini dans docker-compose.yml

Paiements et crons​

NomRôleExemple
HELLOASSO_WEBHOOK_TOKENToken vérifié en query string du webhookDoit correspondre à l'URL déclarée chez HelloAsso
CRON_SECRETJeton de la route /api/cron/reminders. Absent, la route refuse de tourneropenssl rand -base64 32

Badges​

NomRôleExemple
BADGE_PRIVATE_KEY_JWKClé privée EdDSA de signature des badges (JWK)Générée par scripts/generate-badge-key.ts
BADGE_BASE_URLBase de la page publique d'un badgehttps://badge.fresquesystemique.org

Google Slides​

NomRôle
GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRETClient OAuth Google
GOOGLE_OAUTH_REFRESH_TOKENRefresh token d'un compte ayant accès aux présentations

URLs croisées et échanges avec LeSite​

NomRôleExemple
NEXT_PUBLIC_APP_URLURL publique du Hubhttps://hub.fresquesystemique.org
NEXT_PUBLIC_HUB_URLURL du Hub pour les liens sortantsidem
NEXT_PUBLIC_BOARD_URLURL de LeBoard (SSO animateur)https://board.fresquesystemique.org
NEXT_PUBLIC_SITE_URL, PUBLIC_SITE_URLURL de LeSite côté client et côté serveurhttps://fresquesystemique.org
PUBLIC_SITE_ORIGINSOrigines autorisées sur /api/public (lib/cors.ts), séparées par des virguleshttps://fresquesystemique.org,https://www.fresquesystemique.org
LESITE_URLURL de LeSite pour la revalidation et l'inventaire SEOhttps://fresquesystemique.org
LESITE_REVALIDATE_SECRETSecret de revalidation partagé avec LeSiteMême valeur des deux côtés
SEO_CRAWLER_SECRETSecret de l'inventaire des routes de LeSite (en-tête x-seo-crawler)Même valeur des deux côtés
CONTACT_NOTIFY_SECRETSecret du dépôt d'une demande de contact par LeSiteMême valeur des deux côtés
Quatre secrets partagés avec LeSite

LESITE_REVALIDATE_SECRET, SEO_CRAWLER_SECRET, CONTACT_NOTIFY_SECRET et le nom du cookie de session doivent être identiques dans les deux dépôts. Une valeur qui dérive ne casse rien au démarrage : elle casse silencieusement la revalidation, le tableau SEO, le formulaire de contact ou le contournement admin du mode chantier.

Lancement​

# Dev mode avec hot reload
npm run dev

# Build production
npm run build
npm run start

# Typecheck (utile pour dev)
npm run typecheck

# Lint
npm run lint

# Tests
npm test
npm run test:watch

Il n'y a plus d'environnement de développement partagé​

dev.hub et dev.board ont été supprimés le 20 août 2026, avec leur base partagée. Les branches de développement sont conservées dans les dépôts principaux. Une feature se teste donc en local, puis en production : il n'y a pas d'étage intermédiaire, ce qui rend la vérification locale d'un build de production d'autant plus importante (voir les pièges ci-dessous).

Pièges de développement​

1. hasProHabilitation() exige ses 3 arguments​

Piège classique : la fonction hasProHabilitation(habilitationAnimation, cotisationStatus, cotisationExpiry) (dans lib/permissions.ts) n'a pas de défaut pour ses arguments. Un appel avec 2 arguments silencieusement retourne false (faux négatif invisible).

// ❌ MAUVAIS — retourne false sans erreur TypeScript
hasProHabilitation(member.habilitationAnimation, member.cotisationStatus)

// ✅ BON
hasProHabilitation(
member.habilitationAnimation,
member.cotisationStatus,
member.cotisationExpiry
)

2. Admins ont role='formateur', pas role='admin'​

Toujours vérifier session.user.isAdmin pour les opérations admin, jamais seulement session.user.role.

// ❌ MAUVAIS
if (session.user.role === 'admin') { /* access admin */ }

// ✅ BON
if (session.user.isAdmin) { /* access admin */ }

3. Soft delete sur Workshop, Member, Participant​

Queries doivent inclure deletedAt: null ou elles retourneront des entités "supprimées".

// ❌ MAUVAIS — inclut les workshops supprimés
await prisma.workshop.findMany({ where: { status: 'published' } })

// ✅ BON
await prisma.workshop.findMany({ where: { status: 'published', deletedAt: null } })

4. Unique composite [workshopId, email] sur Registration​

Une même personne ne peut s'inscrire qu'une fois par atelier, même si elle annule. Pour retester une inscription, changer l'email de test ou changer d'atelier.

En prod, AUTH_COOKIE_DOMAIN=.fresquesystemique.org partage le cookie de session avec LeSite (pont auth). Localement, laisser vide — sinon impossible de tester login sur localhost:3000.

6. Timezone dans le code​

Utiliser toujours lib/workshop-form.ts pour les calculs timezone (fct datetimeLocalToUTC()). Le stockage en DB est UTC, les formes locales convertissent.

import { datetimeLocalToUTC } from '@/lib/workshop-form'

const utcDate = datetimeLocalToUTC('2025-12-25 19:00', 'Europe/Paris')

7. Les templates emails sont codés (pas d'édition DB)​

Les 15 gabarits d'e-mail sont durcodés dans lib/email-template-defaults.ts (blocs, charte, variables). Pour modifier un template, éditer le fichier source et redéployer. Aucune interface admin pour le contenu.

8. CSP en prod seulement​

Un domaine externe absent de connect-src ne génère pas d'erreur avec npm run dev. Tester avec un build prod local :

npm run build
npm run start
# Puis vérifier DevTools pour les blocages CSP

9. Relation Board (propriété LeBoard)​

Les modèles Board, CardPlacement, Connection, AnimatorSession, Lot, BoardLotDistribution, ModelCardTitle appartiennent à LeBoard (repo séparé). Le Hub y a accès en écriture directe (dans la même DB PostgreSQL).

Conséquence : les migrations schema pour ces modèles sont gérées par LeBoard. Le Hub ne doit jamais modifier le schema de ces modèles sans synchronisation LeBoard.

10. Path alias @/* vers racine repo​

Dans les imports, utiliser l'alias :

// ✅ BON
import { prisma } from '@/lib/prisma'
import { sendEmail } from '@/lib/email'

// ❌ MAUVAIS
import { prisma } from '../../../lib/prisma'

L'alias est défini dans tsconfig.json et résout depuis la racine du repo.

11. Rate limiting sur les endpoints publics​

  • /api/checkout : 10 requêtes / 10 minutes par IP.
  • /api/public/registrations/[token]/cancel : 10 tentatives / 15 minutes par IP.

Utilisé pour prévenir les abus. Tester en dev avec des IPs différentes si le limit est declenché.

12. Prisma 7 : le client est dans generated/, hors git​

Après toute modification de prisma/schema.prisma — et après tout git pull qui en touche une :

npx prisma generate

Le client n'est plus dans node_modules/.prisma mais dans generated/prisma, dossier ignoré par git et compilé dans le bundle Next. Sans régénération, le client importé ne connaît pas les nouveaux champs, et l'erreur ressemble à un champ « inexistant » dans la base alors que la migration est bien passée. L'URL de connexion vit dans prisma.config.ts (CLI) et lib/prisma.ts (adaptateur pg), plus dans le bloc datasource.

13. Lancer les tests avec npm test, pas npx jest​

npm test # ✅
npx jest # ❌ trois suites SEO en faux échec

npm test passe NODE_OPTIONS="--experimental-vm-modules", indispensable parce que yoastseo tire parse5 en ESM. Sans ces options, trois suites échouent avec « Test suite failed to run » alors que le code est bon : npm test donne 166 suites et 1 261 tests au vert, npx jest donne 3 suites en échec et 37 tests jamais exécutés (mesuré le 3 novembre 2026). C'est la même raison qui fait tourner la CI sur Node 24 alors que la production est sur Node 22.

14. Un composant client ne doit jamais importer lib/prisma​

Un import de lib/prisma (même indirect, via un module qui en dépend) depuis un fichier marqué 'use client' fait échouer le chargement de la page entière, avec le message « couldn't load » et aucune trace utile côté serveur.

C'est pour cela que plusieurs modules sont volontairement « purs » : lib/eligibility.ts, lib/organisation-options.ts, lib/vat.ts, lib/price-tiers.ts sont importables côté client et ne doivent jamais se mettre à importer Prisma. Leurs en-têtes le disent.

15. Les scripts TS one-off se lancent avec jiti​

./node_modules/.bin/jiti scripts/mon-script.ts # ✅
npx ts-node scripts/mon-script.ts # ❌ casse (package.json type=module)

Sur l'hôte, les crons utilisent tsx installé globalement (avec --env-file=.env), pas le binaire local.

16. git stash --include-untracked détruit public/uploads/​

Les images uploadées en production vivent dans public/uploads/, non suivi par git. Un git stash --include-untracked dans le dossier de production les emporte, et un stash drop les rend très difficiles à récupérer (il faut aller chercher le troisième parent du commit de stash, <sha>^3).

Ne jamais stasher avec --include-untracked dans un dépôt qui porte des données de production.

17. Purger des données de test : filtrer sur l'identifiant, pas seulement l'e-mail​

Une purge SQL filtrée sur la seule adresse e-mail a supprimé neuf lignes pour six visées en septembre 2026 : la même adresse sert sur plusieurs ateliers. Toujours croiser workshopId (ou l'id des lignes) et l'e-mail, et compter les lignes avant de supprimer.