Registre public de diplômes — Vue d'ensemble¶
Description¶
public-diploma-verify est un petit frontend Vite + React + TypeScript, sans backend propre, qui permet à n'importe qui de vérifier publiquement un diplôme EPITECH Afrique à partir de son numéro matricule. C'est volontairement un projet minimal : une seule page React (src/App.tsx), pas de routeur, pas de gestion d'état externe, pas d'authentification.
- Repository : EpitechAfrik/public-diploma-verify (privé)
- URL production : https://diplome.epitech.africa
- Registry :
ghcr.io/epitechafrik/public-diploma-verify - Backend : aucun backend propre — le frontend appelle l'API EPIDOCS (voir epidocs-api/overview.md), qui porte la logique de vérification et les données de diplômes. Ce document ne duplique pas cette page — seul ce qui concerne l'intégration côté
public-diploma-verifyest couvert ici.
Ce que fait l'application¶
Un visiteur arrive avec une URL de la forme :
- Si le chemin est vide (
/), l'app affiche un formulaire de recherche (SearchPanel,src/App.tsx) : l'utilisateur saisit le numéro matricule, le bouton « Vérifier » ne s'active que si le format est respecté, puis redirige vers/<matricule>. - Si le chemin contient un matricule, l'app extrait la référence depuis
window.location.pathname(pas de librairie de routing — voirgetReferenceFromPath()), la valide côté client, puis appelle l'API EPIDOCS pour afficher le résultat.
Règle de format du numéro matricule¶
Regex appliquée côté client (src/App.tsx, DIPLOMA_REFERENCE_PATTERN) :
(insensible à la casse — le flag i est présent). Si le format ne correspond pas, l'application affiche une erreur de format sans jamais appeler l'API — c'est une validation purement front, avant tout appel réseau.
Cette règle n'est plus appliquée par nginx
À l'origine, nginx.conf avait une deuxième location avec la même regex pour ne servir que / et les chemins /EPA-.../ (toute autre route → 404). Cette règle est commentée depuis les commits fix(nginx) : comment rule for validation (4714c77, 8d9c9cf) et location / sert désormais try_files $uri /index.html pour tout chemin (comportement SPA standard). Concrètement : nginx ne filtre plus les URLs par format, seule la validation React côté client (ci-dessus) protège l'affichage — n'importe quel chemin renvoie 200 avec l'app React, qui décide ensuite quoi afficher. Voir troubleshooting.md pour le détail.
Intégration avec l'API EPIDOCS¶
Le frontend appelle :
où <ref> est le matricule, encodé (encodeURIComponent). Codes de réponse gérés explicitement côté frontend (src/App.tsx) :
| Code | Traitement frontend |
|---|---|
404 |
« Diplôme introuvable ou informations incorrectes » |
429 |
« Trop de tentatives. Veuillez réessayer plus tard. » |
autre !ok |
Message générique « le service de vérification ne répond pas » |
200 |
Rendu des champs du diplôme |
Endpoint non documenté côté EPIDOCS API
Au moment de la rédaction, les pages epidocs-api ne référencent pas /api/diplomas/verify/<ref>/ dans leur table des routes (Authentification, Documents, Étudiants, Administration, Réclamations, Zeno, Présence) — c'est un gap de documentation côté epidocs-api, pas une erreur ici. Si vous documentez ce endpoint côté epidocs-api, pensez à faire un lien croisé depuis cette page.
Flux de vérification en deux temps (depuis fix: secure public diploma verification flow)¶
Historiquement (commit initial 65916d3), un seul GET renvoyait directement toutes les informations d'identité (civilité, nom, prénom, date/ville/pays de naissance) pour n'importe quel matricule syntaxiquement valide — donc scriptable/énumérable sans jamais avoir le diplôme physique en main, le format du matricule étant très structuré (année + numéro séquentiel à 4 chiffres).
Le commit a71e396 (« fix: secure public diploma verification flow ») a introduit un flux en deux étapes :
GET /api/diplomas/verify/<ref>/ne renvoie plus que des informations non identifiantes :status,issuer,year,specialty(fonctiongetDiplomaStatus).- Pour voir les informations d'identité, l'utilisateur doit soumettre un formulaire (« Afficher les informations personnelles ») avec le nom et prénoms tels qu'imprimés sur le diplôme —
POST /api/diplomas/verify/(référence dans le corps, pas dans l'URL cette fois) avec{ reference, fullName, verificationWebsite }(fonctionunlockDiplomaDetails). verificationWebsiteest un champ honeypot : un<input>cosmétiquement masqué (.verification-website { display: none }), jamais rempli par un humain, qui sert à piéger les bots de scraping qui remplissent tous les champs d'un formulaire.- Un
429sur cet endpoint est traité comme un rate-limit ; toute autre erreur affiche le message renvoyé par l'API ({ error: "..." }) ou un message générique.
sequenceDiagram
participant User as Visiteur
participant FE as Frontend (diplome.epitech.africa)
participant API as API EPIDOCS
User->>FE: Ouvre /EPA-BJ-MSC-2025-0001
FE->>FE: Valide le format (regex, côté client)
FE->>API: GET /api/diplomas/verify/EPA-BJ-MSC-2025-0001/
API-->>FE: { status, issuer, year, specialty } — sans identité
FE->>User: Affiche le statut, demande "Nom et prénoms"
User->>FE: Saisit le nom imprimé sur le diplôme
FE->>API: POST /api/diplomas/verify/ { reference, fullName, verificationWebsite }
API-->>FE: { civility, lastName, firstName, ..., mention }
FE->>User: Affiche les informations complètes
Ce flux en deux temps est la version voulue — confirmé le 2026-09-11
Une tentative locale non commitée de revenir à un flux en une seule étape a existé brièvement (retour à un unique GET affichant tout directement) puis a été écartée : le flux sécurisé en deux temps ci-dessus reste la version correcte, à ne pas simplifier sans revalider le risque de scraping qu'il limite. Détail : troubleshooting.md — Le flux en deux temps est la version voulue.
Données affichées¶
D'après le README.md du projet, le rendu public est volontairement limité à : civility, lastName, firstName, birthDate, birthCity, birthCountry, specialty, reference — les champs sensibles email/phone ne sont ni lus ni affichés par le frontend.
Architecture¶
graph TB
User[Visiteur / Navigateur]
NginxProxy["Nginx (reverse proxy partagé)<br/>TLS + diplome.epitech.africa"]
AppNginx["nginx:1.27-alpine<br/>sert le build Vite statique"]
API["API EPIDOCS<br/>(projet séparé)"]
User --> NginxProxy
NginxProxy -->|127.0.0.1:APP_PORT| AppNginx
AppNginx -.->|appel navigateur, pas serveur→serveur| API
Le frontend est un site statique : AppNginx ne fait aucun appel réseau lui-même. C'est le navigateur du visiteur qui appelle directement l'API EPIDOCS (fetch côté client), en utilisant l'URL bakée dans le bundle JS au build (VITE_API_BASE_URL).
Stack technique¶
| Composant | Technologie | Version |
|---|---|---|
| Framework | Vite | 6.4.3 |
| UI | React | 19.2.7 |
| Langage | TypeScript | 5.9.3 |
| Gestionnaire de paquets | pnpm | 9.15.4 |
| Serveur statique | Nginx | 1.27-alpine |
| Image de build | node:22-alpine | — |
Pas de routeur (react-router ou équivalent), pas de state manager, pas de librairie HTTP (juste fetch) — le projet entier tient dans src/App.tsx, src/main.tsx, src/types.ts, src/styles.css.
Gotcha critique : variable injectée au build, pas au runtime¶
VITE_API_BASE_URL est lue par Vite via import.meta.env.VITE_API_BASE_URL — Vite remplace cette expression par la valeur littérale au moment du vite build. Ce n'est donc pas une variable d'environnement lue au démarrage du conteneur (contrairement à une appli Node/Next.js classique).
Dans Dockerfile :
L'ARG/ENV n'existe que pendant l'étape build (image node:22-alpine) — la valeur est gravée dans les fichiers JS produits dans dist/, puis copiés dans l'image finale nginx:1.27-alpine. Le conteneur final ne contient aucune trace de cette variable en tant que variable d'environnement runtime (docker exec ... env ne la montrera pas).
Conséquence directe : changer l'URL du backend (ex. migration de l'API EPIDOCS vers un nouveau domaine) nécessite un rebuild complet de l'image, jamais un simple restart du conteneur ni une modification du .env du VPS. Le .env généré par la CI (APP_BIND, APP_PORT, IMAGE) ne contient d'ailleurs même pas VITE_API_BASE_URL — cette variable n'existe que côté secret GitHub Actions, consommée uniquement par le job build-docker (build-args: VITE_API_BASE_URL=${{ secrets.VITE_API_BASE_URL }}).
Voir diagnostics.md pour vérifier quelle URL est effectivement bakée dans une image donnée.
Déploiement¶
Docker¶
Dockerfile: build multi-stage. Stage 1 (node:22-alpine,corepack enable,pnpm install --frozen-lockfile,pnpm build) → stage 2 (nginx:1.27-alpine) copiedist/dans/usr/share/nginx/htmletnginx.confdans/etc/nginx/conf.d/default.conf.EXPOSE 80.docker-compose.yml(dev/local, build depuis les sources) etdocker-compose.prod.yml(prod, tire l'image${IMAGE}depuis GHCR) exposent tous deux le port80du conteneur sur${APP_BIND:-127.0.0.1}:${APP_PORT:-7015}.- Conteneur prod nommé
public_diploma_verify(container_namedansdocker-compose.prod.yml). - Pas de
healthcheckdéclaré dans aucun des deux fichiers Compose (contrairement à EPIDOCS) — voir diagnostics.md.
Nginx (nginx.conf)¶
- Headers de sécurité systématiques :
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,Permissions-Policyrestrictive. - Assets statiques (
css|js|mjs|ico|png|jpg|jpeg|gif|svg|webp|woff2?) : cache30d,immutable. - Fichiers cachés (
/\.sauf.well-known) :404. location /:try_files $uri /index.html— SPA fallback pour toute route (voir l'avertissement plus haut sur la règle de validation désormais commentée).
Pipeline CI/CD (.github/workflows/ci-cd.yml)¶
graph LR
CI["ci: install (frozen-lockfile)<br/>pnpm audit<br/>pnpm build"] --> Docker["build-docker:<br/>buildx + push GHCR<br/>build-arg VITE_API_BASE_URL"]
Docker --> Deploy["deploy:<br/>scp compose file<br/>ssh: pull + up -d"]
Deploy --> Health["Health check<br/>curl 127.0.0.1:APP_PORT"]
- Déclencheurs : push sur
main, PR versmain, dispatch manuel. Seulsbuild-dockeretdeploysont conditionnés àgithub.ref == 'refs/heads/main'. - Image poussée sur
ghcr.io/epitechafrik/public-diploma-verifyavec deux tags :main-<sha>etlatest. - Le job
deployécrit un.envsur le VPS (/root/projects/public-diploma-verify/.env) contenantIMAGE,APP_BIND,APP_PORT— pasVITE_API_BASE_URL(déjà bakée dans l'image à ce stade). - Secrets GitHub Actions nécessaires (vérifiés via
gh api, 6 au total) :APP_BIND,APP_PORT,SSH_PRIVATE_KEY,VITE_API_BASE_URL,VPS_IP,VPS_USER.
Détail des incidents réels sur ce pipeline (gestion d'image GHCR, pnpm-workspace.yaml) : troubleshooting.md.