Aller au contenu

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-verify est couvert ici.

Ce que fait l'application

Un visiteur arrive avec une URL de la forme :

https://diplome.epitech.africa/EPA-[Pays]-[Code diplôme]-[Année]-[Numéro]
  • 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 — voir getReferenceFromPath()), la valide côté client, puis appelle l'API EPIDOCS pour afficher le résultat.

Règle de format du numéro matricule

EPA-[Pays]-[Code diplôme]-[Année]-[Numéro séquentiel]

Regex appliquée côté client (src/App.tsx, DIPLOMA_REFERENCE_PATTERN) :

^EPA-[A-Z]{2}-[A-Z0-9]{2,12}-\d{4}-\d{4}$

(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 :

GET <VITE_API_BASE_URL>/api/diplomas/verify/<ref>/

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 :

  1. GET /api/diplomas/verify/<ref>/ ne renvoie plus que des informations non identifiantes : status, issuer, year, specialty (fonction getDiplomaStatus).
  2. 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 } (fonction unlockDiplomaDetails).
  3. verificationWebsite est 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.
  4. Un 429 sur 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 :

ARG VITE_API_BASE_URL=""
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
...
RUN pnpm build

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) copie dist/ dans /usr/share/nginx/html et nginx.conf dans /etc/nginx/conf.d/default.conf. EXPOSE 80.
  • docker-compose.yml (dev/local, build depuis les sources) et docker-compose.prod.yml (prod, tire l'image ${IMAGE} depuis GHCR) exposent tous deux le port 80 du conteneur sur ${APP_BIND:-127.0.0.1}:${APP_PORT:-7015}.
  • Conteneur prod nommé public_diploma_verify (container_name dans docker-compose.prod.yml).
  • Pas de healthcheck dé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-Policy restrictive.
  • Assets statiques (css|js|mjs|ico|png|jpg|jpeg|gif|svg|webp|woff2?) : cache 30d, 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 vers main, dispatch manuel. Seuls build-docker et deploy sont conditionnés à github.ref == 'refs/heads/main'.
  • Image poussée sur ghcr.io/epitechafrik/public-diploma-verify avec deux tags : main-<sha> et latest.
  • Le job deploy écrit un .env sur le VPS (/root/projects/public-diploma-verify/.env) contenant IMAGE, APP_BIND, APP_PORT — pas VITE_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.