Files
MaksTinyWorkshop 9b7af9f1b0 Refonte Structure
2026-03-25 08:34:19 +01:00

7.9 KiB

Backend — Patterns : Multi-tenant

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/patterns/README.md pour l'index complet.


Pattern : Guardrails multi-tenant — 403 vs 404 selon la sémantique

  • Objectif : éviter les fuites d'information inter-tenant tout en gardant une sémantique d'erreur claire.
  • Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur.
  • Quand l'utiliser : dès qu'une vérification d'appartenance tenant peut soit refuser explicitement l'accès, soit masquer l'existence d'une ressource.
  • Quand l'éviter : contexte mono-tenant ou endpoints purement internes sans enjeu de fuite.
  • Avantage :
    • clarifie la convention de sécurité
    • évite les réponses incohérentes selon les modules
    • facilite les tests d'isolation tenant
  • Limites / vigilance :
    • la convention doit être documentée et appliquée partout
    • un mauvais choix entre 403 et 404 peut révéler une information sensible
  • Validé le : 16-03-2026
  • Contexte technique : API multi-tenant / HTTP / services métier

Implémentation (exemple minimal)

- `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais l'accès refusé
- `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer l'existence d'une ressource d'un autre tenant
- documenter la convention dans le module
- couvrir les deux sémantiques par des tests dédiés

Checklist

  • Convention 403 vs 404 documentée
  • Helpers distincts selon la sémantique métier
  • Aucune fuite d'existence cross-tenant sur les ressources métier
  • Tests dédiés sur les deux comportements

Pattern : Repository tenant-aware — tenantId obligatoire dans la signature

  • Objectif : rendre impossible par construction une query non scopée sur un domaine multi-tenant.
  • Contexte : repositories ou services d'accès aux données sur ressources tenant-scoped.
  • Quand l'utiliser : dès qu'un domaine métier est massivement filtré par tenant.
  • Quand l'éviter : domaines réellement globaux ou méthodes volontairement cross-tenant.
  • Avantage :
    • force le scoping dès la signature TypeScript
    • réduit les oublis de filtre tenant dans les call sites
    • rend les exceptions cross-tenant visibles
  • Limites / vigilance :
    • les exceptions cross-tenant doivent être rares et documentées explicitement
    • ne dispense pas d'un second garde-fou dans les mutations sensibles
  • Validé le : 16-03-2026
  • Contexte technique : TypeScript / Prisma / architecture repository

Implémentation (exemple minimal)

- chaque méthode métier tenant-scoped prend `tenantId` en paramètre obligatoire
- les méthodes réellement cross-tenant sont nommées et documentées comme exception
- les call sites Prisma directs sur ces domaines sont interdits ou supprimés

Checklist

  • tenantId obligatoire sur les méthodes tenant-scoped
  • Exceptions cross-tenant documentées
  • Appels directs concurrents à Prisma supprimés
  • Tests sur scoping tenant au niveau repository

Pattern : Défense en profondeur — inclure tenantId dans les updates

  • Objectif : éviter une mutation cross-tenant même si un identifiant a été mal résolu en amont.
  • Contexte : update ou updateMany sur une ressource tenant-scoped.
  • Quand l'utiliser : dès qu'une mutation dépend d'un id reçu ou résolu dans un flux multi-tenant.
  • Quand l'éviter : ressources globales non liées à un tenant.
  • Avantage :
    • ajoute une seconde barrière côté base
    • réduit l'impact d'un call site mal scopé
    • rend la mutation plus sûre sans complexité forte
  • Limites / vigilance :
    • ne remplace pas le scoping en lecture ni la vérification d'autorisation
    • suppose que tenantId soit disponible au moment de la mutation
  • Validé le : 16-03-2026
  • Contexte technique : Prisma / multi-tenant / mutations métier

Implémentation (exemple minimal)

- préférer `where: { id, tenantId }` à `where: { id }` sur les updates tenant-scoped
- appliquer la même règle sur `updateMany` et opérations de révocation
- conserver les vérifications métier amont, mais ne pas leur déléguer toute la sécurité

Checklist

  • tenantId présent dans les clauses where des updates sensibles
  • Pas de mutation tenant-scoped basée sur id seul
  • Revue explicite des exceptions documentées

Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié

  • Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant.
  • Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier.
  • Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier.
  • Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct.
  • Validé le : 17-03-2026
  • Contexte technique : Next.js / TypeScript — app-template-resto

Implémentation

// ✅ src/server/tenant/resolvePublicTenant.ts
export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... }

// ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";

Pattern : Helper centralisé d'activation de features tenant-scoped

  • Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur.
  • Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations).
  • Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant.
  • Avantage :
    • helper pur et testable sans I/O
    • comportement par défaut sain (null/undefined → tout activé)
    • composants de navigation et pages importent ce helper, jamais Prisma directement
  • Validé le : 17-03-2026
  • Contexte technique : Next.js App Router / TypeScript — app-template-resto

Implémentation

// src/server/public/publicPagesConfig.ts
export function isPublicPageEnabled(
  config: PublicPagesConfigRecord | null | undefined,
  pageKey: PublicPageKey
): boolean {
  if (!config) return true; // config absente = tout activé par défaut
  return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]];
}

Règle : null/undefined → tout activé. Évite les régressions si la config n'a pas été provisionnée.


Pattern : EN enforcement optionnel par tenant (toggle + publish gate)

  • Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication.
  • Contexte : app multi-tenant avec internationalisation optionnelle.
  • Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n.
  • Validé le : 21-03-2026
  • Contexte technique : Prisma / Next.js App Router — app-template-resto

Implémentation

// 1. Modèle Tenant
enableEn Boolean @default(false)

// 2. Vérification dans chaque action mutante (create/update)
const { enableEn } = await getEnConfig(tenantId);
if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 });

// 3. Gate publish — vérification de complétude
const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all
// Exclut : isSystem:true, tenantId:null, isVisible:false
if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 });

Règles :

  • isVisible: false n'est pas inclus dans le check (une entité masquée ne bloque pas la publication)
  • revalidatePath sur toutes les pages menu après toggle du flag (pas seulement /settings)