docs: ajouter ONBOARDING, SECURITY, CHANGELOG, ADR 001-006

This commit is contained in:
Hermann_Kitio 2026-04-17 18:35:15 +03:00
parent 28c9c08d31
commit 52b8e9d011
9 changed files with 1380 additions and 0 deletions

37
docs/CHANGELOG.md Normal file
View file

@ -0,0 +1,37 @@
# Changelog — Expria Frontend
Toutes les modifications notables du projet frontend sont documentées dans ce fichier.
Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
## Convention
Chaque entrée suit ce format :
```
## [Unreleased] — YYYY-MM-DD — Session <nom>
### Added (nouveautés)
- ...
### Changed (modifications)
- ...
### Fixed (corrections)
- ...
### Removed (suppressions)
- ...
### Security (sécurité)
- ...
```
---
## [Unreleased]
### Added
- Documentation initiale du projet (ARCHITECTURE, ONBOARDING, SECURITY, etc.)
- 5 ADRs pour les décisions architecturales majeures
- Code source de `src/entities/user/access.ts` et `lib.ts` avec tests

345
docs/ONBOARDING.md Normal file
View file

@ -0,0 +1,345 @@
# ONBOARDING.md — Expria Frontend
> **Temps de lecture : 25 minutes.**
> Ce document te permet de prendre en main le projet et de commencer à contribuer.
> Si tu ne l'as pas encore lu, commence par `ARCHITECTURE.md`.
---
## 1. Contexte produit (2 min)
Expria est une application web de coaching pour l'examen TCF Canada, destinée aux francophones qui préparent leur immigration au Canada via Express Entry. Les utilisateurs viennent principalement d'Algérie, du Maroc, du Cameroun et sont quasi exclusivement sur mobile.
**Fonctionnalités clés :**
- Simulation des 6 tâches de l'examen (EE T1/T2/T3 + EO T1/T2/T3)
- Correction automatique par IA (DeepSeek pour les textes, Gemini pour l'audio)
- Rapport détaillé selon le plan de l'utilisateur (free / standard / premium)
- Dialogue oral temps réel avec un examinateur IA (feature Premium exclusive)
- Mode examen chronométré (Premium)
**Monétisation :** abonnement via Stripe (Standard 19,90€/4 semaines, Premium 39,90€/4 semaines).
---
## 2. Installation (5 min)
### Prérequis
- Node.js 20 LTS ou supérieur
- npm 10+ (ou pnpm 9+ si tu préfères)
- Git
- Un compte GitHub avec accès au dépôt `germannoff/expria-frontend`
### Étapes
```bash
git clone https://github.com/germannoff/expria-frontend.git
cd expria-frontend
npm install
cp .env.example .env
# Remplis .env avec les vraies valeurs (voir section 3)
npm run dev
```
L'application devrait être accessible sur `http://localhost:5173`.
### Commandes principales
```bash
npm run dev # dev server avec HMR
npm run build # build production (sortie dans dist/)
npm run preview # preview du build de production
npm run typecheck # vérification TypeScript stricte
npm run test # tests Vitest (une passe)
npm run test:watch # tests Vitest en mode watch
npm run lint # ESLint
npm run format # Prettier (écriture)
```
---
## 3. Variables d'environnement
Crée un fichier `.env` à la racine en copiant `.env.example` :
```
VITE_API_URL=https://api.expria.app
VITE_SUPABASE_URL=https://<project>.supabase.co
VITE_SUPABASE_ANON_KEY=<clé publique Supabase>
VITE_ENABLE_T2_LIVE=false
VITE_SENTRY_DSN= (optionnel)
```
Les valeurs de développement te seront transmises par Hermann ou via un coffre-fort (1Password, Bitwarden).
**Ne JAMAIS committer `.env`.** Le fichier est dans `.gitignore`.
**Ne JAMAIS mettre de clé privée** (Supabase Service Role, Stripe Secret, Gemini API Key) côté frontend. Ces clés n'existent que dans le backend.
---
## 4. Structure du projet (5 min)
La règle essentielle : **trois couches hiérarchiques**, et les dépendances ne remontent jamais.
```
app/ ← peut importer de : entities, features, shared
features/ ← peut importer de : entities, shared
entities/ ← peut importer de : shared
shared/ ← ne doit RIEN importer d'autre
```
### Où mettre quoi ?
| Tu écris... | Ça va dans... |
|---|---|
| Un type métier (`Plan`, `Production`, `Report`) | `src/entities/<domaine>/types.ts` |
| Une fonction pure métier (`hasAccess`, `canSimulate`, `applyFloutage`) | `src/entities/<domaine>/lib.ts` |
| Un appel API vers le backend | `src/entities/<domaine>/api.ts` |
| Un composant de page | `src/features/<feature>/pages/` |
| Un composant UI spécifique à une feature | `src/features/<feature>/components/` |
| Un hook React spécifique à une feature | `src/features/<feature>/hooks/` |
| Un composant UI générique (`Button`, `Modal`) | `src/shared/components/ui/` |
| Un helper technique (`apiFetch`, `getAccessToken`) | `src/shared/lib/` |
| Un hook utilitaire (`useDebounce`) | `src/shared/hooks/` |
### Exemple concret
Tu veux ajouter une feature "historique des paiements". Tu crées :
```
src/entities/billing/types.ts # type Payment
src/entities/billing/api.ts # GET /billing/payments
src/features/billing/pages/PaymentHistoryPage.tsx
src/features/billing/hooks/usePaymentHistory.ts
```
Tu n'importes rien de `features/` dans `entities/`. Jamais. C'est la règle d'or.
---
## 5. Règles absolues (5 min)
### Règle A — Permissions
**Interdit :**
```typescript
if (plan === 'premium') { ... }
if (user.plan !== 'free') { ... }
if (PLANS[plan].exam_mode) { ... }
```
**Obligatoire :**
```typescript
import { hasAccess, canSimulate } from '@/entities/user/lib'
if (hasAccess(plan, 'exam_mode')) { ... }
const { allowed, reason } = canSimulate(plan, simulationsUsed)
if (!allowed) {
if (reason === 'quota_reached') openUpgradeModal()
}
```
Pourquoi ? Voir ADR 005.
### Règle B — Appels API
**Interdit :**
```typescript
fetch('https://api.expria.app/simulations') // oubli du token, pas de retry
supabase.from('productions').select() // contourne le backend
```
**Obligatoire :**
```typescript
import { apiFetch } from '@/shared/lib/api-client'
const response = await apiFetch<Production[]>('/simulations', { method: 'GET' })
if (response.error) {
// gérer l'erreur selon response.error.code
}
```
### Règle C — Logique métier
**Interdit :** mettre des règles métier dans un composant React.
```typescript
// ❌ Dans une page
{user.plan === 'free' && productions.length >= 5 && <Paywall />}
```
**Obligatoire :** extraire dans `entities/` :
```typescript
// entities/user/lib.ts
export function canSimulate(plan: Plan, used: number) { ... }
// features/dashboard/pages/DashboardPage.tsx
const { allowed } = canSimulate(plan, simulationsUsed)
{!allowed && <Paywall reason={reason} />}
```
### Règle D — Clés privées
Aucune clé privée dans le frontend. Aucune. Si tu as un doute, c'est que c'est privé.
### Règle E — Supabase côté frontend
Supabase est utilisé **uniquement** pour l'auth :
- `supabase.auth.signInWithPassword()`
- `supabase.auth.signOut()`
- `supabase.auth.getSession()` (via `auth-client.ts`)
Toute lecture/écriture de données métier passe par le backend Hono. **Jamais** de `supabase.from('productions')` côté frontend.
### Règle F — Workflow de dev
1. **Lire** `ARCHITECTURE.md` + `DEVELOPMENT_PRINCIPLES.md` + `PLANS_TARIFAIRES.md`
2. **Plan** avant code — tu produis un plan détaillé, tu attends validation
3. **Code** — maximum 3 fichiers par étape
4. **Test**`npm run typecheck && npm run test` vert
5. **Golden Dataset** — rejouer les tests manuels concernés
6. **Commit** — message clair (`feat:`, `fix:`, `refactor:`)
---
## 6. Points d'entrée à connaître (5 min)
### Fichiers que tu liras en premier
| Fichier | Rôle |
|---|---|
| `src/app/main.tsx` | Entry point React |
| `src/app/providers.tsx` | Wrappers globaux (QueryClient, Router, etc.) |
| `src/app/router.tsx` | Toutes les routes de l'app |
| `src/entities/user/access.ts` | Source de vérité des plans — **copie exacte du backend** |
| `src/entities/user/lib.ts` | `hasAccess()` et `canSimulate()` |
| `src/shared/lib/api-client.ts` | Wrapper fetch avec retry, timeout, logging |
| `src/shared/lib/auth-client.ts` | Gestion du token Supabase |
| `src/features/dashboard/hooks/usePlan.ts` | Hook central pour le plan utilisateur |
### Comptes de test
Voir `docs/TEST_ENVIRONMENT.md` (copie du backend). Résumé :
| Email | Plan | Usage |
|---|---|---|
| test.free@gmail.com | free | Parcours Free normal |
| test.standard@gmail.com | standard | Parcours Standard |
| test.premium@gmail.com | premium | Parcours Premium |
| test.quota@gmail.com | free (5/5) | Blocage quota |
Mot de passe commun : `Expria2025!test`.
---
## 7. Workflow Claude Code (3 min)
Le projet est développé avec assistance IA (Claude Code, Cursor). Le fondateur Hermann est non-technique — le workflow est strict pour éviter les dérives.
### Début de session
```
"Je commence une session sur expria-frontend.
Lis dans l'ordre :
1. docs/ARCHITECTURE.md
2. docs/DEVELOPMENT_PRINCIPLES.md
3. docs/PLANS_TARIFAIRES.md
4. docs/PARCOURS_UTILISATEURS.md (section du plan concerné)
Puis annonce : 'Documents lus, voici mon plan pour cette session.'
Ne commence à coder qu'après validation du plan."
```
### Pendant la session
- L'IA propose un plan → Hermann valide → l'IA code → l'IA lance les tests → l'IA présente un résumé.
- Maximum 3 fichiers par étape. Si plus : découper.
- Tests rouges = on arrête et on corrige avant de continuer.
### Fin de session
- Résumé des modifications
- Tests Golden Dataset manuels rejoués
- Mise à jour de `docs/CHANGELOG.md`
- Commit Git avec message clair
---
## 8. Quand tu ne sais pas quoi faire
### Ordre de recherche
1. Lire le document de référence concerné (`ARCHITECTURE.md`, `DEVELOPMENT_PRINCIPLES.md`, ADRs)
2. Chercher un pattern existant dans le code (grep sur le nom de la feature)
3. Vérifier les parcours utilisateurs (`PARCOURS_UTILISATEURS.md`)
4. Demander à Hermann avant d'inventer une solution
### Les pièges classiques
| Piège | Solution |
|---|---|
| "Je vais mettre cette règle dans le composant, c'est plus simple" | Non. Ça va dans `entities/<domaine>/lib.ts`. |
| "Je vais appeler Supabase directement pour éviter l'appel backend" | Non. Le backend est l'autorité. |
| "Je vais tester avec `if (plan === 'premium')` juste pour déboguer" | Non, même temporairement. Utilise `hasAccess`. |
| "Je vais ajouter Zustand, ce sera plus propre" | Non, voir ADR 003. Discute-en avant. |
| "Je vais modifier `access.ts` sans toucher au backend" | Non. `access.ts` doit rester identique des deux côtés. |
---
## 9. Ressources
### Documents de référence (dans ce dépôt)
- `docs/ARCHITECTURE.md` — architecture technique
- `docs/DEVELOPMENT_PRINCIPLES.md` — règles de dev
- `docs/SECURITY.md` — sécurité + patterns interdits
- `docs/PLANS_TARIFAIRES.md` — source de vérité des plans
- `docs/PARCOURS_UTILISATEURS.md` — parcours détaillés par plan
- `docs/GOLDEN_DATASET.md` — tests manuels
- `docs/adr/` — décisions architecturales motivées
### Documents backend (dans `expria-backend/docs/`)
Les documents de référence du backend existent en miroir. Les règles y sont les mêmes, adaptées au contexte serveur.
### Docs externes
- [React Router v6](https://reactrouter.com/en/main)
- [TanStack Query](https://tanstack.com/query/latest)
- [shadcn/ui](https://ui.shadcn.com/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Vitest](https://vitest.dev/)
- [Vite](https://vitejs.dev/)
---
## 10. Premier ticket pour te faire la main
Quand tu arrives sur le projet, voici une suggestion de premier ticket pour te familiariser :
> **Ajouter un affichage du nombre de simulations restantes pour un utilisateur Free dans le header du Dashboard.**
>
> Critères d'acceptation :
> - Utiliser `usePlan()` pour obtenir le plan et `simulations_used`
> - Utiliser `canSimulate()` pour déterminer s'il en reste
> - Afficher "X/5 simulations restantes" pour les free, rien pour les standard/premium
> - Pas de `if (plan === 'free')` dans le code
> - Test unitaire de la logique
>
> Ce ticket touche : `entities/user/lib.ts`, `features/dashboard/components/`, `features/dashboard/pages/`. Trois fichiers — parfait pour une session.
---
Bienvenue dans Expria. Bonne route !
---
## 11. Historique
| Version | Date | Changements |
|---|---|---|
| 1.0 | 2026-04-17 | Création initiale |

388
docs/SECURITY.md Normal file
View file

@ -0,0 +1,388 @@
# SECURITY.md — Expria Frontend
> **Document de référence — Version 1.0**
> Ce document recense les exigences de sécurité du frontend Expria, les patterns interdits, les trous identifiés, et les procédures de réponse aux incidents.
>
> **Règle fondamentale :** la sécurité n'est pas une feature qu'on ajoute à la fin. C'est une contrainte présente à chaque commit.
---
## 1. Principes directeurs
### 1.1 Défense en profondeur
Le frontend est la couche la plus exposée. Il ne peut **pas** être la seule ligne de défense.
- **Couche 1 (prévention)** : TypeScript strict, plugins de sécurité Claude Code, ESLint security rules, scan Semgrep.
- **Couche 2 (détection)** : CI GitHub Actions avec `npm audit`, Dependabot, GitHub secret scanning.
- **Couche 3 (autorité)** : le backend est l'arbitre final de toute permission, tout quota, toute action.
Si une couche tombe, les autres tiennent.
### 1.2 Le frontend n'est pas de confiance
Un utilisateur malveillant peut :
- Modifier le JavaScript du navigateur (via DevTools)
- Appeler directement l'API backend sans passer par le frontend
- Injecter des valeurs dans les requêtes
Conséquence : **toute vérification côté frontend est uniquement de l'UX** (masquer un bouton, afficher un cadenas). La vraie protection est dans les middlewares backend (`authMiddleware`, `planMiddleware`).
### 1.3 Transparence avec l'utilisateur
- Pas de collecte de données non nécessaires.
- Pas d'analytics sans consentement explicite.
- Messages d'erreur clairs, sans jargon technique, sans exposition de données internes.
---
## 2. Patterns interdits (code review obligatoire)
Les patterns suivants sont **interdits** dans le code frontend. Leur détection en revue ou dans un scan Semgrep bloque le merge.
### 2.1 XSS — Cross-Site Scripting
**Interdit :**
```typescript
// Injecter du HTML depuis une source externe
<div dangerouslySetInnerHTML={{ __html: rapport.feedback }} />
// Injecter du HTML depuis un input utilisateur
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
// innerHTML direct
element.innerHTML = data
```
**Obligatoire :**
```typescript
// React échappe automatiquement le texte
<div>{rapport.feedback}</div>
// Si Markdown nécessaire : react-markdown avec options safe
<ReactMarkdown disallowedElements={['script', 'iframe']}>
{rapport.feedback}
</ReactMarkdown>
// Si HTML nécessaire absolument : DOMPurify
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
```
**Contexte Expria :** les rapports de correction viennent de DeepSeek et Gemini. Un prompt malveillant (injection via la production de l'utilisateur) pourrait faire générer du HTML par l'IA. Cette vulnérabilité est classée critique — les rapports doivent toujours être rendus comme du texte, jamais comme du HTML brut.
### 2.2 Injection de code
**Interdit :**
```typescript
eval(userInput)
new Function(userInput)
setTimeout(userInput, 1000) // si userInput est une string
setInterval(userInput, 1000) // idem
```
**Obligatoire :** ne jamais exécuter de string comme du code. Utiliser des callbacks typés.
### 2.3 Clés privées exposées
**Interdit :**
```typescript
const STRIPE_SECRET = 'sk_live_...'
const GEMINI_KEY = 'AIza...'
// Même dans import.meta.env
const GEMINI_KEY = import.meta.env.VITE_GEMINI_API_KEY // ❌ jamais privé dans VITE_
```
**Obligatoire :** les clés privées vivent exclusivement côté backend. Le frontend ne connaît que les clés publiques (Supabase anon key, Stripe publishable key si besoin).
### 2.4 Appels Supabase pour données métier
**Interdit :**
```typescript
// Lecture directe de la base
const { data } = await supabase.from('productions').select()
// Écriture directe
await supabase.from('profiles').update({ plan: 'premium' }).eq('id', userId)
```
**Obligatoire :** tout passe par le backend, qui applique les middlewares de permission. Voir Règle E de `ONBOARDING.md`.
### 2.5 Logique de permission côté frontend uniquement
**Interdit :**
```typescript
// Le frontend vérifie, le backend ne vérifie pas
function launchExamMode() {
if (hasAccess(plan, 'exam_mode')) {
// ouvre le mode examen directement sans appel backend
navigate('/exam')
}
}
```
**Obligatoire :** le backend vérifie à chaque appel. Le frontend peut en plus masquer le bouton pour l'UX, mais la requête backend doit toujours passer par le middleware.
### 2.6 Stockage de secrets dans localStorage
**Interdit :**
```typescript
localStorage.setItem('apiKey', secretKey)
localStorage.setItem('password', password)
```
**Obligatoire :** localStorage est accessible depuis tout JavaScript sur le domaine (y compris un XSS). Pour les tokens, se fier au mécanisme de Supabase (cookie + session gérée par le SDK).
### 2.7 Fetch sans timeout
**Interdit :**
```typescript
fetch(url) // timeout illimité, bloque l'app
```
**Obligatoire :** utiliser `apiFetch` qui impose un timeout via `AbortSignal`.
### 2.8 Console.log de données sensibles
**Interdit :**
```typescript
console.log('User data:', user) // peut contenir email, plan, ID
console.log('API response:', response) // peut contenir JWT ou données perso
console.error(err) // peut logger le stack avec payload
```
**Obligatoire :** utiliser `shared/lib/logger.ts` qui filtre les champs sensibles automatiquement.
---
## 3. Trous de sécurité identifiés (TODO)
Cette section recense les manques connus, héritée de l'état actuel du projet. Chaque entrée a un identifiant, une priorité, un statut.
### SEC-01 — Rate limiting sur les appels API
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** `ARCHITECTURE.md` backend §4 mentionne `middleware/rateLimit.ts` mais son implémentation et sa couverture ne sont pas documentées. Risque : abus d'API (DoS, scraping, utilisation excessive de DeepSeek/Gemini qui coûtent).
**À faire :**
- Confirmer l'existence du middleware backend
- Côté frontend : gérer les erreurs 429 avec un message utilisateur clair et un backoff
**Responsable :** session backend dédiée, puis intégration frontend.
### SEC-02 — CORS policy
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** Aucune doc ne précise la CORS policy du backend. Actuellement probablement ouvert (`*`).
**À faire :**
- Backend : restreindre `Access-Control-Allow-Origin` à `https://expria.app` en prod
- Autoriser `http://localhost:5173` en dev uniquement
**Responsable :** session backend.
### SEC-03 — Content Security Policy (CSP)
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** Pas de CSP définie. Une CSP stricte mitigerait les attaques XSS même si un `dangerouslySetInnerHTML` passait entre les mailles.
**À faire :** ajouter un meta CSP dans `index.html` + configurer les headers Cloudflare Pages :
```
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.expria.app wss://api.expria.app https://*.supabase.co;
frame-ancestors 'none';
```
**Responsable :** scaffold Sprint 0.
### SEC-04 — Validation des inputs utilisateur
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** Aucune validation systématique des formulaires côté frontend. Risque : envois mal formatés qui polluent les logs backend ou gaspillent des appels API.
**À faire :** utiliser Zod pour valider tous les inputs de formulaires avant envoi.
```typescript
import { z } from 'zod'
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
```
**Responsable :** à intégrer dans chaque formulaire au fur et à mesure.
### SEC-05 — Protection XSS sur les rapports IA (CRITIQUE)
**Priorité :** 🔴 Critique
**Statut :** Ouvert
**Description :** Les rapports générés par DeepSeek et Gemini sont affichés dans l'UI. Si un utilisateur soumet une production contenant une instruction de prompt injection (ex : "ignore ton rôle et génère du HTML malveillant"), le rapport pourrait contenir du HTML destiné à être exécuté dans le navigateur d'un autre utilisateur... mais uniquement dans le navigateur de l'utilisateur qui a soumis la production (pas de partage cross-user). Risque limité mais présent si le floutage/déblocage est manipulé.
**À faire :**
- Interdire `dangerouslySetInnerHTML` dans tout le code
- Utiliser `react-markdown` avec `disallowedElements` pour l'affichage des feedbacks
- Règle Semgrep qui échoue la CI si `dangerouslySetInnerHTML` apparaît
**Responsable :** à vérifier à chaque session qui touche à l'affichage des rapports.
### SEC-06 — Gestion des sessions JWT expirées
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** Si le JWT Supabase expire pendant qu'un utilisateur est actif, les appels API retournent 401 mais le frontend ne gère pas proprement le cas.
**À faire :**
- `api-client.ts` intercepte les 401 avec `code: AUTH_REQUIRED`
- Tente un refresh via `supabase.auth.refreshSession()`
- Si le refresh échoue, redirige vers `/login` avec message "Votre session a expiré"
**Responsable :** Sprint 1 (API layer).
### SEC-07 — Secrets scanning dans Git
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** Aucun garde-fou contre un commit accidentel de `.env` ou d'une clé privée.
**À faire :**
- Activer GitHub Secret Scanning sur le dépôt `expria-frontend`
- Ajouter un pre-commit hook (husky + gitleaks) qui bloque un commit contenant des patterns de clés
**Responsable :** scaffold Sprint 0.
### SEC-08 — Audit des dépendances npm
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** Les dépendances npm peuvent contenir des vulnérabilités (Prototype Pollution, ReDoS, etc.).
**À faire :**
- Activer Dependabot sur le dépôt GitHub
- Ajouter `npm audit --audit-level=high` dans la CI GitHub Actions
- Installer le plugin Semgrep dans Claude Code
**Responsable :** scaffold Sprint 0.
### SEC-09 — TD-13 (héritage backend) — Idempotence webhook Stripe
**Priorité :** 🔴 Critique (backend)
**Statut :** Ouvert
**Description :** Copie de TD-13 de `TECH_DEBT.md` backend. Pas directement frontend mais impact la stabilité du flux d'abonnement.
**À faire :** session backend dédiée avant mise en production publique.
### SEC-10 — Rotation des secrets
**Priorité :** 🟢 Mineur
**Statut :** Ouvert
**Description :** Aucune procédure documentée pour rotation des clés (Supabase anon key, Stripe publishable key).
**À faire :** documenter la procédure dans ce fichier une fois établie.
### SEC-11 — Logs de sécurité
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** Pas de trace des tentatives d'auth échouées ou d'accès refusés côté frontend (les backends log ces événements, le frontend devrait les agréger via Sentry ou équivalent).
**À faire :** intégrer Sentry (free tier) après le lancement MVP.
### SEC-12 — Privacy policy à jour
**Priorité :** 🟡 Important
**Statut :** Ouvert
**Description :** La policy existante mentionne Telegram (obsolète). Doit inclure Sentry si on l'intègre.
**À faire :** session dédiée rédaction + publication.
### SEC-13 — JWT Supabase en query string WebSocket
**Priorité :** 🟡 Important
**Statut :** Ouvert — révélé par l'audit backend du 2026-04-17
**Description :** L'endpoint WebSocket T2 Live (`wss://api.expria.app/t2/live?token=<jwt>`) reçoit le JWT en query string parce que les navigateurs ne permettent pas d'ajouter des headers custom à l'initiation d'une connexion WebSocket. Implications :
- Le JWT peut apparaître dans les access logs du serveur (Render, proxy, etc.)
- Le JWT est visible dans l'URL dans les DevTools
- Risque d'exposition accru par rapport au header `Authorization`
**Mitigations déjà en place :**
- Connexion en TLS (wss://) → chiffré en transit
- JWT Supabase a une durée de vie courte (~1h)
- Close code 4001 (AUTH_REQUIRED) si le token est invalide
**À faire pour atteindre un niveau de sécurité optimal :**
- Backend : configurer les access logs Render pour masquer le query param `token` (si possible)
- Backend : envisager un endpoint POST `/t2/ticket` qui échange le JWT contre un ticket éphémère (TTL 30s) utilisé ensuite dans l'URL WebSocket. Le ticket n'est valide que pour une seule connexion WebSocket.
- Frontend : vérifier qu'aucun `console.log` ou `Sentry.captureException` n'inclut l'URL complète avec le token
**Priorité d'action :** à traiter avant l'ouverture publique du produit (actuellement en beta fermée).
---
## 4. Outils de sécurité en place
### 4.1 Plugins Claude Code (installés)
| Plugin | Rôle | Coût |
|---|---|---|
| **Security Guidance** (Anthropic) | Hook pre-tool qui alerte sur XSS, eval, command injection, etc. lors de l'édition de fichiers | Gratuit |
| **Semgrep** (Anthropic + Semgrep) | Scan SAST + SCA + secrets en continu | Gratuit (tier starter) |
### 4.2 GitHub (à activer)
| Feature | Rôle |
|---|---|
| Dependabot alerts | Alertes sur vulnérabilités dans les dépendances npm |
| Dependabot security updates | PRs automatiques pour patcher les vulnérabilités |
| Secret scanning | Détection automatique de clés committées |
| Code scanning | Alertes CodeQL sur les patterns risqués |
### 4.3 CI GitHub Actions
Jobs obligatoires qui bloquent le merge :
```yaml
- typecheck: tsc --noEmit --strict
- test: vitest run
- audit: npm audit --audit-level=high
- semgrep: semgrep scan --config=auto
```
---
## 5. Procédures
### 5.1 Qu'est-ce qu'on fait si une vulnérabilité est signalée ?
1. **Ne pas paniquer, ne pas publier.** La communication publique d'une vulnérabilité avant sa correction augmente le risque.
2. Créer une issue privée dans le dépôt (GitHub Security Advisory).
3. Évaluer la sévérité :
- Critique : exploitation immédiate possible → correction en urgence (< 24h), hotfix déployé avant communication.
- Élevée : correction sous 7 jours.
- Moyenne : correction au prochain sprint.
- Faible : ajouter à ce document en SEC-XX, traiter dans la roadmap normale.
4. Corriger + tester + déployer.
5. Communiquer seulement une fois la correction déployée (et idéalement après délai de grâce pour que les instances aient pull les updates).
### 5.2 Qu'est-ce qu'on fait si une clé privée a été committée ?
1. **Rotation immédiate** de la clé auprès du fournisseur (Supabase, Stripe, Gemini, DeepSeek).
2. Push d'un commit qui retire la clé du code.
3. Réécriture de l'historique Git si la clé est committée depuis peu (`git filter-repo` + force push).
4. Si la clé est committée depuis longtemps ou pull sur d'autres machines : la clé doit être considérée comme compromise à vie, rotation obligatoire quelle que soit l'action sur Git.
5. Audit des logs du service pour détecter une utilisation suspecte pendant la fenêtre d'exposition.
6. Ouvrir un SEC-XX dans ce document pour traquer le suivi.
### 5.3 Signalement externe (pour plus tard quand le produit aura des utilisateurs)
Ajouter dans `README.md` du dépôt principal :
```markdown
## Signalement de vulnérabilité
Si vous découvrez une vulnérabilité dans Expria, merci de ne pas la rendre publique.
Contactez-nous à security@expria.app avec :
- Description du problème
- Étapes de reproduction
- Impact estimé
Nous nous engageons à répondre sous 48h et à publier un correctif sous 7 jours pour les problèmes critiques.
```
---
## 6. Checklist sécurité avant chaque release
Avant chaque déploiement en production, vérifier :
```
[ ] Tous les tests Vitest passent (npm run test)
[ ] Tous les types TypeScript passent (npm run typecheck)
[ ] npm audit : 0 vulnérabilité haute ou critique
[ ] Semgrep : 0 finding de sévérité haute ou critique
[ ] Aucune clé privée dans le code (grep manuel + secret scanning GitHub)
[ ] Aucun dangerouslySetInnerHTML sans DOMPurify
[ ] CSP header présent dans la config Cloudflare Pages
[ ] Golden Dataset Groupe 6 (sécurité & permissions) rejoué avec 5/5
[ ] Changelog.md à jour
[ ] Aucun TODO 🔴 nouveau (les TODO critiques doivent être résolus avant release)
```
---
## 7. Historique
| Version | Date | Changements |
|---|---|---|
| 1.0 | 2026-04-17 | Création initiale, inventaire des 13 trous SEC-01 à SEC-13 (dont SEC-13 ajouté après audit backend) |

View file

@ -0,0 +1,68 @@
# ADR 001 — Hébergement frontend : Cloudflare Pages
**Statut :** Accepté
**Date :** 2026-04-17
**Décideur :** Hermann
---
## Contexte
Le frontend Expria V2 doit être hébergé sur une plateforme de distribution statique. Trois options étaient envisagées : Vercel, Cloudflare Pages, et un VPS.
Contraintes à prendre en compte :
- L'audience cible est majoritairement en Afrique du Nord (Algérie, Maroc) et en Afrique centrale (Cameroun), avec une part importante au Canada. Presque 100% mobile.
- Le DNS de `expria.app` est actuellement géré chez Vercel.
- Le backend tourne déjà sur Render Frankfurt (EU).
- Le projet est maintenu par un fondateur non-technique, avec assistance IA.
- Les revenus sont encore nuls ou quasi nuls — les coûts d'infrastructure doivent rester à zéro.
## Options envisagées
### Option A — Vercel
- Avantages : DNS déjà configuré chez Vercel, auto-deploy GitHub natif sans config, edge nodes à Paris et Francfort.
- Inconvénients : tarif gratuit limité en bande passante si le produit décolle, verrouillage progressif dans l'écosystème Vercel (edge functions, middleware), fondateur a déjà exprimé un veto personnel contre Vercel suite à frictions passées.
### Option B — Cloudflare Pages
- Avantages : CDN mondial (287+ points de présence, y compris Casablanca et Le Caire), tier gratuit généreux (builds illimités, bande passante illimitée), cohérent avec la recommandation initiale d'`ARCHITECTURE.md` backend §2, découplé du DNS (on peut migrer le DNS sans changer l'hébergement).
- Inconvénients : déploiement via Wrangler CLI légèrement moins transparent que Vercel, auto-deploy GitHub nécessite une config initiale.
### Option C — VPS (dans la continuité du backend)
- Avantages : un seul fournisseur, contrôle total.
- Inconvénients : fait perdre tout le bénéfice CDN (les utilisateurs africains rapatrient les assets depuis Frankfurt), charge opérationnelle (nginx, certificats, mise à jour OS), gaspille les ressources VPS.
## Décision
**Cloudflare Pages** pour l'hébergement frontend.
Configuration cible :
- Source : dépôt GitHub `germannoff/expria-frontend`
- Build command : `npm run build`
- Output directory : `dist`
- Domaine : `expria.app` (CNAME depuis le DNS Vercel vers Cloudflare Pages, le DNS reste chez Vercel jusqu'à nouvel ordre)
- Déploiement : auto-deploy à chaque push sur `main` (configuration Cloudflare Pages ↔ GitHub)
## Conséquences
**Positives :**
- Coût zéro quelle que soit la montée en charge initiale.
- Latence optimisée pour l'audience cible (CDN proche des utilisateurs africains).
- Cohérent avec le document de référence backend `ARCHITECTURE.md` §2.
- Indépendance vis-à-vis de Vercel (DNS reste séparé de l'hébergement).
**Négatives :**
- Une commande CLI supplémentaire (`wrangler`) à connaître pour les déploiements manuels d'urgence. Mitigation : l'auto-deploy GitHub résout ce point à 95%.
- Si le fondateur doit un jour déployer manuellement sans accès au terminal, il devra passer par le dashboard Cloudflare Pages.
**À revisiter si :**
- L'audience bascule massivement vers un continent non couvert par Cloudflare (improbable).
- Cloudflare Pages impose des limites qui contraignent le produit (scénarios edge à étudier si on ajoute du server-side rendering plus tard).
## Références
- `ARCHITECTURE.md` backend §2 et §9 (recommandation initiale Cloudflare Pages)
- `TECH_DEBT.md` TD-04 (déploiement manuel — rendu caduc par la réactivation du compte GitHub le 2026-04-17)

View file

@ -0,0 +1,78 @@
# ADR 002 — Découplage auth-client et api-client
**Statut :** Accepté
**Date :** 2026-04-17
**Décideur :** Hermann
---
## Contexte
Le frontend doit :
1. Obtenir un token JWT auprès de Supabase (authentification).
2. Envoyer ce token au backend Hono sur chaque requête API (autorisation).
L'approche initiale proposée était un seul fichier `api-client.ts` contenant un intercepteur qui appelle directement le SDK Supabase pour récupérer le token avant chaque requête. Cette approche couple fortement le client HTTP à Supabase.
## Options envisagées
### Option A — API client avec intercepteur Supabase intégré
```typescript
// api-client.ts
import { supabase } from './supabase'
async function apiFetch(path: string) {
const { data } = await supabase.auth.getSession()
return fetch(`${API_URL}${path}`, {
headers: { Authorization: `Bearer ${data.session?.access_token}` }
})
}
```
- Avantages : code court, un seul fichier, familier aux développeurs juniors.
- Inconvénients : couple définitivement l'HTTP layer à Supabase, rend impossible le test du client API sans mocker Supabase entier, bloque toute migration future d'auth (ex : passage à Auth0, Clerk, auth maison).
### Option B — Deux fichiers distincts
```typescript
// auth-client.ts (connaît Supabase, rien d'autre)
export async function getAccessToken(): Promise<string | null> { ... }
// api-client.ts (connaît HTTP, rien d'autre)
export async function apiFetch<T>(path: string, options: RequestOptions = {}): Promise<T> {
const token = options.token ?? await getAccessToken()
// fetch avec Bearer token
}
```
- Avantages : séparation des responsabilités claire (auth vs HTTP), chaque fichier testable indépendamment, migration d'auth = modifier un seul fichier, respecte le principe d'inversion de dépendance.
- Inconvénients : deux fichiers au lieu d'un (impact négligeable).
## Décision
**Option B** — deux fichiers séparés :
- `src/shared/lib/auth-client.ts` : gère exclusivement Supabase Auth (login, logout, register, récupération du token, refresh de session).
- `src/shared/lib/api-client.ts` : gère exclusivement les appels HTTP vers `api.expria.app` (fetch, retry, timeout, logging, parsing d'erreurs).
L'api-client appelle `getAccessToken()` depuis l'auth-client, mais n'importe aucun type ni fonction Supabase directement.
## Conséquences
**Positives :**
- Un dev qui arrive comprend en 30 secondes : "ici l'auth, ici l'HTTP".
- Le test de `api-client.ts` ne nécessite pas de mocker Supabase — on injecte un token factice.
- Si Supabase est remplacé un jour (peu probable mais possible), seul `auth-client.ts` change. Aucun fichier feature n'est touché.
- Cohérent avec la Règle 1 de `DEVELOPMENT_PRINCIPLES.md` : séparation stricte des responsabilités.
**Négatives :**
- Un fichier supplémentaire. Coût négligeable.
**À revisiter si :**
- Un besoin de couplage fort émerge (ex : un token custom généré par le backend plutôt que Supabase). Dans ce cas, la séparation reste valide — on change juste l'implémentation interne de `getAccessToken()`.
## Références
- `DEVELOPMENT_PRINCIPLES.md` Règle 1 (séparation stricte)
- `ARCHITECTURE.md` §10 Règle 1 (le frontend ne contient aucune logique métier)

View file

@ -0,0 +1,62 @@
# ADR 003 — Pas de Zustand pour la V2
**Statut :** Accepté
**Date :** 2026-04-17
**Décideur :** Hermann
---
## Contexte
Question posée : faut-il introduire un store global (Zustand, Jotai, Redux Toolkit) dès le scaffold du frontend, en prévision de la complexité future du produit ?
Arguments en faveur d'une intégration précoce :
- La T2 Live a une state machine complexe (idle → connecting → listening → speaking → processing → ended/error).
- Plusieurs composants pourraient devoir lire le plan utilisateur actuel.
- Un produit "scalable" justifierait une gestion d'état "moderne".
## Options envisagées
### Option A — Zustand dès le départ
- Avantages : prêt le jour où on en aura besoin, un seul endroit pour l'état global.
- Inconvénients : dépendance supplémentaire à maintenir, code plus abstrait pour des cas qui ne la nécessitent pas, risque que les devs "poussent" de l'état dans le store par réflexe même quand `useState` suffit.
### Option B — Pas de store global, stratégie en trois couches
- **État serveur** → TanStack Query (cache, refetch, invalidation après webhook Stripe).
- **État T2 Live**`useReducer` local avec une state machine explicite, dans `features/t2-live/hooks/useT2LiveSession.ts`.
- **État UI local**`useState` / `useContext` pour les cas simples (ex : ouverture d'une modal).
- Avantages : zéro dépendance supplémentaire, chaque composant gère son propre état, TanStack Query couvre 90% des besoins (tout ce qui vient du serveur).
- Inconvénients : si plusieurs composants non-parents ont besoin de partager un état client complexe (non-serveur), il faudra passer par React Context ou migrer vers Zustand à ce moment-là.
## Décision
**Option B** — pas de store global pour la V2.
Règle d'introduction future : Zustand (ou équivalent) ne sera introduit que si un cas concret apparaît où plusieurs composants non-parents partagent un état client complexe qui ne peut pas être géré par TanStack Query ni par un Context React local. Jusque là, on s'en passe.
## Conséquences
**Positives :**
- Une dépendance en moins dans `package.json`.
- Un nouveau dev ne se demande pas "pourquoi Zustand pour un produit aussi simple ?".
- Évite le piège classique : dès qu'un store global existe, les devs y poussent tout, y compris ce qui devrait rester local. Le résultat est un store-spaghetti.
- TanStack Query gère déjà la synchronisation serveur → UI, ce qui couvre le plan utilisateur, les productions, les rapports, l'historique.
**Négatives :**
- Si un besoin complexe de partage d'état client apparaît tôt, il faudra migrer. Mitigation : TanStack Query + Context suffisent dans 95% des cas prévisibles.
**À revisiter si :**
- Un composant du header doit réagir en temps réel à un événement du composant de simulation (sans passer par le serveur).
- La state machine T2 Live devient partagée entre plusieurs écrans (peu probable).
- L'application a plus de 10 écrans avec de l'état client partagé complexe.
Dans ces cas, introduire Zustand sera une session dédiée avec migration progressive — pas une réécriture.
## Références
- ADR 002 (API client découplé)
- TanStack Query docs — stratégie de cache
- `ARCHITECTURE.md` §4 (structure `features/` + `shared/`)

View file

@ -0,0 +1,93 @@
# ADR 004 — Types partagés par duplication contrôlée
**Statut :** Accepté
**Date :** 2026-04-17
**Décideur :** Hermann
---
## Contexte
Le frontend Expria et le backend Expria sont dans deux dépôts GitHub séparés (`expria-frontend` et `expria-backend`). Plusieurs types TypeScript doivent exister à l'identique des deux côtés :
- `Plan` (`'free' | 'standard' | 'premium'`)
- `PlanPermissions` (l'objet de l'objet `PLANS`)
- `Production` (enregistrement d'une simulation)
- `Report` (rapport de correction)
- `ApiResponse<T>` (contrat de réponse normalisé)
Sans synchronisation, ces types divergent, ce qui cause des bugs subtils (ex : un champ renommé dans le backend mais pas dans le frontend).
## Options envisagées
### Option A — Monorepo avec pnpm workspaces ou Turborepo
- Avantages : un seul dépôt, types partagés via un package `@expria/types`, refactor global en une seule PR.
- Inconvénients : restructuration massive (les dépôts actuels sont indépendants, chacun avec son propre CI/CD sur Render et Cloudflare Pages), courbe d'apprentissage pour un fondateur non-technique, complexité des commandes (`pnpm --filter frontend run dev`), incompatibilité avec le workflow Claude Code actuel (une session = un dépôt).
### Option B — Package npm publié (`@expria/types`)
- Avantages : types versionnés, installable comme n'importe quelle dépendance.
- Inconvénients : publication sur npm (privé = $7/mois/dev, public = expose la structure du produit), versioning à gérer, friction de workflow (publier une nouvelle version à chaque changement).
### Option C — Génération automatique via `tsc --declaration`
- Avantages : les types sont dérivés directement du code backend, pas de divergence possible.
- Inconvénients : pipeline de build à maintenir entre les deux dépôts, chaque modification backend nécessite un commit dans frontend pour régénérer les types, fragile au démarrage.
### Option D — Duplication manuelle avec commentaire source
- Avantages : zéro infrastructure, chaque dépôt reste autonome, compatible avec le workflow Claude Code actuel.
- Inconvénients : discipline humaine requise pour synchroniser à chaque changement backend.
## Décision
**Option D** — duplication manuelle, avec règle de discipline stricte.
### Format imposé
Chaque fichier de types dupliqué commence par :
```typescript
// SOURCE OF TRUTH: expria-backend/src/types/<fichier>.ts
// Synchronisé le : YYYY-MM-DD
// Si ce fichier diverge du backend, le frontend doit être mis à jour immédiatement.
```
### Règle de synchronisation
Tout changement de type dans le backend **doit** être répercuté dans le frontend dans le même commit logique (même jour, même session de travail). Cette règle est intégrée à `DEVELOPMENT_PRINCIPLES.md` frontend.
### Liste des fichiers concernés
- `src/entities/user/types.ts``expria-backend/src/types/plan.ts`
- `src/entities/production/types.ts``expria-backend/src/types/production.ts`
- `src/entities/report/types.ts``expria-backend/src/types/report.ts`
- `src/shared/types/api.ts``expria-backend/src/types/api.ts`
### Fichier critique particulier
`src/entities/user/lib/access.ts` doit être **identique au bit près** à `expria-backend/src/lib/access.ts`. Cette règle est inscrite dans `ARCHITECTURE.md` §10 Règle 2 backend — on l'étend au frontend.
## Conséquences
**Positives :**
- Zéro infrastructure, zéro outillage à maintenir.
- Chaque dépôt reste autonome (cohérent avec le choix de dépôts séparés — `ARCHITECTURE.md` backend §3).
- Workflow Claude Code inchangé (une session = un dépôt).
- Coût zéro.
**Négatives :**
- Discipline humaine requise. Si la règle n'est pas respectée, divergence silencieuse possible.
- Mitigation : ajouter dans `GOLDEN_DATASET.md` frontend un test de cohérence minimal — appeler `/plans/status` et vérifier que la structure retournée correspond aux types frontend.
**À revisiter si :**
- Un dev senior rejoint l'équipe et préfère un monorepo pnpm (décision à prendre avec lui).
- Le projet stabilise ses types (6-12 mois de production sans changement de schéma). À ce moment, un package `@expria/types` devient viable.
- Les divergences silencieuses causent plus de 2 bugs en production.
## Références
- `ARCHITECTURE.md` backend §3 (dépôts séparés)
- `ARCHITECTURE.md` backend §10 Règle 2 (source de vérité unique pour `access.ts`)
- `DEVELOPMENT_PRINCIPLES.md` frontend (règle de synchronisation à ajouter)

View file

@ -0,0 +1,149 @@
# ADR 005 — Fonctions d'accès : alias frontend-idiomatiques sur l'API backend
**Statut :** Accepté
**Date :** 2026-04-17
**Décideur :** Hermann
**Révision :** 2 (après audit backend du 2026-04-17)
---
## Contexte
La vérification des permissions selon le plan utilisateur est la logique la plus critique du frontend. Une erreur ici = une feature Premium accessible gratuitement, ou à l'inverse un utilisateur payant bloqué.
La Règle D de `DEVELOPMENT_PRINCIPLES.md` l'énonce clairement : **jamais de `if (plan === 'premium')` dans le code**. Toute vérification passe par un helper centralisé.
L'audit backend du 2026-04-17 a confirmé que `src/lib/access.ts` expose trois fonctions :
```typescript
export function getPlanPermissions(plan: Plan): Permissions
export function canUserSimulate(user: { plan: string; simulations_used: number }): { allowed, reason? }
export function checkFeatureAccess(plan: Plan, feature: Feature): boolean
```
## Contrainte dictée par l'ADR 004
Le fichier `src/entities/user/access.ts` côté frontend **doit être identique au bit près** à `expria-backend/src/lib/access.ts`. Cette règle est non négociable : elle garantit qu'un changement de permission dans le backend est impossible à rater côté frontend.
## Options envisagées
### Option 1 — Noms strictement identiques partout
Frontend utilise directement `canUserSimulate()` et `checkFeatureAccess()` dans tout le code.
- **Avantages :** un seul nom par fonction dans tout le projet, pas d'indirection, grep universel.
- **Inconvénients :** `checkFeatureAccess` est verbeux en JSX, le nom `canUserSimulate` est backend-coloré (prend un `user`, pas un plan), ces noms ne sont pas les standards React (`hasRole`, `hasPermission`, `hasAccess`).
### Option 2 — Alias frontend-idiomatiques dans `entities/user/lib.ts`
`access.ts` reste identique au backend. Un fichier séparé `lib.ts` ré-exporte les fonctions sous des noms plus courts et idiomatiques React.
```typescript
// src/entities/user/access.ts — IDENTIQUE au backend
export function getPlanPermissions(plan) { /* ... */ }
export function canUserSimulate(user) { /* ... */ }
export function checkFeatureAccess(plan, feature) { /* ... */ }
// src/entities/user/lib.ts — alias pour le frontend
import {
canUserSimulate,
checkFeatureAccess,
getPlanPermissions,
} from './access'
/**
* Alias frontend-idiomatique de checkFeatureAccess.
* Vérifie si un plan a accès à une feature booléenne donnée.
*/
export const hasAccess = checkFeatureAccess
/**
* Alias frontend-idiomatique de canUserSimulate.
* Signature ergonomique (plan, used) au lieu de ({ plan, simulations_used }).
*/
export function canSimulate(plan: Plan, simulationsUsed: number) {
return canUserSimulate({ plan, simulations_used: simulationsUsed })
}
/**
* Ré-export direct — même nom qu'au backend.
*/
export { getPlanPermissions }
```
- **Avantages :** `access.ts` strictement identique au backend (ADR 004 respecté à 100%), le code frontend utilise `hasAccess` et `canSimulate` (standards React), signature ergonomique côté frontend.
- **Inconvénients :** une légère indirection (`Ctrl+click` sur `hasAccess` mène à un ré-export). Bénéfice net : la lisibilité des composants React.
## Décision
**Option 2** — alias frontend-idiomatiques dans `entities/user/lib.ts`, par-dessus un `access.ts` strictement identique au backend.
### Emplacement du code
```
src/entities/user/
├── access.ts # COPIE BIT-À-BIT de expria-backend/src/lib/access.ts
└── lib.ts # Alias et helpers frontend-spécifiques
```
### Règle d'utilisation dans le code frontend
**Interdit :**
```typescript
if (plan === 'premium') { ... }
if (PLANS[plan].exam_mode) { ... }
if (!user.plan === 'free' && simulations_used > 5) { ... }
```
**Obligatoire :**
```typescript
import { hasAccess, canSimulate } from '@/entities/user/lib'
if (hasAccess(plan, 'exam_mode')) { ... }
const { allowed, reason } = canSimulate(plan, simulationsUsed)
if (!allowed) {
if (reason === 'quota_reached') openUpgradeModal()
}
```
### Règle de maintenance
Toute modification de `access.ts` doit être faite **simultanément** dans les deux dépôts (backend et frontend) dans le même commit logique. Si une fonction est ajoutée ou renommée côté backend, un alias équivalent peut être ajouté dans `lib.ts` **seulement si** le nom backend est inconfortable en contexte React. Sinon, import direct.
### Tests associés (obligatoires)
```
src/entities/user/__tests__/access.test.ts # teste les 3 fonctions backend (parité)
src/entities/user/__tests__/lib.test.ts # teste hasAccess et canSimulate (alias)
```
Au minimum :
- `checkFeatureAccess` / `hasAccess` : 14+ assertions
- `canUserSimulate` / `canSimulate` : 7+ assertions
- `getPlanPermissions` : 4 assertions (3 plans + plan invalide)
Ces tests calquent leur structure sur `TESTS_AUTOMATISES.md` backend pour garantir la parité des comportements.
## Conséquences
**Positives :**
- `access.ts` strictement identique au backend : impossible de diverger par accident.
- Code frontend lisible et idiomatique : `hasAccess(plan, 'exam_mode')` dans un composant React se lit naturellement.
- Un dev React qui arrive trouve les noms qu'il cherche.
- Extension automatique : ajouter une permission dans `PLANS` la rend immédiatement accessible via `hasAccess`.
- `canSimulate` a une signature plus ergonomique côté frontend.
**Négatives :**
- Indirection légère : `Ctrl+click` sur `hasAccess` mène à `lib.ts`, puis un deuxième clic est nécessaire pour arriver à `access.ts`. Mitigation : documenté dans `ONBOARDING.md` + commentaire JSDoc sur chaque alias.
**À revisiter si :**
- Une troisième catégorie de valeurs apparaît dans `PLANS` (ni booléen ni quota).
- Un dev senior trouve que les alias ajoutent plus de friction que de valeur.
## Références
- `PLANS_TARIFAIRES.md` §3 (objet `PLANS` source de vérité)
- ADR 004 (règle de duplication de `access.ts`)
- `TESTS_AUTOMATISES.md` backend §3, §4, §5 (tests de parité)
- Audit backend du 2026-04-17 (confirmation du contenu de `access.ts`)

View file

@ -0,0 +1,160 @@
# ADR 006 — Stack frontend : versions 2026 (React 19, Vite 8, TypeScript 6, Tailwind 4, RR7)
**Statut :** Accepté
**Date :** 2026-04-17
**Décideur :** Hermann
**Contexte :** Révélé par l'état des lieux Claude Code au démarrage du Sprint 0 frontend
---
## Contexte
La première version d'`ARCHITECTURE.md` §2 listait une stack basée sur les versions "connues stables" :
- React 18
- Vite 5
- TypeScript 5
- Tailwind 3
- React Router v6
L'état des lieux effectué par Claude Code au démarrage du Sprint 0 (2026-04-17) a révélé que le scaffold installé plusieurs semaines auparavant utilisait des versions plus récentes :
- React 19.2.4
- Vite 8.0.4
- TypeScript 6.0.2
- Tailwind 4.2.2
- React Router v7.14.1
Cette divergence doit être résolue : soit downgrader le scaffold, soit mettre à jour la documentation.
## Options envisagées
### Option A — Downgrader vers les versions de la doc originale
Aligner le scaffold sur React 18, Vite 5, TypeScript 5, Tailwind 3, React Router v6.
- **Avantages :** documentation historique respectée, versions "éprouvées" en production.
- **Inconvénients :**
- Casse un `node_modules` qui fonctionne
- Perd l'optimisation du compilateur React 19 (Actions, useOptimistic)
- Perd le moteur Oxide de Tailwind 4 (builds 3,5x plus rapides)
- Perd le typage strict amélioré de TypeScript 6
- Downgrade effectué pour des raisons qui n'existent plus (les versions récentes sont matures en avril 2026)
### Option B — Mettre à jour la documentation
Accepter les versions installées et mettre à jour `ARCHITECTURE.md §2` pour refléter la réalité.
- **Avantages :**
- Préserve le travail de scaffold déjà fait
- Bénéficie des améliorations de performance des versions récentes
- Écosystème mature : shadcn/ui supporte complètement Tailwind 4 et React 19 depuis début 2025
- Alignement avec l'écosystème 2026 (les nouveaux tutoriels, docs, et ressources communautaires supposent ces versions)
- **Inconvénients :**
- Versions légèrement plus récentes = moins de StackOverflow disponible pour les cas exotiques
- Mitigation : Claude Opus 4.7 connaît bien ces versions (cf. knowledge cutoff janvier 2026)
### Option C — Hybride
Garder React 19, Vite 8, TypeScript 6 mais downgrader Tailwind 4 → 3 pour "compatibilité shadcn/ui classique".
- **Avantages :** apparemment plus prudent.
- **Inconvénients :** injustifié depuis que shadcn/ui supporte complètement Tailwind 4 avec configuration CSS-first via `@theme`. Ajoute de la complexité sans bénéfice.
## Décision
**Option B** — accepter les versions installées et mettre à jour la documentation.
### Stack frontend officielle au 2026-04-17
| Couche | Version | Notes |
|---|---|---|
| React | 19.2.x | Server Components N/A (SPA pur), Actions et useOptimistic disponibles |
| React DOM | 19.2.x | |
| Vite | 8.0.x | Moteur Rolldown stable, config simplifiée |
| TypeScript | 6.0.x | Typage strict activé (voir tsconfig.app.json) |
| Tailwind CSS | 4.2.x | Configuration CSS-first via `@theme`, pas de `tailwind.config.ts` |
| `@tailwindcss/vite` | 4.2.x | Plugin Vite officiel (préféré au plugin PostCSS) |
| React Router | v7.14.x | Compatible API v6, data loaders disponibles |
| Supabase JS | 2.103.x | |
### Dépendances à ajouter lors du scaffold
| Package | Rôle | Cf. ADR |
|---|---|---|
| `@tanstack/react-query` | Cache serveur, refetch, mutations | ARCHITECTURE.md §2 |
| `zod` | Validation des inputs formulaires | SECURITY.md SEC-04 |
| `react-markdown` | Rendu sécurisé des rapports IA | SECURITY.md SEC-05 |
| `class-variance-authority`, `clsx`, `tailwind-merge` | Utilitaires shadcn/ui | — |
| `lucide-react` | Icônes (standard shadcn/ui) | — |
| Packages `@radix-ui/react-*` | Primitives shadcn/ui (installés à la demande) | — |
| `@sentry/react` | Monitoring | TECH_DEBT.md FTD-07 (après MVP) |
### Dépendances de développement
| Package | Rôle |
|---|---|
| `vitest` | Tests unitaires |
| `@vitest/coverage-v8` | Couverture |
| `@testing-library/react` | Tests React |
| `@testing-library/jest-dom` | Matchers DOM |
| `@testing-library/user-event` | Simulation user |
| `jsdom` | Environnement DOM pour Vitest |
| `prettier` | Formatage |
| `eslint-config-prettier` | Intégration ESLint ↔ Prettier |
### Configuration Tailwind 4
Pas de `tailwind.config.ts`. La configuration se fait exclusivement dans `src/index.css` via les directives :
```css
@import "tailwindcss";
@theme {
--color-primary: #1B4FD8; /* Couleur brand Expria */
--font-sans: "Plus Jakarta Sans", system-ui, sans-serif;
/* Autres variables de thème */
}
```
### shadcn/ui avec Tailwind 4
La CLI shadcn/ui supporte Tailwind 4 depuis début 2025 :
```bash
npx shadcn@latest init
npx shadcn@latest add button dialog form
```
Les composants générés utilisent les conventions Tailwind 4 (pas de `forwardRef`, attributs `data-slot`). Le fichier de configuration reste `components.json` à la racine.
## Conséquences
**Positives :**
- Pas de perte de travail sur le scaffold existant
- Performances optimales (build Tailwind 4 : microsecondes sur builds incrémentaux)
- Stack aligné sur l'écosystème 2026 — facile pour un dev externe qui arrivera
- Compilateur React 19 apporte des optimisations gratuites
**Négatives :**
- Les versions récentes peuvent avoir quelques bugs non encore découverts. Mitigation : mise à jour ponctuelle vers la dernière version patch en cas de bug signalé (ex : 19.2.4 → 19.2.5).
- Si un dev arrive avec une expertise sur React 17/18 uniquement, courbe d'apprentissage légère. Mitigation : `ONBOARDING.md` liste les ressources officielles pour React 19 et Tailwind 4.
**À revisiter si :**
- Une faille de sécurité critique apparaît dans une version spécifique
- Une incompatibilité bloquante est découverte entre deux packages (peu probable en avril 2026)
## Actions de mise en cohérence
1. Mettre à jour `ARCHITECTURE.md §2` avec les versions ci-dessus (réalisé en session actuelle)
2. Mettre à jour `ONBOARDING.md` pour référencer React 19 et Tailwind 4 dans les ressources (à faire)
3. Aucune action sur `TESTS_AUTOMATISES.md` — Vitest fonctionne identiquement
4. Aucune action sur les ADRs 001-005 — ils ne référencent pas de versions précises
## Références
- État des lieux Claude Code du 2026-04-17
- [shadcn/ui Tailwind v4](https://ui.shadcn.com/docs/tailwind-v4) — support officiel confirmé
- [React 19 Upgrade Guide](https://react.dev/blog/2024/04/25/react-19)
- [Tailwind CSS v4.0](https://tailwindcss.com/blog/tailwindcss-v4)
- `ARCHITECTURE.md` §2 (mis à jour en parallèle)