À propos de ce guide
Ce guide s'adresse aux développeurs travaillant sur le code de bjschool (nom technique interne :
SchoolBoost). Il résume l'architecture, les conventions et les règles de sécurité définies dans
les documents internes AMBJ Consulting (CLAUDE.md, ARCHITECTURE.md, SECURITY.md, CODING.md,
TESTING.md, GIT.md). En cas de divergence, ces documents sources font foi.
Topologie des dépôts
bjschool est un polyrepo de trois dépôts frères, plus le site vitrine que vous lisez en ce moment :
| Dépôt | Rôle |
|---|---|
SchoolBoost-infra |
Spécifications, plan de livraison, docker-compose, QA |
SchoolBoost-api |
Backend ASP.NET Core |
SchoolBoost-front |
SPA Vue 3 + Vite |
bjschool-site |
Site vitrine + manuels (ce dépôt) |
Stack technique
| Couche | Technologie |
|---|---|
| Framework backend | ASP.NET Core 10 — REST API, Controllers, Middleware, DI |
| ORM | Entity Framework Core 10 (migrations, filtre global multi-tenant) |
| Base de données | PostgreSQL 16 — une base par tenant + schoolboost_admin |
| CQRS | MediatR — tous les cas d'usage passent par Command/Query |
| Validation | FluentValidation |
| Temps réel | SignalR (portail public de disponibilité) |
| Cache | Redis (sessions + backplane SignalR) |
| Sécurité | BCrypt cost ≥ 12, JWT en cookies HttpOnly |
| Tests | xUnit + Moq (MockBehavior.Strict) + FluentAssertions + Testcontainers |
| Frontend | Vue 3 + Vite + Pinia + shadcn-vue/Tailwind v4/Reka UI, @ambj-consulting/vue-core |
La langue du code est l'anglais partout (classes, méthodes, routes, logs) ; les documents de spécification et de planification restent en français, langue métier du produit tunisien.
Architecture Clean / CQRS
Domain ← Application ← Infrastructure ← API
↑
(interfaces)
- Domain ne dépend d'aucun package externe (0 NuGet). Entités avec constructeur privé, factory method statique, mutations via méthodes-verbes métier, jamais de setters publics.
- Application contient les Commands/Queries MediatR (
VerbNomCommand,GetNomQuery,ListNomQuery) et leurs Validators FluentValidation — aucune Command sans validator. - Infrastructure implémente les interfaces définies dans Application (EF Core, BCrypt, Npgsql).
- API expose les Controllers, qui héritent de
BaseControlleret n'accèdent jamais directement aux services.
Routes : /api/v1/{resource} (métier, filtré par tenant), /api/v1/admin/{resource}
(Super-Admin uniquement), /hubs/{nom} (SignalR).
Multi-tenant — le cœur du système
Base de données par tenant
schoolboost_admin → Tenants, Users, Licences (AdminDb)
schoolboost_tenant_{slug} → Données métier (TenantDb, une base par tenant)
Résolution du tenant (app.bjschool.tn / slug.bjschool.tn)
Le tenant est résolu par le TenantResolutionMiddleware selon une priorité stricte — jamais
depuis un paramètre de requête HTTP ou un header client arbitraire :
- Claim JWT
tenant_slug(prioritaire) — porté par le cookie de session ; fonctionne même si un client utilise un domaine personnalisé. - Sous-domaine du hostname (repli) — pour les routes sans JWT ;
TenantSlugResolverrejette les IP,localhost, le préfixeapi, et exige au moins 3 composants (donccentre-elamal.bjschool.tnest valide,bjschool.tnseul ne l'est pas).
Ce mécanisme est ce qui permet à app.bjschool.tn de servir de portail d'accueil pendant que
chaque centre a son propre espace isolé sur {slug}.bjschool.tn.
this.TenantId), jamais une constante capturée à la construction du modèle — sinon EF
Core fige le tenant du premier accès dans le modèle compilé (mis en cache par type de
contexte), et toute requête suivante avec les mêmes DbContextOptions continue de
filtrer avec le mauvais tenant. C'est une fuite cross-tenant critique, couverte par un test de
régression dédié.
IgnoreQueryFilters() est interdit en dehors du contexte Super-Admin explicitement justifié.
Sécurité
Règles non négociables (bloquantes avant tout merge) :
| # | Règle |
|---|---|
| SEC-01 | Tokens JWT en cookies HttpOnly; Secure; SameSite=Strict — jamais localStorage |
| SEC-02 | Rate limiting sur les endpoints d'auth : 5 requêtes / 15 min / IP en production |
| SEC-03 | Rotation atomique du refresh token après chaque usage |
| SEC-04 | Filtre EF Core global TenantId sur toutes les entités ITenantEntity |
| SEC-05 | Validation FluentValidation sur toutes les Commands |
| SEC-06 | Mots de passe hachés BCrypt, cost ≥ 12 |
| SEC-07 | Secrets (.env, appsettings.Local.json) toujours hors git |
| SEC-08 | Garde applicative sur SaveChangesAsync : toute écriture avec un TenantId différent du |
| contexte lève une exception d'isolation → 403 neutre + log critique | |
| SEC-09 | Rôle PostgreSQL dédié par tenant, identifiants chiffrés AES-GCM |
Durées de vie des tokens : access token 15 minutes, refresh token 7 jours (révocable par jti).
Tenant.Status,
SuspendTenantCommand...). Un tenant suspendu ne se voit donc pas encore bloqué
techniquement. Acceptable tant qu'aucun tenant payant réel n'existe ; à corriger avant le premier
client facturé. Voir aussi la checklist du guide administrateur.
Autres points de vigilance avant une mise en production commerciale : headers de sécurité HTTP (CSP, HSTS, X-Frame-Options) côté Caddy, hardening SSH/UFW/fail2ban de la VM, et décision sur le câblage réel de Redis comme backplane SignalR.
Conventions de code
- Nommage : Command =
VerbNomCommand, Query liste =ListNomQuery, Query détail =GetNomQuery, DTO =NomDto, Handler ={Command}Handler. - Tables PostgreSQL :
snake_casepluriel (clients,lignes_commande,grilles_tarifaires), index explicites sur toutes les FK et colonnes filtrées fréquemment. - EF Core : toujours des migrations, jamais
EnsureCreated()en production ;SaveChangesAsync()uniquement viaIUnitOfWork. - SignalR : un Hub authentifié rejoint un groupe dérivé d'un claim JWT (jamais d'un paramètre client) ; le Hub du portail public est une exception assumée et documentée (aucune donnée personnelle n'y transite).
- Composants Vue : les composants shadcn-vue générés dans
src/components/ui/ne se modifient pas à la main — on les régénère vianpx shadcn-vue@latest add <nom> --overwrite. Les tokens de marque vivent uniquement danssrc/styles/theme.css, source de vérité unique des couleurs. - i18n : toutes les chaînes visibles passent par les fichiers
src/locales/fr.jsonetsrc/locales/ar.json(jamais de texte en dur dans un composant) ; l'arabe est en RTL complet.
Tests
- xUnit + Moq (
MockBehavior.Strict, aucun mock permissif) + FluentAssertions (rester en 7.x — la v8 est passée en licence commerciale) + Testcontainers pour les tests d'intégration avec une vraie base PostgreSQL éphémère. - Chaque cas d'usage critique métier a un test
UC_{CODE}_{Titre}_{Condition}dansSchoolBoost.Domain.Tests/Scenarios/. - Un test IDOR est obligatoire avant toute mise en production :
GET /api/v1/{ressource}/{id-tenant-A}avec un jeton du tenant B doit renvoyer404, jamais200. - Frontend : Vitest (unitaire) + Playwright (bout-en-bout,
playwright.config.ts).
Environnement de développement local
# Backend
cd SchoolBoost-api
dotnet restore
docker compose -f docker-compose.dev.yml up -d # PostgreSQL + Redis locaux
dotnet run --project src/SchoolBoost.API
# Frontend
cd SchoolBoost-front
npm install
npm run dev # serveur de développement Vite
npm run build # vue-tsc -b && vite build
npm run test # Vitest
En local, l'API applique la politique CORS Dev (origine http://localhost:5173) ; en
production, uniquement https://app.bjschool.tn (ou le domaine du tenant concerné). Les cookies
de session sont émis avec Secure = !Host.Equals("localhost").
Packages internes AMBJ Core
<PackageReference Include="AMBJ.Core.Domain" Version="3.1.0" />
<PackageReference Include="AMBJ.Core.Application" Version="3.1.0" />
<PackageReference Include="AMBJ.Core.Infrastructure" Version="3.1.0" />
Source : git@github.com:ambj-consulting/ambj-consulting-core.git, distribué également via
GitHub Packages (https://nuget.pkg.github.com/ambj-consulting/index.json, authentification par
PAT scope SchoolBoost-api/SchoolBoost-front).
MediatR reste sur la branche 12.x (licence commerciale depuis la v13) et
FluentAssertions sur la branche 7.x maximum (licence commerciale depuis la v8) — ne
pas monter au-delà sans validation explicite du coût de licence avec l'utilisateur.
Ce dépôt (bjschool-site)
Contrairement à SchoolBoost-front, ce site vitrine est volontairement simple : pages HTML
statiques, CSS écrit à la main (pas de pipeline Tailwind — l'accès au registre npm était
indisponible dans l'environnement où ce site a été généré ; à réévaluer si l'équipe souhaite
adopter Tailwind comme sur bjmarket-site). Aucune étape de build n'est nécessaire : les fichiers
sont servis tels quels par le Caddy de la VM Dataxion, sur le même modèle que bjmarket-site. Le
contenu des manuels (manuel/*.html) est généré depuis des sources Markdown (build_docs.py et
content_*.py) — modifiez le Markdown, pas le HTML généré.