expria-frontend/docs/adr/005-has-access-typed-strict.md

6 KiB

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 :

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.

// 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 :

if (plan === 'premium') { ... }
if (PLANS[plan].exam_mode) { ... }
if (!user.plan === 'free' && simulations_used > 5) { ... }

Obligatoire :

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)