feat: Infrastructure multi-tenant avec isolation par sous-domaine

Une application SaaS éducative nécessite une séparation stricte des données
entre établissements scolaires. L'architecture multi-tenant par sous-domaine
(ecole-alpha.classeo.local) permet cette isolation tout en utilisant une
base de code unique.

Le choix d'une résolution basée sur les sous-domaines plutôt que sur des
headers ou tokens facilite le routage au niveau infrastructure (reverse proxy)
et offre une UX plus naturelle où chaque école accède à "son" URL dédiée.
This commit is contained in:
2026-01-30 23:34:10 +01:00
parent 6da5996340
commit 1fd256346a
71 changed files with 14390 additions and 37 deletions

View File

@@ -0,0 +1,62 @@
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
/**
* Construit l'URL de base de l'API en fonction du hostname actuel.
*
* En multi-tenant, l'API utilise le même sous-domaine que le frontend
* pour garantir l'isolation des données par établissement.
*
* Exemples :
* - Dev: ecole-alpha.classeo.local:5174 -> ecole-alpha.classeo.local:18000/api
* - Prod: ecole-alpha.classeo.fr -> ecole-alpha.classeo.fr/api (relative)
*/
export function getApiBaseUrl(): string {
// Côté browser : toujours utiliser le hostname actuel pour préserver le tenant
if (browser) {
const { hostname, protocol } = window.location;
// En prod (pas de port API séparé), utiliser une URL relative
// Cela préserve automatiquement le sous-domaine du tenant
if (!env['PUBLIC_API_PORT']) {
return '/api';
}
// En dev, construire l'URL avec le hostname actuel et le port API
const apiPort = env['PUBLIC_API_PORT'];
return `${protocol}//${hostname}:${apiPort}/api`;
}
// SSR : utiliser PUBLIC_API_URL si défini, sinon fallback interne
if (env['PUBLIC_API_URL']) {
return env['PUBLIC_API_URL'];
}
// SSR fallback : communication interne Docker
return 'http://php:8000/api';
}
/**
* Extrait le sous-domaine (tenant) du hostname actuel.
*
* Exemple : ecole-alpha.classeo.local -> ecole-alpha
*/
export function getCurrentTenant(): string | null {
if (!browser) return null;
const hostname = window.location.hostname;
const baseDomain = env['PUBLIC_BASE_DOMAIN'] || 'classeo.local';
if (!hostname.endsWith(baseDomain)) {
return null;
}
const subdomain = hostname.replace(`.${baseDomain}`, '');
// Pas de sous-domaine ou sous-domaine réservé
if (!subdomain || subdomain === hostname || subdomain === 'www') {
return null;
}
return subdomain;
}

View File

@@ -0,0 +1 @@
export { getApiBaseUrl, getCurrentTenant } from './config';