Backend REST d'EventHub. Spécification : ../docs/BACKEND_MANIFEST.md.
api/
├── src/
│ ├── EventHub.Domain/ # Entités + règles métier (aucune dépendance)
│ ├── EventHub.Application/ # CQRS : commandes/requêtes + handlers, DTOs, interfaces (→ Domain)
│ ├── EventHub.Infrastructure/ # EF Core (SQLite), Identity, dépôts, médiateur (→ Application)
│ └── EventHub.Api/ # Contrôleurs, SignalR, DI (→ Application + Infra)
└── tests/
├── EventHub.UnitTests/ # Tests unitaires (domaine, TDD)
└── EventHub.IntegrationTests/# Tests d'intégration (WebApplicationFactory + SQLite in-memory)
Chaque cas d'usage est un message ICommand<T> (écriture) ou IQuery<T>
(lecture) traité par un unique ICommandHandler / IQueryHandler. Les contrôleurs
ne dépendent que de ISender (médiateur maison, Infrastructure/Messaging/Sender.cs),
qui résout le handler par type et lui délègue l'exécution. Les handlers sont
découverts et enregistrés automatiquement par scan de l'assembly Application
(voir AddApplicationHandlers dans DependencyInjection.cs), aucun registre à tenir.
Pas de dépendance externe (ni MediatR) : dispatch par réflexion.
Les entités sont des agrégats riches : setters privés, construction via
fabriques Entity.Create(...) (gardes d'invariants → DomainException), et
mutations par méthodes métier (Registration.Cancel/PromoteFromWaitlist/MarkAttended,
Activity.Update, Post.Hide, Report.Resolve…). Impossible d'instancier un état
invalide.
Le Domaine possède tous les ports (hexagonal) et ne dépend de rien :
Domain/Repositories/, dépôts d'écriture (agrégats) et de lecture (read-repos) ;Domain/ReadModels/, DTOs retournés par les read-repos (ActivityDto,PostDto…) ;Domain/Services/, ports techniquesIClock,ICurrentUser,IRealtimeNotifier,IUserAdminService.
Infrastructure (EF, SystemClock, UserAdminService) et Api (CurrentUser,
SignalRNotifier) n'implémentent que des interfaces du Domaine. Le dispatcher CQRS
(ISender/Sender) est de la plomberie applicative → il vit dans Application. Restent
en Application les résultats assemblés (RegistrationResult, HeartsSummaryDto,
ProfileDto…), qui ne sont pas des read models de dépôt.
L'invariant de capacité est protégé par concurrence optimiste : Activity porte
un jeton Version (IsConcurrencyToken). S'inscrire consomme une place via
Activity.ClaimSpot(...) (nouveau Version) ; deux demandes simultanées sur la
dernière place entrent en conflit sur la même ligne, la perdante reçoit une
ConcurrencyConflictException (traduite depuis EF dans RegistrationRepository),
est rejouée par le handler (jusqu'à 3 fois) et bascule en liste d'attente.
Jamais de sur-réservation. Couvert par RegisterForActivityHandlerTests (rejeu
déterministe : l'in-memory SQLite mono-connexion ne permet pas un vrai test parallèle).
Chaque fonctionnalité (Registrations/, Social/, Moderation/, Users/, …) est
découpée en sous-dossiers un type = un fichier ; le namespace reste au niveau de
la fonctionnalité (ex. EventHub.Application.Social) quel que soit le sous-dossier.
Application/<Feature>/
Commands/ # records ICommand<T>
Queries/ # records IQuery<T>
Handlers/ # ICommandHandler / IQueryHandler
Dtos/ # DTOs, résultats (Result) et enums de retour
Abstractions/ # interfaces de dépôts/services de la fonctionnalité
Application/Common/
Messaging/ # contrats CQRS (ICommand, IQuery, ISender…)
Interfaces/ # abstractions transverses (IClock, ICurrentUser, IRealtimeNotifier)
- .NET 8, ASP.NET Core (contrôleurs) + SignalR (
/hubs/notifications). - EF Core 8 + SQLite (
Microsoft.EntityFrameworkCore.Sqlite), schéma géré par migrations (InitialCreate), appliquées au démarrage (Database.Migrate()). - ASP.NET Core Identity (
IdentityCore+ rôles, cléGuid). - Auth Microsoft Entra ID : JWT Bearer actif dès que la config est fournie
(
Authentication:Authority/Audience, via user-secrets en dev), avec provisioning JIT des utilisateurs et rôles en base. Sans config, repli sur le schéma dev (X-User-Id), utilisé par les tests. - Tests : xUnit, FluentAssertions, Moq,
Microsoft.AspNetCore.Mvc.Testing.
cd api
dotnet build # compiler la solution
dotnet test # tous les tests (unit + intégration)
dotnet run --project src/EventHub.Api # lancer l'API (migrations appliquées au démarrage)
# Migrations EF (schéma dans src/EventHub.Infrastructure/Persistence/Migrations) :
dotnet ef migrations add <Nom> --project src/EventHub.Infrastructure --startup-project src/EventHub.Api --output-dir Persistence/Migrations| Méthode | Route | Description |
|---|---|---|
| GET | /api/activities |
Catalogue des activités publiées (+ currentParticipants) |
| GET | /api/activities/featured |
Activités « à la une » |
| GET | /api/activities/{id} |
Détail d'une activité (404 si absente) |
| POST | /api/activities/{id}/register |
S'inscrire → registered / waitlisted (liste d'attente si complet), idempotent, 409 si échéance passée |
| POST | /api/activities/{id}/cancel |
Annuler → promeut le 1ᵉʳ en liste d'attente, 404 si non inscrit |
| GET | /api/me |
Profil : nom, courriel, avatar, total cœurs, niveau, inscriptions |
| GET | /api/me/hearts |
Résumé cœurs de l'utilisateur : total, niveau (Bronze/Argent/Or), historique |
| POST · PUT | /api/admin/activities · /{id} |
Back office : créer / mettre à jour une activité (statut, « à la une ») |
| POST | /api/admin/activities/{id}/attendance |
Marque des présents → crédite les cœurs (idempotent) |
| GET | /api/posts · /api/posts/{id} |
Fil communautaire / détail (auteur, likes, commentaires) |
| POST | /api/posts |
Publier une photo (imageUrl, caption, activityId?) → 201 |
| POST · DELETE | /api/posts/{id}/like |
Aimer / retirer (idempotent) → { likesCount } |
| POST | /api/posts/{id}/comments |
Commenter (text) |
| POST | /api/reports |
Signaler un post/commentaire (targetType, targetId, reason) → 201, 404 si absent |
| GET | /api/admin/reports |
Back office : file des signalements ouverts (auteur, cible, motif) |
| POST | /api/admin/posts/{id}/hide |
Masquer un post → disparaît du fil + clôt ses signalements |
| POST | /api/admin/comments/{id}/hide |
Masquer un commentaire → disparaît du détail |
| GET | /api/admin/users |
Back office : rechercher des utilisateurs (?q=), rôle, statut, cœurs |
| PATCH | /api/admin/users/{id} |
Changer le rôle (student/organizer/moderator/admin) et/ou le statut |
| POST | /api/admin/users/{id}/hearts |
Ajustement manuel de cœurs (hearts, reason) → { totalHearts } |
| POST | /api/activities/{id}/check-in |
Auto-émargement par QR : jeton secret + inscrit + fenêtre horaire → présence confirmée, cœurs crédités (idempotent) |
| GET | /api/me/registrations |
Mes activités inscrites (avec myStatus : registered/attended/noshow/waitlisted) |
| POST | /api/me/avatar |
Persiste la photo de profil (chemin renvoyé par l'upload) |
| POST | /api/uploads/image |
Upload d'image (multipart) → chemin relatif /uploads/… (jamais de domaine en base) |
| GET | /api/categories · /api/leaderboard · /api/stats/community |
Référentiels, classement et stats publics |
| GET | /api/about/contributors |
Contributeurs de la page « À propos » (tri par ordre) |
| CRUD | /api/admin/contributors |
Back office : gestion des contributeurs |
| CRUD | /api/admin/categories · /api/admin/organizers |
Back office : référentiels |
| GET/PATCH | /api/admin/settings/gamification |
Seuils de niveaux (Argent/Or) |
| POST | /api/admin/notifications/broadcast |
Diffusion d'une notification |
| WS | /hubs/notifications |
Hub SignalR, évènements activityParticipantsChanged, registrationPromoted |
Réponses en camelCase, conformes au contrat figé par l'app mobile (BACKEND_MANIFEST.md §3.0).
Identité :
POST /registerlit l'utilisateur depuis le claim Entraoid; tant que l'auth n'est pas active, repli sur l'en-têteX-User-Id(dev/tests).
- Santé :
GET /health→{ status, version }(sonde + connectivité base). - Erreurs : gestionnaire global → ProblemDetails (RFC 7807), invariants de domaine → 400, conflit de concurrence → 409, reste → 500 (détail interne journalisé, non divulgué).
- CORS : politique
Defaultpilotée parCors:AllowedOrigins(tout autoriser si vide, dev). - Rate limiting : fenêtre fixe par IP (
RateLimiting:PermitPerMinute, défaut 600) → 429. - Observabilité : journalisation des requêtes HTTP (
AddHttpLogging). - Secrets :
appsettings< user-secrets (dev,UserSecretsId) < variables d'environnement ; aucune valeur sensible en dur (connexion, Authority, stockage).
Le contrôle d'accès est en place et testé, avec deux modes :
- Mode dev (par défaut) : schéma
Dev, l'identité vient des en-têtesX-User-Id(id interne) etX-User-Roles(rôles, séparés par des virgules). Permet à[Authorize]de fonctionner en tests/dev. Ne pas utiliser en prod. - Mode Entra : dès que
Authentication:Authorityest renseigné, l'API valide les jetons Entra (JWKS) et un middleware de provisioning JIT mappe le claimoid→ unApplicationUserinterne (EntraObjectId), en le créant au 1ᵉʳ login.CurrentUserrenvoie toujours le Guid interne.
Routes protégées : [Authorize] sur /api/me/* (utilisateur connecté) et
[Authorize(Roles=…)] sur /api/admin/* + /attendance (rôles
organizer/moderator/admin selon la route). Les lectures publiques
(catalogue, catégories, fil, leaderboard, stats, health) restent ouvertes.
Activer l'auth Entra (les identifiants Entra ne sont pas des secrets en Option B) :
cd src/EventHub.Api
dotnet user-secrets set "Authentication:Authority" "https://login.microsoftonline.com/<tenantId>/v2.0"
dotnet user-secrets set "Authentication:Audience" "api://<clientId-API>"
# Restrictions (optionnelles mais recommandées) :
dotnet user-secrets set "Authentication:TenantId" "<tenantId>"
dotnet user-secrets set "Authentication:AllowedEmailDomain" "uqtr.ca"Renseigner Authority bascule automatiquement du mode dev vers Entra (le schéma
Dev et le repli X-User-Id se désactivent). L'app mobile (MSAL/PKCE) demande la
scope api://<clientId-API>/access_as_user et envoie l'access token en
Authorization: Bearer ….
Auth Entra : active en dev (config Authority/Audience/AdminEmails
fournie via user-secrets), validation des jetons + provisioning JIT + rôles DB.
Voir la section Authentification ci-dessus.
Non encore implémenté (hors périmètre de cette itération) :
- Loi 25 :
GET /me/export,DELETE /me(anonymisation), registre de consentement. - Webhook Google Forms (
POST /webhooks/google-forms) pour la réconciliation. - Push réel (FCM/APNs) : l'adaptateur actuel (
LoggingPushSender) est un stub ; le stockage objet (StubStorageService) fabrique des URL sans provisionner S3. - Jobs CRON (clôture des inscriptions à l'échéance, rappels push, recalcul
app_stats).
Le reste du manifeste (catalogue filtré, catégories, profil, inscriptions + statut, leaderboard, stats, back-office activités/référentiels/utilisateurs/modération, dashboard + export CSV, notifications, uploads, réglages gamification) est implémenté et couvert par des tests (unit + intégration).