Alert-Parent Backend¶
Description¶
Alert-Parent est une plateforme interne qui permet aux équipes pédagogiques d'EPITECH Bénin d'envoyer des alertes aux parents et référents financiers des étudiants concernant leurs résultats académiques.
- Repository : EpitechAfrik/alert-parent-backend
- URL production : https://api.epitools.bj
- Port :
127.0.0.1:5011(hôte) →5010(conteneur)
Fonctionnalités principales¶
- Gestion des promos : création de promotions par cycle (Bachelor, MSc, Codac) et année de fin
- Gestion multi-campus : création de campus, promos par campus, étudiants rattachés à un campus
- Gestion des étudiants : création unitaire, import CSV en masse, changement de campus/classe/promo
- Alertes pédagogiques : Tepitech, Stumpers, Modules, Présences, Projets, Roadblocks, -42, Évaluation de stage, Absence, Comportement, Information générale
- Templates d'alertes dynamiques : templates globaux, surcharges par campus, corps HTML WYSIWYG, variables et fichiers attendus
- Campagnes d'alertes : preview éphémère, brouillon, envoi, historique, surcharges par campagne ou par étudiant
- File d'attente email : les alertes sont mises en queue dans MongoDB et envoyées progressivement via l'API Resend
- Authentification : JWT classique + Microsoft Azure AD (SSO)
Architecture¶
Stack technique¶
| Composant | Technologie |
|---|---|
| Framework | Express.js (Node.js 20) |
| Base de données | MongoDB Atlas |
| Email transactionnel | Resend API |
| Authentification | JWT + Microsoft Azure AD |
| Conteneur | Docker (node:20-alpine, pnpm) |
Routes API¶
| Route | Description |
|---|---|
/api/users |
Inscription, login, gestion des comptes admin |
/api/students |
CRUD étudiants, import CSV, bulk update, soft delete |
/api/promos |
CRUD promos, mapping classe/promo |
/api/alerts |
Lecture seule (historique). Les POST d'envoi par type sont désactivés (HTTP 410) → voir /api/alert-campaigns |
/api/campuses |
CRUD campus |
/api/alert-templates |
Templates d'alertes dynamiques (+ field-options par classe) |
/api/alert-campaigns |
Campagnes d'alertes, preview, brouillons, envoi, mode libre, variables |
/api/docs |
Documentation Swagger interactive |
/health |
Endpoint de vérification de santé |
Modèle de données¶
Student
├── firstname, lastname, email, sexe
├── campus (ObjectId → Campus)
├── promo (ObjectId → Promo, populated: {cycle, end_year, campus})
├── classe (TEK1, TEK1 PSO, TEK2, TEK3, PreMSC, MSC1, MSC2, CODAC)
├── active (Boolean, default: true) — soft delete
├── ref_financier_1_* (prénom, nom, email, sexe)
└── ref_financier_2_* (optionnel)
Campus
├── name, city, country, address
├── active (Boolean)
└── createdAt, updatedAt
Promo
├── campus (ObjectId → Campus)
├── cycle (BACHELOR, MSC, CODAC)
└── end_year (ex: 2027)
AlertTemplate
├── key, name, topic, campus (optionnel), baseTemplate (optionnel)
├── ruleKey (règle métier: tepitech, module, project, stumper)
├── subjectTemplate, bodyTemplate (HTML WYSIWYG)
├── variables, variablesSchema, fileSchemas
├── source (system/user), active
└── createdBy, updatedBy
AlertCampaign
├── name, kind (template | free), topic, campus, promo, classe
├── template (null en mode free)
├── freeSubject, freeBody, extraRecipients (mode free)
├── students, globalVariables, files, rows
├── overrideSubjectTemplate, overrideBodyTemplate, overrideVariables
├── studentOverrides, renderedPreviews, fileWarnings, fileErrors
├── mails, status (draft, previewed, queued, sent, partial_failed)
└── createdBy, sentBy, sentAt
MailSended
├── from (user qui a envoyé), to (email destinataire)
├── student (ObjectId → Student, null pour un destinataire libre), campus, template, campaign
├── topic (Tepitech, Module, Présence, etc.)
├── obj (sujet), message (HTML)
├── isSent (yes/no)
└── createdAt, updatedAt
Routes étudiants (détail)¶
| Action | Méthode | Route | Body |
|---|---|---|---|
| Lister (actifs par défaut) | GET | /api/students?active=true |
— |
| Lister les inactifs | GET | /api/students?active=false |
— |
| Créer | POST | /api/students |
{firstname, lastname, email, ...} |
| Modifier | PUT | /api/students/:id |
{firstname, lastname, ...} |
| Import CSV | POST | /api/students/import |
FormData |
| Bulk update (classe/promo) | PUT | /api/students/bulk |
{students: [ids], classe, promo} |
| Désactiver | PUT | /api/students/:id/deactivate |
— |
| Réactiver | PUT | /api/students/:id/reactivate |
— |
| Désactiver en masse | POST | /api/students/bulk/deactivate |
{students: [ids]} |
Soft delete
Les étudiants ne sont jamais supprimés définitivement. La route DELETE /api/students/:id n'existe plus.
La désactivation conserve l'historique des alertes associées.
Par défaut, GET /api/students ne retourne que les étudiants actifs (active=true).
Promo peuplée
Le champ promo est désormais peuplé automatiquement par l'API. Au lieu d'un simple ObjectId, il retourne {cycle: "BACHELOR", end_year: 2030}.
Campus, templates et campagnes d'alertes¶
Campus¶
Les étudiants et promos sont rattachés à un campus. Les anciennes données sans campus sont rattachées automatiquement au campus par défaut Cotonou au démarrage.
| Action | Méthode | Route |
|---|---|---|
| Lister | GET | /api/campuses |
| Créer | POST | /api/campuses |
| Modifier | PUT | /api/campuses/:id |
| Supprimer | DELETE | /api/campuses/:id |
Une promo est unique par couple campus + cycle + end_year. Deux campus peuvent donc avoir la même promo en parallèle.
Templates dynamiques¶
Les templates ne sont plus figés dans le code. Ils sont stockés en base et peuvent être globaux ou spécifiques à un campus.
| Action | Méthode | Route |
|---|---|---|
| Lister | GET | /api/alert-templates |
| Résoudre le template effectif | GET | /api/alert-templates/resolve?topic=Module&campus=ID |
| Options de champ par classe (selects bornés) | GET | /api/alert-templates/:id/field-options?classe=TEK2 |
| Créer | POST | /api/alert-templates |
| Modifier | PUT | /api/alert-templates/:id |
| Désactiver | DELETE | /api/alert-templates/:id |
Champs importants :
subjectTemplate: sujet texte avec variables{{...}}bodyTemplate: HTML WYSIWYGvariablesSchema: champs à afficher côté frontendfileSchemas: fichiers attendus pour les campagnescampus: vide pour template global, renseigné pour surcharge campus
Variables système générées par le backend :
Les variables computed.* sont calculées au rendu, par exemple computed.earnedCredits, computed.moduleResultSummary, computed.projectResultSummary.
Campagnes d'alertes¶
Le workflow recommandé est :
- Preview éphémère sans sauvegarde.
- Sauvegarde brouillon si l'utilisateur clique sur
Save draft. - Envoi en deux appels
create/update -> send. - Historique uniquement pour les campagnes envoyées ou en file d'attente.
| Action | Méthode | Route |
|---|---|---|
| Preview sans persistance | POST | /api/alert-campaigns/preview-draft |
| Créer un brouillon | POST | /api/alert-campaigns |
| Lister les brouillons | GET | /api/alert-campaigns?scope=drafts |
| Modifier un brouillon | PUT | /api/alert-campaigns/:id |
| Supprimer un brouillon | DELETE | /api/alert-campaigns/:id |
| Preview d'un brouillon sauvegardé | POST | /api/alert-campaigns/:id/preview |
| Envoyer | POST | /api/alert-campaigns/:id/send |
| Historique | GET | /api/alert-campaigns?scope=history |
DELETE /api/alert-campaigns/:id est autorisé uniquement pour les brouillons draft ou anciennes previews previewed sans mail envoyé/queue.
send crée un MailSended par parent renseigné. Les deux parents reçoivent l'alerte si les deux emails existent.
Routes legacy /api/alerts/* désactivées (HTTP 410)
Depuis la refonte de juin 2026, les anciennes routes d'envoi par type
(POST /api/alerts/tepitech, /stumpers, /modules, /roadblocks, /cheaters,
/eval-internship, /presences, /projects, /resend/:id) renvoient 410 Gone.
Tout passe désormais par /api/alert-campaigns. Seules les lectures
GET /api/alerts et GET /api/alerts/:id restent actives.
Moteur de règles métier et validation¶
La logique métier propre à chaque type d'alerte est centralisée dans un registre de règles
(utils/rules/), keyé par le ruleKey du template (fallback sur le topic) :
| ruleKey | Règles |
|---|---|
tepitech |
seuils par classe (TEK1/PSO 600, TEK2 700, TEK3 750) ; sessions par classe (TEK1/PSO 6, TEK2/TEK3 4) ; pass/fail |
stumper |
sessions par topic (solo 10, duo 7) ; seuil de validation 15 ; pass/fail |
module |
grade, crédits acquis, résumés de validation |
project |
grade prévisionnel, trajectoire de validation |
Un validateur unifié (campaignValidator.js) est appliqué au preview et à l'envoi.
Il renvoie { errors[], warnings[] } et couvre 3 couches :
- Champs requis déclarés dans
variablesSchema(lerequiredest désormais réellement appliqué). - Fichiers : colonnes, login, croisements (
campaignFiles.js). - Règle métier : ex. session hors plage pour la classe → erreur
invalid_session.
S'il y a des errors, la requête renvoie 400 et l'envoi est bloqué.
Options de champ dynamiques¶
GET /api/alert-templates/:id/field-options?classe=<classe> renvoie le variablesSchema du
template avec les options résolues selon la classe et le topic — pour que le frontend
affiche un <select> borné aux valeurs valides (sessions Tepitech 6 en TEK1 / 4 en TEK2,
solo 10, duo 7) plutôt qu'une saisie libre rejetée ensuite.
Mode libre (composer sans template)¶
Une campagne porte un champ kind :
template(défaut) : construite depuis unAlertTemplate.free: composer ad hoc —freeSubject+freeBody(WYSIWYG) avec variables{{...}}, destinataires = étudiants ciblés et/ouextraRecipients(emails libres).
| Action | Méthode | Route |
|---|---|---|
| Catalogue des variables DB + snippets (salutation/signature) | GET | /api/alert-campaigns/variables |
| Enregistrer une campagne libre comme template | POST | /api/alert-campaigns/:id/save-as-template |
En mode libre, template vaut null et les MailSended des destinataires libres ont student: null.
Civilité et rendu¶
- Le destinataire est adressé par civilité :
recipient.salutation= « Mr <Nom> » / « Mme <Nom> » (repli « Madame, Monsieur »). - Les mails sont rendus à la charte EPITECH Africa (100% CSS, sans image) : wordmark
{EPITECH} AFRICA, eyebrow + titre Anton, footer terminal. Voirutils/mailBodies.jsetutils/mailPresentation.js.
File d'attente email (Resend)¶
Un processus asynchrone tourne en boucle dans server.js :
- Toutes les 5 secondes, il cherche un
MailSendedavecisSent: "no" - Il l'envoie via l'API Resend
- En cas de succès, il marque
isSent: "yes" - En cas d'erreur, il attend 60 secondes avant de réessayer
Ce mécanisme découple la création d'alertes de leur envoi effectif.
Collections MongoDB¶
| Collection | Rôle |
|---|---|
students |
Étudiants avec leurs référents financiers |
promos |
Promotions par cycle et année |
mailsendeds |
File d'attente des mails à envoyer |
users |
Comptes administrateurs |
emailconfirms |
Tokens de confirmation email |
passwordresets |
Tokens de reset mot de passe |
Diagrammes¶
Architecture globale¶
graph TB
User[Utilisateur / Navigateur]
Frontend["Frontend<br/>alert.epitools.bj"]
Nginx["Nginx<br/>Reverse proxy + TLS"]
Backend["Backend API<br/>Express.js :5010"]
MongoDB["MongoDB Atlas<br/>(Cloud)"]
Resend["Resend API<br/>(Email)"]
AzureAD["Azure AD<br/>(SSO)"]
User --> Nginx
Nginx --> Frontend
Nginx --> Backend
Backend --> MongoDB
Backend --> Resend
Backend --> AzureAD
Flux d'envoi d'alerte¶
sequenceDiagram
participant Admin as Admin (Frontend)
participant API as Backend API
participant DB as MongoDB Atlas
participant Queue as Mail Queue (loop)
participant Resend as Resend API
participant Parent as Parent (Email)
Admin->>API: POST /api/alert-campaigns (brouillon)
Admin->>API: POST /api/alert-campaigns/:id/preview
API->>API: validateCampaign (champs, fichiers, règles)
API-->>Admin: {previews, warnings, errors}
Admin->>API: POST /api/alert-campaigns/:id/send
API->>DB: MailSended.insertMany({isSent: "no"})
API-->>Admin: {campaign (queued), queuedMails}
loop Toutes les 5 secondes
Queue->>DB: MailSended.find({isSent: "no"})
Queue->>Resend: resend.emails.send(...)
Resend->>Parent: Email envoyé
Queue->>DB: MailSended.update({isSent: "yes"})
end
Docker¶
Dockerfile¶
L'image utilise node:20-alpine avec pnpm comme gestionnaire de paquets. L'application tourne en tant qu'utilisateur non-root pour la sécurité.
docker-compose.yml¶
services:
backend:
image: ghcr.io/epitechafrik/alert-parent-backend:${IMAGE_TAG:-latest}
env_file: .env
ports:
- "127.0.0.1:5011:5010"
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5010/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Points importants :
- L'image est tirée depuis GHCR (
ghcr.io/epitechafrik/alert-parent-backend) - Le port est bindé sur
127.0.0.1uniquement (accessible seulement via Nginx) - Les variables d'environnement sont chargées depuis
.envviaenv_file - Le healthcheck vérifie
/healthsur le port interne5010
Health Endpoint¶
GET /health¶
Utilisé par Docker Compose (healthcheck), Uptime Kuma (monitoring) et le pipeline CI/CD (vérification post-déploiement).
Réponse (200 OK)¶
{
"status": "ok",
"timestamp": "2026-02-22T10:30:00.000Z",
"uptime_seconds": 86400,
"environment": "production",
"dependencies": {
"mongodb": {
"status": "ok",
"latency_ms": 12
},
"resend": {
"status": "configured"
},
"smtp": {
"status": "configured"
}
}
}
Réponse (503 Service Unavailable)¶
Retourné quand MongoDB est inaccessible :
{
"status": "degraded",
"timestamp": "2026-02-22T10:30:00.000Z",
"uptime_seconds": 120,
"environment": "production",
"dependencies": {
"mongodb": {
"status": "error",
"latency_ms": null
},
"resend": {
"status": "configured"
},
"smtp": {
"status": "configured"
}
}
}
Utilisation¶
# Test simple
curl http://127.0.0.1:5011/health
# Test avec formatage
curl -s http://127.0.0.1:5011/health | python3 -m json.tool
# Vérifier uniquement le status code
curl -o /dev/null -s -w "%{http_code}" http://127.0.0.1:5011/health
# 200 = OK, 503 = dégradé
Variables d'environnement¶
Toutes les variables sont définies dans le fichier .env à la racine du projet (jamais commité dans git). Le template est dans .env.example.
Application¶
| Variable | Description | Exemple |
|---|---|---|
NODE_ENV |
Mode d'exécution | production |
PORT |
Port interne du serveur | 5010 |
Base de données¶
| Variable | Description | Exemple |
|---|---|---|
MONGO_URI |
URI MongoDB Atlas | mongodb+srv://user:pass@cluster.mongodb.net/dbname |
Authentification¶
| Variable | Description | Exemple |
|---|---|---|
JWT_SECRET |
Clé secrète JWT (64 chars min) | chaîne aléatoire longue |
ADMIN_EMAIL |
Email du premier admin créé au démarrage | admin@epitech.eu |
PASSWORD_SIZE |
Taille minimum des mots de passe | 8 |
PASSWORD_CAPITAL |
Exiger une majuscule | true |
PASSWORD_LOWERCASE |
Exiger une minuscule | true |
PASSWORD_NUMBER |
Exiger un chiffre | true |
PASSWORD_CHAR |
Exiger un caractère spécial | true |
Email¶
| Variable | Description | Exemple |
|---|---|---|
MAIL_NAME |
Nom d'expéditeur | EPITECH Info |
MAIL_HOST |
Serveur SMTP | alouette.o2switch.net |
MAIL_PORT |
Port SMTP | 465 |
MAIL_ADDR |
Adresse d'envoi | no-reply@epitech.bj |
MAIL_PASS |
Mot de passe SMTP | secret |
RESEND_TOKEN |
Token API Resend (envoi des alertes) | re_xxxxx |
URLs¶
| Variable | Description | Exemple |
|---|---|---|
CORS_ORIGIN |
Origine autorisée CORS | https://alert.epitools.bj |
FRONT_URL |
URL du frontend | https://alert.epitools.bj |
API_URL |
URL publique de l'API | https://api.epitools.bj |
Microsoft Azure AD (optionnel)¶
| Variable | Description |
|---|---|
MICROSOFT_CLIENT_ID |
Client ID de l'App Registration Azure |
MICROSOFT_TENANT_ID |
Tenant ID Azure |
Docker¶
| Variable | Description | Exemple |
|---|---|---|
IMAGE_TAG |
Tag de l'image Docker à utiliser | latest ou sha-abc1234 |
Rollback¶
Quand faire un rollback¶
- Le déploiement a cassé une fonctionnalité critique
- Le health check échoue après un déploiement
- Une régression est détectée en production
Procédure¶
# 1. Trouver la version précédente
docker images ghcr.io/epitechafrik/alert-parent-backend --format "table {{.Tag}}\t{{.CreatedAt}}"
# 2. Exécuter le rollback
cd /root/projects/alert-parent/alert-parent-backend
./scripts/rollback.sh sha-XXXXXXX
# 3. Vérifier
curl -s http://127.0.0.1:5011/health | python3 -m json.tool
docker compose logs backend --tail 20
Après le rollback¶
- Investiguer la cause du problème sur la branche
main - Corriger et pousser un fix
- Le pipeline redéploiera automatiquement
Backups¶
MongoDB Atlas¶
La base de données est hébergée sur MongoDB Atlas, qui gère automatiquement :
- Snapshots automatiques : toutes les 6 heures
- Rétention : configurable dans Atlas (7 jours par défaut)
- Point-in-time recovery : restauration à n'importe quel moment
Backup manuel¶
mongodump --uri="mongodb+srv://USER:PASSWORD@cluster.mongodb.net/al3rtParent" --out=./dump-$(date +%Y%m%d)