EPIDOCS API — Backend¶
Description¶
EPIDOCS est une plateforme de gestion documentaire pour EPITECH Bénin. Elle permet aux étudiants de générer leurs documents administratifs (certificats de scolarité, attestations, lettres de recommandation) et aux administrateurs de gérer les étudiants, les templates, les migrations de promotions et la génération en lot.
- Repository : EpitechAfrik/epidocs-api
- URL production : https://api.mydocs.epitools.bj
- Port :
7010(hôte) →8000(conteneur) - Registry :
ghcr.io/epitechafrik/epidocs-api:deploy
Fonctionnalités principales¶
- Authentification Azure AD : SSO Microsoft pour étudiants et admins, sessions JWT en cookie HttpOnly
- Génération de documents bilingues : templates Word (.docx) en FR et EN remplis dynamiquement, convertis en PDF avec QR code de vérification. Chaque document partage un seul tracking code pour ses deux versions.
- Support multi-campus : chaque campus peut avoir ses propres templates de documents (surcharge
CampusDocumentType). Si un campus est associé à l'étudiant, ses templates priment sur les templates globaux. - Génération en lot (batch) : génération de documents pour toute une promotion, export en ZIP
- Gestion des étudiants : import CSV, migration de promotions (PGE1→PGE2, etc.), suspension de comptes
- Demandes de modification : les étudiants peuvent demander la correction de leurs informations personnelles
- Réclamations : système de tickets avec pièces jointes
- Mode maintenance : middleware qui bloque l'accès avec maintenance planifiée
- Annonces : système d'annonces ciblées par classe ou global
- Notes d'information : upload de PDFs (convocations, plannings) ciblés par classe, téléchargeables par les étudiants
- Audit log : traçabilité de toutes les actions admin
- Présence Campus (Attendance) : chaque étudiant reçoit par e-mail un badge QR signé ; un agent de sécurité/chaperon (PIN, sans session admin/étudiant) ou un admin le scanne à l'entrée du campus pour autoriser/refuser l'accès. Historique et statistiques de présence, envoi de badges par email. Mécanique de signature/rotation et détails de rendu du badge dans
apps/attendance/plus bas sur cette page ; routes, modèle de données complet et pièges connus sur sa propre page — voir Présence Campus. - Vérifications d'intégrité des données : registre extensible de checks admin (
/admin/data-checks) pour détecter et corriger des anomalies de données sans script one-off — voir Opérations manuelles - Intégration service-à-service (Zeno) : API dédiée permettant à un service externe (Zeno) de lister, consulter, générer et télécharger les documents des étudiants via une clé API (sans session navigateur). Comptes de service et clés gérés depuis le dashboard. Doc OpenAPI live (ReDoc/Swagger).
- Droits fins par admin : au-delà du simple statut admin/superadmin, 24 droits (
Right) accordables individuellement bornent l'accès aux actions sensibles (suspension, migration, gestion des templates, présence, etc.).
Architecture¶
Stack technique¶
| Composant | Technologie |
|---|---|
| Framework | Django 5 + Django REST Framework |
| Base de données | PostgreSQL 16 |
| Authentification | JWT (cookie HttpOnly) + Azure AD (MSAL) ; clé API pour les services externes |
| Génération de documents | docxtpl + LibreOffice (conversion PDF) |
| QR Codes | qrcode + Pillow |
| Stockage fichiers | Local (media/) ou Cloudinary (remote) |
| Conteneur | Docker (python:3.12 + LibreOffice) |
| WSGI | Gunicorn (3 workers, timeout 120s) |
| API format | CamelCase JSON (djangorestframework-camel-case) |
Routes API¶
Authentification (/auth/, /student/auth/)¶
| Méthode | Route | Description |
|---|---|---|
| GET | /auth/sign_in |
Initie le flow Azure AD |
| GET | /auth/callback |
Callback OAuth Azure AD |
| GET | /auth/me |
Infos de l'admin connecté |
| POST | /auth/sign_out |
Déconnexion |
| GET | /student/auth/me |
Infos de l'étudiant connecté |
Documents (/documents/)¶
| Méthode | Route | Description |
|---|---|---|
| GET | /documents/available-document-types |
Documents disponibles pour une classe |
| POST | /documents/generate-document |
Générer un document (étudiant) — retourne le PDF FR |
| GET | /documents/download-en/:tracking_code |
Télécharger la version EN d'un document existant |
| GET | /documents/history |
Historique des documents générés |
| GET | /documents/tracking/:doc_ref |
Vérification d'un document via QR code |
| POST | /documents/admin/generate-for-student |
Générer un doc pour un étudiant (admin) |
| POST | /documents/admin/batch-generate |
Génération en lot pour une classe (admin) |
| GET | /documents/admin/dashboard/stats/ |
Statistiques du dashboard |
| GET | /documents/admin/student-history/:email/ |
Historique d'un étudiant (admin) |
| DELETE | /documents/admin/delete-document/:tracking_code/ |
Supprimer un document |
| GET/POST | /documents/admin/info-notes |
Lister / créer des notes d'information |
| GET/PUT/DELETE | /documents/admin/info-notes/:id |
Détail / modifier / supprimer une note |
| PATCH | /documents/admin/info-notes/:id/toggle |
Activer / désactiver une note |
| GET | /documents/info-notes/:id/download |
Télécharger le PDF d'une note (étudiant) |
Étudiants (/student/)¶
| Méthode | Route | Description |
|---|---|---|
| POST | /student/upload-csv/ |
Import d'étudiants via CSV |
| POST | /student/upload-tepitech-csv/ |
Import des scores TEPITECH |
| POST | /student/migrate-students/ |
Migration de promotion |
| GET | /student/admin/list/ |
Liste paginée des étudiants (filtres: section, annee, search, login) |
| POST | /student/admin/set-graduation-status/ |
Marquer des étudiants PGE3/PGE5 comme diplômés en masse |
| POST | /student/pending-students |
Ajouter un étudiant en attente |
| POST | /student/validate-students |
Valider les étudiants en attente |
| POST | /student/info-change-request/ |
Demande de modification (étudiant) |
| GET | /student/admin/info-change-requests/ |
Liste des demandes (admin) |
Administration (/admin/)¶
| Méthode | Route | Description |
|---|---|---|
| GET/PUT | /admin/system-status |
État du système / mode maintenance |
| GET/POST | /admin/announcements |
Gestion des annonces |
| GET | /admin/audit-logs |
Journal d'audit |
| GET | /admin/analytics |
Tableau de bord analytique |
| POST | /admin/backup/export |
Export de la base de données |
| POST | /admin/backup/import |
Import de la base de données |
Réclamations (/complaints/)¶
| Méthode | Route | Description |
|---|---|---|
| GET/POST | /complaints/student |
Réclamations de l'étudiant |
| GET/POST | /complaints/admin |
Gestion des réclamations (admin) |
Intégration Zeno — service-à-service (/integrations/v1/)¶
Authentification par clé API : header Authorization: Api-Key <clé> (pas de cookie). Réponses JSON camelCase, rate limit 1000 req/h par clé, accès global (tous étudiants / tous campus).
Scopes (permissions par clé) : chaque compte de service porte des scopes qui bornent ses clés :
| Scope | Autorise |
|---|---|
read |
Lister, détail, available-documents, télécharger |
generate |
Générer un document (POST) |
Une clé lecture seule a ["read"] (la génération renvoie 403) ; une clé complète a ["read", "generate"]. Défaut à la création : accès complet. Un appel hors scope renvoie 403.
| Méthode | Route | Description |
|---|---|---|
| POST | /integrations/v1/documents/generate |
Générer (ou réutiliser) un doc — body { studentLogin, docId }, renvoie trackingCode + URLs de téléchargement |
| GET | /integrations/v1/students/:login/documents |
Lister les documents générés d'un étudiant (filtre ?section=) |
| GET | /integrations/v1/students/:login/available-documents |
Types de documents générables selon la classe de l'étudiant |
| GET | /integrations/v1/documents/:tracking_code |
Métadonnées + URLs de téléchargement fraîches |
| GET | /integrations/v1/documents/:tracking_code/download |
Télécharger le PDF (?lang=fr\|en, storage-agnostic) |
Documentation live (publique, sans clé) :
| Format | URL |
|---|---|
| ReDoc | /integrations/v1/docs |
| Swagger UI | /integrations/v1/swagger |
| Schéma OpenAPI | /integrations/v1/schema.json |
Administration des comptes & clés (session admin JWT, super-admin uniquement) :
| Méthode | Route | Description |
|---|---|---|
| GET/POST | /integrations/v1/admin/service-accounts |
Lister / créer un compte de service (la création renvoie la clé une seule fois) |
| GET/PATCH/DELETE | /integrations/v1/admin/service-accounts/:id |
Détail / modifier (isActive, name…) / supprimer |
| POST | /integrations/v1/admin/service-accounts/:id/keys |
Émettre une nouvelle clé |
| POST | /integrations/v1/admin/keys/:id/revoke |
Révoquer une clé |
| POST | /integrations/v1/admin/keys/:id/rotate |
Révoquer la clé et en émettre une nouvelle |
Clé en clair affichée une seule fois : seules les opérations de création / émission / rotation renvoient le champ
apiKey. Le backend ne stocke que le hash SHA-256. La création en CLI est possible :python manage.py create_service_account zeno.
Présence campus (/attendance/)¶
Version détaillée
Cette table est le résumé rapide. Modèle de données complet (rôle chaperon, annulation de scan), pièges cross-origin et incident de perf sur la génération de badge : Présence Campus (Attendance).
Authentification par cookie de session dédié (attendance_session, JWT, 12h) pour un agent/chaperon, ou cookie auth_token admin avec le droit can_scan_attendance pour scan/.../note/.../cancel — totalement séparé de l'auth admin/étudiant classique pour le reste. Chaque route existe avec et sans slash final (APPEND_SLASH=False au niveau du projet, voir troubleshooting).
| Méthode | Route | Auth | Description |
|---|---|---|---|
| POST | /attendance/auth |
aucune | Connexion agent/chaperon par PIN → pose le cookie attendance_session |
| GET | /attendance/me |
cookie agent | Vérification proactive de la session (sans effet de bord) |
| POST | /attendance/logout |
cookie agent | Invalide la session courante |
| POST | /attendance/scan |
cookie agent OU admin | Scanne un QR ({ "payload": "<signature:login>" }) → granted/denied + fiche étudiant |
| PATCH | /attendance/scan/:log_id/note |
cookie agent OU admin | Ajoute/édite une remarque libre sur un scan déjà effectué |
| PATCH | /attendance/scan/:log_id/cancel |
cookie agent OU admin | Nouveau (2026-09-09) — annule un scan granted par erreur ({ "reason": "..." } obligatoire) |
| GET/POST | /attendance/reasons |
admin + can_manage_attendance |
Lister / créer un motif de suspension |
| GET/POST | /attendance/guards?role= |
admin + can_manage_attendance |
Lister / créer un agent ou chaperon (PIN affiché une seule fois) |
| PATCH | /attendance/guards/:id/revoke |
admin + can_manage_attendance |
Désactive un agent (réversible) |
| POST | /attendance/guards/:id/reset-pin |
admin + can_manage_attendance |
Régénère le PIN, invalide les sessions en cours |
| DELETE | /attendance/guards/:id |
admin + can_manage_attendance |
Suppression définitive (n'efface pas l'historique des scans) |
| GET | /attendance/logs |
admin + can_manage_attendance |
Historique paginé (filtres student_login, decision, guard_id, guard_role, cancelled, ordering) |
| GET | /attendance/stats/by-scanner |
admin + can_manage_attendance |
Nouveau — activité agrégée par personne ayant scanné (agent/chaperon/admin) |
| POST | /attendance/qrcodes/audience |
admin + can_manage_attendance |
Aperçu en lecture seule de qui recevrait un envoi de badges |
| POST | /attendance/qrcodes/send |
admin + can_manage_attendance |
Envoi en masse des badges (asynchrone, 202 Accepted) |
| GET | /attendance/media/photos/:file_name |
cookie agent/admin/étudiant | Sert une photo étudiant |
| GET | /attendance/media/qrcodes/:file_name |
admin | Sert un badge/QR généré |
Endpoints connexes sur apps/student/ et apps/user/ :
| Méthode | Route | Description |
|---|---|---|
| GET/POST | /student/:login/photo |
Consulter / mettre à jour la photo d'un étudiant (admin, étudiant sur sa propre photo, ou agent) |
| POST | /student/photos/bulk-upload/ |
Import en masse des photos via ZIP ({login}.jpg) |
| GET | /student/:login/qrcode |
Génère/prévisualise le badge d'un étudiant (admin, avant envoi) |
| POST | /student/admin/:login/qrcode/send |
Nouveau — renvoie le badge d'UN étudiant, éventuellement vers une adresse email personnelle |
| PUT | /students/suspend |
Suspendre/réactiver — exige désormais reason_id ou reason_label à la suspension |
Modèle de données¶
Student
├── login (PK, email EPITECH)
├── civilite, nom, prenom
├── date_naissance, ville_naissance, nationalite
├── section (PGE1, PGE2, PGE3, etc.)
├── promo, annee, ville_etude
├── inscription_year, inscription_month
├── nom_pere, prenom_pere, nom_mere, prenom_mere
├── specialite
├── PGE3_status, PGE5_status, Codingacademy_status (success/null)
├── campus → FK(Campus, nullable)
├── account_suspended (0/1)
├── suspension_reason → FK(SuspensionReason, nullable) — motif figé au moment de la suspension
└── photo (ImageField, upload_to=student_photos/, nullable) — badge de présence
SuspensionReason
├── label (unique, ex: "Renvoi pour raisons de scolarité")
└── created_at
User (Admin)
├── email (PK), first_name, last_name
├── is_staff, is_superuser
├── rights → ManyToMany(Right, via UserRight)
└── azure_id
Right
└── name (unique, ex: can_manage_attendance — voir § Système de permissions)
UserRight
├── user → FK(User)
├── right → FK(Right)
└── granted_at
(unique_together: user + right)
Campus
├── name (ex: "Bénin")
├── code (ex: "BJ")
├── director_title (titre du directeur en FR)
├── director_title_en (titre du directeur en EN)
└── extra_fields (JSON — champs supplémentaires par campus)
CampusDocumentType
├── campus → FK(Campus)
├── document_type → FK(DocumentType)
├── modele (template FR spécifique au campus, optionnel)
└── modele_en (template EN spécifique au campus, optionnel)
ClassDocumentType (override par classe — même principe que CampusDocumentType, axe différent)
├── classe (PGE1, PGE2, ... — clé de settings.CLASSES)
├── document_type → FK(DocumentType, related_name=class_overrides)
├── modele (template FR spécifique à la classe, optionnel)
└── modele_en (template EN spécifique à la classe, optionnel)
DocumentType
├── doc_id (certificat_scolarite, attestation_passage_pge, etc.)
├── modele (fichier .docx template FR)
├── modele_en (fichier .docx template EN, optionnel)
├── is_active
└── created_by → FK(User)
TrackingCode
├── tracking_code (PK, UUID)
├── doc_ref (REF/CS/abc/def)
├── generated_by → FK(Student)
└── generated_by_admin → FK(User)
GenDocumentHistory
├── tracking_code → OneToOne(TrackingCode)
├── doc_id, current_classe
├── generation_count
├── remote_url (chemin local ou Cloudinary public_id — version FR)
├── remote_url_en (chemin local ou Cloudinary public_id — version EN, nullable)
├── generated_by → FK(Student)
└── generated_by_admin → FK(User)
UserSession
├── session_id (UUID)
├── user → FK(User) ou student → FK(Student)
├── role (admin/student)
├── is_valid, expires_at
└── ip_address, user_agent
AuditLog
├── action (login, document_generated, document_downloaded, batch_generate_by_admin,
│ info_note_created/deleted, service_account_created/updated/deleted,
│ service_key_issued/revoked/rotated, etc.)
├── actor_email, actor_role (admin/superadmin/student/service)
├── target_type, target_id
├── details (JSON)
└── ip_address, timestamp
ServiceAccount (intégration Zeno)
├── name (unique, ex: "zeno")
├── description
├── is_active
└── created_at
ServiceApiKey
├── service → FK(ServiceAccount)
├── prefix (8 premiers caractères, pour identification)
├── key_hash (SHA-256 — la clé brute n'est jamais stockée)
├── is_active, revoked_at
├── last_used_at
└── created_at
SystemStatus
├── is_maintenance (bool)
├── maintenance_message
├── scheduled_start, scheduled_end
└── updated_by → FK(User)
Announcement
├── title, message
├── target (all/students/admins)
├── target_class (PGE1, PGE2, etc.)
├── priority (info/warning/danger)
├── is_active, start_date, end_date
└── created_by → FK(User)
InfoNote
├── title, description
├── pdf_file (upload PDF)
├── target_classes (JSONField, ex: ["PGE1", "PGE2"])
├── is_active
├── created_by → FK(User)
├── created_at, updated_at
└── downloads → InfoNoteDownload[]
InfoNoteDownload
├── info_note → FK(InfoNote)
├── student → FK(Student)
└── downloaded_at
PendingStudent (inscription via le formulaire public, avant validation admin)
├── login, nom, prenom, ... (mêmes champs que Student, PAS de champ campus —
│ voir troubleshooting.md, incident du 2026-09-07)
└── status (pending/rejected)
Right / UserRight (droits fins, indépendants d'is_staff/is_superuser)
├── Right.name (ex: can_manage_attendance, can_edit_student_info, ...)
└── UserRight (M2M User↔Right, + granted_at)
SecurityGuard / AttendanceSession / AttendanceLog / SuspensionReason
└── modèle complet (avec le rôle chaperon et l'annulation de scan ajoutés le
2026-09-09) documenté sur la page dédiée : Présence Campus (Attendance)
Système de permissions¶
Modèle Right (nom unique) + UserRight (M2M User↔Right, table de jointure explicite avec granted_at). Vérifié par HasRightPermission (libs/permissions.py) : un superadmin (is_superuser) passe toujours, un admin (is_staff) doit avoir le droit nommé dans request.user.rights, tout autre type d'utilisateur (étudiant, agent de sécurité PIN) est rejeté proprement. Chaque droit a sa propre classe de permission dédiée dans libs/permissions.py (ex. CanManageAttendancePermission, CanEditStudentInfoPermission) — jamais de vérification de droit inline dans une vue.
24 droits existent aujourd'hui (liste faisant autorité : apps/user/management/commands/seed_rights.py). Seuls 4 sont accordés automatiquement aux admins non-superadmin (base_rights) ; les 20 autres sont à accorder explicitement, y compris pour des comptes admin déjà existants :
| Droit | Description | Auto-accordé (base_rights) |
|---|---|---|
can_upload_tepitech |
Import des scores TEPITECH | oui |
can_upload_user |
Import CSV des étudiants | oui |
can_generate |
Générer des documents | oui |
can_generate_for_student |
Générer pour un étudiant | oui |
can_migrate_students |
Migrer les promotions | non |
can_batch_generate |
Génération en lot | non |
manage_campus |
Gérer campus/overrides campus | non |
can_manage_attendance |
Gérer la présence campus (agents, motifs, logs, envoi QR) | non |
can_scan_attendance |
Scanner un QR depuis le dashboard admin (sans compte agent PIN) | non |
can_manage_data_integrity |
Utiliser le registre de vérifications d'intégrité des données | non |
can_edit_student_info |
Corriger les infos d'un étudiant sans passer par une demande | non |
can_view_students |
Consulter la liste des étudiants | non |
can_export_students |
Exporter les étudiants en CSV | non |
can_suspend_students |
Suspendre/réactiver un étudiant | non |
can_transfer_students |
Transférer le campus d'un étudiant | non |
can_view_student_logins |
Consulter les logins étudiants | non |
can_review_pending_students |
Valider/rejeter les étudiants en attente | non |
can_review_info_changes |
Traiter les demandes de modification d'info | non |
can_manage_complaints |
Gérer/résoudre les réclamations | non |
can_manage_document_types |
Gérer les types de documents et leurs overrides (campus/classe) | non |
can_delete_documents |
Supprimer un document généré | non |
can_view_document_history |
Consulter l'historique d'un étudiant | non |
can_view_dashboard_stats |
Consulter les statistiques du dashboard | non |
can_manage_info_notes |
Gérer les notes d'information | non |
Les superadmins reçoivent automatiquement tous les droits.
Ne pas dupliquer cette table indéfiniment — elle driftera
Elle a déjà dû être corrigée une fois (20 → 24 droits entre le 2026-09-01 et le 2026-09-09). La liste vit dans le code (seed_rights.py pour la liste brute, RIGHT_METADATA dans apps/user/admin_views.py pour le libellé/description/groupe de chacun) et est exposée en direct via GET /admin/rights — en cas de doute sur l'exhaustivité de cette table, c'est la source à consulter, pas cette page. Catégories actuelles (group dans RIGHT_METADATA) : students, documents, complaints, presence, campus, reports, uploads.
Comment les droits sont initialisés/synchronisés :
- Crée tous les
Rightde la liste s'ils n'existent pas (idempotent) - Assigne tous les droits à chaque
is_superuser - Assigne
base_rights(can_upload_tepitech,can_upload_user,can_generate,can_generate_for_student) à chaque admin non-superadmin qui n'en a pas déjà
Cette commande tourne automatiquement à chaque démarrage du conteneur (docker-compose.prod.yml, chaîne migrate && seed_rights && seed_suspension_reasons && ...) — le système de droits (et les motifs de suspension par défaut) sont donc auto-réparants à chaque déploiement.
Un nouveau droit n'est PAS auto-attribué aux admins non-superadmin
Sauf s'il est explicitement ajouté à base_rights dans seed_rights.py — décision volontaire ("c'est moi qui attribue les droits"). Ajouter un droit à la liste rights seule ne le donne qu'aux superadmins ; il faut l'attribuer manuellement aux autres admins via le dashboard (gestion des droits) ou l'API rights. Le pattern complet pour ajouter un nouveau droit :
- L'ajouter à la liste
rightsdansseed_rights.py(et àbase_rightsseulement s'il doit être auto-accordé). - Sous-classer
HasRightPermissiondanslibs/permissions.py(ex.class CanFooPermission(HasRightPermission): def __init__(self): super().__init__('can_foo')). - L'ajouter à
permission_classesde la vue, en plus depermissions.IsAdminUser(jamais à la place) :permission_classes = [permissions.IsAdminUser, CanFooPermission].
Implication opérationnelle : accorder un nouveau droit sur un endpoint déjà utilisé est un breaking change silencieux pour tout admin non-superadmin qui l'utilisait déjà — l'accès échoue fermé (403), pas ouvert, tant que quelqu'un n'a pas explicitement accordé le droit à ce compte.
Actions volontairement non-délégables (réservées IsSuperAdmin, jamais via Right) : création d'autres comptes admin (CreateUserView, apps/user/views.py) et assignation de gestionnaires de campus (CampusManagerListView/CampusManagerDeleteView, apps/campus/views.py) — ce sont des actions d'élévation de privilège, pas des tâches métier déléguables.
Types de documents¶
| ID | Nom | Classes autorisées | Condition |
|---|---|---|---|
certificat_scolarite |
Certificat de scolarité | Toutes | Étudiant actif (non diplômé) |
attestation_scolarite |
Attestation de scolarité | PGE3, PGE5 | PGE3_status=success ou PGE5_status=success |
attestation_passage_pge |
Attestation de passage PGE | PGE2+ | — |
attestation_validation_tepitech |
Attestation TEPITECH | Toutes | Score TEPITECH requis |
attestation_fin_succes_bachelor |
Attestation fin de Bachelor | PGE3 | PGE3_status=success |
attestation_fin_success_master |
Attestation fin de Master | PGE5 | PGE5_status=success |
lettre_recommandation |
Lettre de recommandation | PGE2+ | — |
attestation_passage_coding_academy |
Attestation Coding Academy | CodingAcademy | — |
Logique diplômé PGE3/PGE5 : dès que
PGE3_statusouPGE5_statusestsuccess, l'étudiant ne peut plus générer lecertificat_scolarite. Il accède à la place à l'attestation_scolarite. Le statut est posé par l'admin via la routePOST /student/admin/set-graduation-status/.
Présence campus, badges QR (apps/attendance/)¶
Demande d'Ernesto, agent de sécurité au portail campus : scanner le QR d'un étudiant à l'entrée et savoir instantanément s'il doit autoriser ou refuser l'accès. Conception détaillée : ATTENDANCE_IMPLEMENTATION.md et API_CHANGES_ATTENDANCE.md à la racine du repo. Pour les chaperons, l'annulation de scan, et le suivi par personne (ajoutés le 2026-09-09) : voir Présence Campus (Attendance).
Authentification isolée : apps/attendance/authentication.py (AttendanceGuardAuthentication) vérifie le cookie attendance_session (JWT, AttendanceSession en base, 12h) et pose request.user = SecurityGuard. Volontairement séparé de JWTSessionAuthentication/UserSession (admin/étudiant) : un agent de sécurité n'a ni email ni droits, et on ne veut prendre aucun risque de régression sur le chemin d'authentification JWT partagé, critique en production, pour une fonctionnalité annexe. Le PIN est hashé (SecurityGuard.pin_hash, SHA-256 salé au SECRET_KEY) — le vrai contrôle d'accès reste humain (l'agent physique), le PIN n'a pas besoin d'un hash lent type bcrypt.
QR signé HMAC (apps/attendance/qr.py) : le contenu encodé est signature:login — login (e-mail Epitech), pas id_etudiant (matricule non encore renseigné pour la plupart des étudiants). La signature est un HMAC-SHA256 calculé avec ATTENDANCE_QR_SECRETS (project/settings.py, dérivé de la variable d'env ATTENDANCE_QR_SECRET, une liste séparée par des virgules, défaut = SECRET_KEY si non définie) :
build_qr_payload()signe toujours avec le premier secret de la liste.verify_qr_payload()accepte une signature valide pour n'importe lequel des secrets de la liste.
Cette dissymétrie permet une rotation sans cutover brutal : ajouter le nouveau secret en tête de la variable d'env (nouveau,ancien), redéployer, renvoyer les badges à tous (POST /attendance/qrcodes/send), laisser une fenêtre de transition, puis retirer l'ancien secret.
Limite assumée : signer empêche de fabriquer un faux QR pour l'identité d'un autre étudiant, mais n'empêche pas un étudiant de présenter le vrai QR de quelqu'un d'autre (capture d'écran, téléphone prêté).
decision: grantedsignifie « ce code est authentique et cet étudiant n'est pas suspendu », pas « la personne en face est bien celle-là » — c'est pour ça que la fiche (photo, nom, prénom) est renvoyée au scan : la protection contre ce scénario est humaine, pas cryptographique.
Badge visuel (apps/attendance/badge.py) : ce qui est envoyé n'est pas un QR nu mais un badge complet — QR signé + identité + logo Epitech + campus de l'étudiant (résolu dynamiquement, jamais codé en dur), sans section/promo (valable toute la scolarité). Rendu suréchantillonné (SCALE = 5, dessiné à 5× puis réduit en LANCZOS) pour un anti-aliasing net en PIL ; polices réelles de la charte (IBM Plex Sans/Mono, fichiers dans apps/attendance/assets/fonts/) pour un rendu identique quel que soit l'environnement de déploiement. Le badge part en pièce jointe e-mail réelle, pas en <img src="data:..."> inline — cassé sur Outlook desktop classique (moteur Word), qui ne rend pas les images en data: URI.
Exclusion des diplômés/suspendus par défaut (_build_qr_audience(), apps/attendance/views.py) — voir l'incident détaillé dans troubleshooting.md. En bref : un étudiant diplômé garde sa section en base (aucune section "ALUMNI" n'existe), donc filtrer un envoi par section seul touche aussi les diplômés. _build_qr_audience() est partagée entre l'aperçu (AttendanceQRAudienceView, lecture seule) et l'envoi réel (AttendanceQRSendView) pour qu'ils ne puissent jamais diverger, et exclut par défaut :
- les diplômés (
PGE3_status/PGE5_status/Codingacademy_status == 'success'pour la section correspondante) ; - les comptes suspendus (
account_suspended == 1).
Désactivable ponctuellement via include_graduated/include_suspended dans le body. Toujours appeler /attendance/qrcodes/audience avant /attendance/qrcodes/send pour vérifier la liste exacte des destinataires.
Envoi asynchrone (threading.Thread) : la génération de badge (rendu PIL suréchantillonné) est trop lente par étudiant pour tenir dans le timeout nginx→gunicorn (~60s) au-delà d'un ou deux étudiants — nginx renvoyait sa propre page 404/504 pendant que Django continuait de tourner en tâche de fond, invisible côté admin (incident du 2026-08-29). AttendanceQRSendView.post matérialise la liste des étudiants avant de lancer le thread (le queryset ne doit pas être évalué paresseusement dans le thread), répond immédiatement 202 Accepted, et trace chaque résultat individuel dans AuditLog (attendance_qr_sent / attendance_qr_send_failed) — à consulter via GET /admin/audit-logs?action=attendance_qr_sent.
APPEND_SLASH=Falseet les routes doublées :project/settings.pydésactive la redirection automatique Django vers/depuis le slash final.apps/attendance/urls.pyenregistre donc chaque route deux fois (avec et sans slash) dans une boucle, pour tolérer les deux conventions côté frontend — pattern à réutiliser pour toute nouvelle app plutôt que de découvrir un 404 en prod sur une route qui existe mais ne matche pas l'URL appelée. Voir troubleshooting.md.
Diagrammes¶
Architecture globale¶
graph TB
Student[Étudiant / Navigateur]
Admin[Admin / Navigateur]
Frontend["Frontend React<br/>api.mydocs.epitools.bj"]
Nginx["Nginx<br/>Reverse proxy + TLS"]
Backend["Backend API<br/>Django :8000"]
PostgreSQL["PostgreSQL 16<br/>(conteneur)"]
AzureAD["Azure AD<br/>(SSO Microsoft)"]
LibreOffice["LibreOffice<br/>(conversion PDF)"]
Media["Media<br/>(bind mount)"]
Student --> Nginx
Admin --> Nginx
Nginx --> Frontend
Nginx --> Backend
Backend --> PostgreSQL
Backend --> AzureAD
Backend --> LibreOffice
Backend --> Media
Flux de génération de document¶
sequenceDiagram
participant User as Étudiant/Admin
participant API as Backend API
participant DB as PostgreSQL
participant TPL as DocxTemplate
participant LO as LibreOffice
participant FS as Stockage (media/)
User->>API: POST /documents/generate-document
API->>DB: Vérifier historique existant
alt Document déjà généré (FR + EN)
API->>FS: Récupérer le PDF FR existant
FS-->>API: PDF
API-->>User: PDF + X-Has-En: true + X-Tracking-Code
else Document déjà généré (FR seulement, template EN maintenant dispo)
API->>TPL: Remplir template EN (.docx)
TPL-->>API: .docx EN rempli
API->>LO: Convertir .docx → .pdf EN
API->>FS: Sauvegarder PDF EN
API->>DB: Mettre à jour remote_url_en (même tracking code conservé)
API->>FS: Récupérer le PDF FR existant
API-->>User: PDF FR + X-Has-En: true + X-Tracking-Code
else Nouveau document
API->>DB: Résoudre template (campus override si applicable)
API->>DB: Générer tracking code + référence
API->>TPL: Remplir template FR .docx
TPL-->>API: .docx FR rempli
API->>LO: Convertir .docx → .pdf FR
API->>TPL: Remplir template EN .docx (si disponible)
TPL-->>API: .docx EN rempli
API->>LO: Convertir .docx → .pdf EN
API->>API: Ajouter QR code, nettoyer pages vides
API->>FS: Sauvegarder PDF FR + PDF EN
API->>DB: Créer GenDocumentHistory (remote_url + remote_url_en)
API-->>User: PDF FR + X-Has-En: true/false + X-Tracking-Code
end
Headers de réponse : chaque génération retourne
X-Tracking-Code(code de suivi) etX-Has-En(true/false). Le frontend utiliseX-Has-Enpour afficher ou non le bouton de téléchargement EN.Résolution des templates (campus + classe) : avant la génération,
resolve_template_paths()(apps/document_type/views.py) applique les overrides dans l'ordreCampusDocumentType>ClassDocumentType>DocumentTypeglobal. Les deux axes d'override sont indépendants (un par campus, un par classe PGE1/PGE2/.../CodingAcademy) ; si les deux existent pour le même document, celui du campus l'emporte — pas de fusion des deux. Voir § Surcharge des templates par classe plus bas.
Flux de génération en lot (batch)¶
sequenceDiagram
participant Admin as Admin
participant API as Backend API
participant DB as PostgreSQL
participant Gen as Génération unitaire
participant ZIP as Archive ZIP
Admin->>API: POST /documents/admin/batch-generate
API->>DB: Récupérer tous les étudiants de la classe
loop Pour chaque étudiant
API->>DB: Vérifier si document existe
alt Existe
API->>Gen: Récupérer document existant
else N'existe pas
API->>Gen: Générer nouveau document
end
Gen-->>API: PDF
API->>ZIP: Ajouter au ZIP
end
API->>DB: AuditLog (batch_generate_by_admin)
API-->>Admin: ZIP (Content-Disposition: attachment)
Middleware de maintenance¶
graph TD
Request[Requête entrante] --> CheckPath{Path exempté ?}
CheckPath -->|Oui| Pass[Laisser passer]
CheckPath -->|Non| CheckMaintenance{Mode maintenance actif ?}
CheckMaintenance -->|Non| Pass
CheckMaintenance -->|Oui| CheckAdmin{Utilisateur admin ?}
CheckAdmin -->|Oui| Pass
CheckAdmin -->|Non| Block[503 Service Unavailable]
Flux des notes d'information¶
Les notes d'information permettent aux admins de publier des PDFs (convocations, plannings, circulaires) ciblés par classe. Contrairement aux documents générés, il n'y a ni template docx, ni QR code, ni tracking code — juste un PDF uploadé et téléchargeable.
sequenceDiagram
participant Admin as Admin
participant API as Backend API
participant DB as PostgreSQL
participant FS as Stockage (media/info_notes/)
Admin->>API: POST /documents/admin/info-notes<br/>(multipart: title, pdf_file, target_classes)
API->>FS: Sauvegarder le PDF
API->>DB: Créer InfoNote
API-->>Admin: 201 Created
Note over Admin,API: Plus tard, l'admin peut toggle le statut
Admin->>API: PATCH /documents/admin/info-notes/:id/toggle
API->>DB: Inverser is_active
API-->>Admin: {id, isActive: false}
sequenceDiagram
participant Student as Étudiant (PGE1)
participant API as Backend API
participant DB as PostgreSQL
participant FS as Stockage
Student->>API: GET /documents/available-document-types?classe=PGE1
API->>DB: Récupérer docs + InfoNote actives pour PGE1
API-->>Student: {documents: [...], infoNotes: [{id, title, description}]}
Student->>API: GET /documents/info-notes/1/download
API->>DB: Vérifier classe étudiant ∈ target_classes
API->>DB: Enregistrer InfoNoteDownload
API->>FS: Lire le PDF
API-->>Student: PDF (Content-Disposition: attachment)
Paths exemptés du mode maintenance :
/auth/sign_in,/auth/callback,/auth/me/student/auth/me/admin/system-status,/admin/announcements
Flux d'intégration Zeno (service-à-service)¶
Zeno (service backend externe) consomme l'API EPIDOCS avec une clé API. Aucun cookie/session : l'authentification se fait par le header Authorization: Api-Key <clé>, validé contre le hash stocké. La génération réutilise exactement le moteur des documents étudiants/admin (generate_document_for_student).
sequenceDiagram
participant Zeno as Zeno (service)
participant API as Backend API
participant Auth as ServiceAccountAuthentication
participant Gen as Service génération
participant DB as PostgreSQL
participant FS as Stockage (media/)
Zeno->>API: POST /integrations/v1/documents/generate<br/>Authorization: Api-Key xxx
API->>Auth: Vérifier la clé (hash SHA-256, active, non révoquée)
Auth-->>API: ServiceAccount (sinon 401)
API->>Gen: generate_document_for_student(student, docId)
Gen->>DB: Vérifier classe + historique existant
alt Document déjà généré
Gen-->>API: GenDocumentHistory existant
else Nouveau
Gen->>FS: Générer + sauvegarder PDF (FR/EN)
Gen->>DB: Créer GenDocumentHistory
end
API->>DB: AuditLog (document_generated, role=service)
API-->>Zeno: JSON { trackingCode, downloadUrl, downloadUrlEn, ... }
Zeno->>API: GET /integrations/v1/documents/:tc/download?lang=fr
API->>Auth: Vérifier la clé
API->>FS: retrieve_student_document (local ou Cloudinary)
API->>DB: AuditLog (document_downloaded, role=service, IP)
API-->>Zeno: PDF (application/pdf)
Audit : chaque génération et chaque téléchargement effectués par un service sont tracés dans
AuditLogavecactor_role = service(incluanttrackingCode,studentLogin, langue et IP). Les lectures de métadonnées (liste/détail) ne sont volontairement pas tracées pour éviter le bruit.
Flux de scan de présence (Ernesto)¶
sequenceDiagram
participant Guard as Agent (Ernesto)
participant API as Backend API
participant Auth as AttendanceGuardAuthentication
participant QR as qr.verify_qr_payload
participant DB as PostgreSQL
Guard->>API: POST /attendance/auth { pin }
API->>DB: SecurityGuard.authenticate(pin) — compare le hash pour chaque agent actif
API->>DB: Créer AttendanceSession (12h)
API-->>Guard: 200 + cookie attendance_session
Guard->>API: POST /attendance/scan { payload }
API->>Auth: Vérifier le cookie attendance_session
Auth-->>API: SecurityGuard (sinon 401)
API->>QR: verify_qr_payload(payload)
alt Signature invalide ou login inconnu
QR-->>API: None
API->>DB: AttendanceLog(decision=denied, reason="QR invalide"/"Étudiant introuvable")
else Login valide mais suspendu
QR-->>API: login
API->>DB: AttendanceLog(decision=denied, reason=motif de suspension)
else Login valide et actif
QR-->>API: login
API->>DB: AttendanceLog(decision=granted)
end
API-->>Guard: 200 { logId, decision, student: {nom, prenom, section, photoUrl}, reason }
Toujours 200 : la décision (
granted/denied) est dans le corps de la réponse, pas dans le status HTTP — unAttendanceLogest créé systématiquement, y compris en cas de refus/erreur.Gestion des clés : les comptes de service et leurs clés se gèrent depuis le dashboard (super-admin). Une clé peut être révoquée ou régénérée (rotate) à tout moment, coupant immédiatement l'accès du service concerné.
Surcharge des templates par classe (ClassDocumentType)¶
Ajouté pour la lettre de recommandation, dont le contenu doit différer selon le niveau de l'étudiant (PGE2/PGE3/PGE4/PGE5/CodingAcademy) — un seul DocumentType ne porte qu'un modele/modele_en global. ClassDocumentType reprend exactement le principe de CampusDocumentType (apps/campus/models.py) mais sur l'axe classe : classe (clé de settings.CLASSES) + document_type (FK) + modele/modele_en optionnels.
Endpoints admin (droit can_manage_document_types) :
| Méthode | Route | Description |
|---|---|---|
| GET/POST | /documents/admin/classes/:classe/templates |
Lister / créer-remplacer l'override d'une classe pour un document |
| DELETE | /documents/admin/classes/:classe/templates/:doc_id |
Supprimer l'override |
Le champ document_type du ClassDocumentTypeSerializer utilise SlugRelatedField(slug_field='doc_id', ...) plutôt que le PrimaryKeyRelatedField par défaut de DRF : identifier un type de document par son doc_id lisible (lettre_recommandation) plutôt que par son PK numérique opaque est plus pratique pour l'admin qui remplit le formulaire et pour le frontend, qui n'a jamais besoin de connaître le PK.
ClassDocumentTypeListView.post fait un get_or_create(classe=classe, document_type=...) puis assigne modele/modele_en champ par champ (pas update_or_create) : un admin doit pouvoir mettre à jour uniquement modele_en sans re-fournir modele, ce qu'un defaults={'modele': None} effacerait par erreur.
Deux bugs réels rencontrés en construisant cette fonctionnalité
1. Méthodes de DocumentTypeSerializer sectionnées par erreur — GET /documents/ (et tout SerializerMethodField de DocumentTypeSerializer) est parti en 500 en production après le déploiement de ClassDocumentType (PR #25). Cause : l'édition texte qui a inséré ClassDocumentTypeSerializer dans apps/document_type/serializers.py l'a insérée entre la classe Meta de DocumentTypeSerializer et ses méthodes (get_name, get_created_by, get_variables, get_variables_en) — celles-ci se sont retrouvées silencieusement reparentées sur ClassDocumentTypeSerializer, qui n'en avait pourtant pas besoin. Aucune erreur à l'édition ni à l'import : Python accepte parfaitement une méthode définie sur la mauvaise classe. Corrigé par fix: restore DocumentTypeSerializer methods severed by ClassDocumentType edit (PR #26). Leçon : après avoir inséré une classe/méthode par édition de texte "entre deux blocs", relire le fichier complet pour confirmer les frontières de classe — ne jamais supposer qu'une insertion "entre deux lignes vides" est tombée au bon endroit.
2. request.data.copy() plante sur un upload de fichier — ClassDocumentTypeListView.post faisait data = request.data.copy(); data['classe'] = classe pour injecter classe dans le payload avant validation. Deux problèmes : classe est read_only sur le serializer (l'injection ne servait déjà à rien, classe venant en réalité de l'URL) et .copy() sur un QueryDict multipart tente un deepcopy qui plante (cannot pickle 'BufferedRandom') dès qu'un fichier est attaché à la requête — le flux de fichier ouvert n'est pas copiable. Corrigé en passant request.data sans le copier. Leçon générale : si un champ doit être injecté avant validation par un serializer, ne jamais appeler .copy() sur un QueryDict multipart — soit éviter la mutation (faire venir la valeur de l'URL/vue plutôt que du body), soit reconstruire un dict simple à partir de zéro.
Docker¶
Dockerfile¶
L'image utilise python:3.12 avec LibreOffice installé (nécessaire pour la conversion DOCX → PDF). L'application ne tourne pas en tant qu'utilisateur non-root (à améliorer).
docker-compose.yml (production)¶
services:
db:
restart: always
image: postgres:16
container_name: epidocs_db
volumes:
- epidocs-api_postgres_data:/var/lib/postgresql/data/
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-epidocs_db}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
retries: 5
start_period: 30s
web:
image: ghcr.io/epitechafrik/epidocs-api:deploy
container_name: epidocs_api
restart: unless-stopped
ports:
- "7010:8000"
env_file:
- .env
volumes:
- ./media:/epidocs/media
- static_data:/epidocs/staticfiles
command: >
bash -c "python manage.py migrate &&
python manage.py seed_rights &&
python manage.py seed_suspension_reasons &&
python manage.py collectstatic --noinput &&
gunicorn project.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 120"
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
epidocs-api_postgres_data:
external: true
static_data:
Points importants :
- Volume externe
epidocs-api_postgres_data: les données PostgreSQL persistent entre les déploiements. Ce volume doit exister avant le premierdocker compose up. - Bind mount
./media: les templates Word et les documents générés sont stockés sur le disque du VPS, pas dans le conteneur. Cela permet de conserver les templates entre les déploiements. - Startup : au démarrage, le conteneur exécute
migrate+seed_rights+seed_suspension_reasons+collectstaticavant de lancer Gunicorn — le système de droits (et les motifs de suspension par défaut) sont donc auto-réparants à chaque déploiement. - Port :
7010côté hôte, accessible uniquement via Nginx. - Le port PostgreSQL n'est pas exposé sur l'hôte (sécurité).
Variables d'environnement¶
Django¶
| Variable | Description | Exemple |
|---|---|---|
DJANGO_ENV |
Mode d'exécution | production |
SECRET_KEY |
Clé secrète Django | chaîne aléatoire longue |
ALLOWED_HOSTS |
Hôtes autorisés (séparés par virgule) | api.mydocs.epitools.bj,localhost |
Base de données¶
| Variable | Description | Exemple |
|---|---|---|
POSTGRES_DB |
Nom de la base | epidocs_db |
POSTGRES_USER |
Utilisateur PostgreSQL | postgres |
POSTGRES_PASSWORD |
Mot de passe PostgreSQL | CHANGE_ME |
POSTGRES_HOST |
Hôte PostgreSQL | db (nom du service Docker) |
Authentification¶
| Variable | Description | Exemple |
|---|---|---|
JWT_SECRET_KEY |
Clé secrète JWT (optionnel, défaut = SECRET_KEY) | chaîne aléatoire |
JWT_COOKIE_DOMAIN |
Domaine du cookie (prod uniquement) | .epitools.bj |
ATTENDANCE_QR_SECRET |
Secret(s) HMAC pour signer/vérifier les QR de présence — liste séparée par virgules pour rotation (signature = 1er secret, vérification = n'importe lequel). Optionnel, défaut = SECRET_KEY (déconseillé en prod : une fuite de SECRET_KEY, utilisé partout ailleurs, aurait alors le même impact que la fuite du secret QR) |
nouveau_secret,ancien_secret |
Azure AD¶
| Variable | Description | Exemple |
|---|---|---|
AZURE_CLIENT_ID |
Client ID de l'App Registration | UUID |
AZURE_CLIENT_SECRET |
Client Secret Azure | CHANGE_ME |
AZURE_TENANT_ID |
Tenant ID Azure | UUID |
CALLBACK_URL |
URL de callback OAuth | https://api.mydocs.epitools.bj/auth/callback |
URLs¶
| Variable | Description | Exemple |
|---|---|---|
WEB_URL |
URL du frontend | https://app.epitools.bj |
SITE_URL |
URL de l'API | https://api.mydocs.epitools.bj |
Email (optionnel)¶
| Variable | Description | Exemple |
|---|---|---|
RESEND_API_KEY |
Clé API Resend | re_xxxxx |
SMTP_SERVER |
Serveur SMTP | smtp.example.com |
SMTP_PORT |
Port SMTP | 465 |
SMTP_USERNAME |
Utilisateur SMTP | no-reply@epitech.bj |
SMTP_PASSWORD |
Mot de passe SMTP | CHANGE_ME |
Cloudinary (si STORAGE_TYPE=remote)¶
| Variable | Description | Exemple |
|---|---|---|
CLOUDINARY_CLOUD_NAME |
Nom du cloud | my-cloud |
CLOUDINARY_API_KEY |
Clé API | 123456 |
CLOUDINARY_API_SECRET |
Secret API | CHANGE_ME |
Déploiement¶
Pipeline CI/CD¶
Le déploiement est déclenché automatiquement sur push vers la branche deploy via GitHub Actions.
graph LR
Push["Push sur deploy"] --> CI["CI: Build & Test"]
CI --> Docker["Build image Docker"]
Docker --> GHCR["Push sur GHCR"]
GHCR --> Deploy["SCP + SSH sur VPS"]
Deploy --> Health["Health check"]
Étapes détaillées :
- CI : Python 3.12, PostgreSQL 16 de test, linting (flake8), vérification des migrations, tests
- Build Docker : construction de l'image et push sur
ghcr.io/epitechafrik/epidocs-api:deploy - Déploiement : SCP du
docker-compose.prod.ymlet.envsur le VPS, puisdocker compose pullavant d'arrêter les anciens conteneurs, puis swap +docker compose up -d(voir encart ci-dessous) - Health check : vérification que le conteneur répond sur le port 7010
Ordre pull avant stop — fenêtre de coupure réduite
Avant correctif (fix: pull the new image before stopping old containers on deploy, PR #23), le job arrêtait/supprimait les anciens conteneurs puis faisait docker compose pull — le service était donc down pendant tout le téléchargement de l'image en plus du redémarrage lui-même. Depuis, l'ordre est inversé :
graph LR
subgraph "Avant (coupure longue)"
A1["Stop anciens conteneurs"] --> A2["docker compose pull"] --> A3["docker compose up -d"]
end
subgraph "Après (coupure réduite)"
B1["docker compose pull<br/>(anciens conteneurs encore up)"] --> B2["Stop anciens conteneurs"] --> B3["docker compose up -d"]
end
La fenêtre de coupure réelle est réduite à la seule phase de swap des conteneurs, pas swap + téléchargement de l'image.
Panne connue : la pipeline ne démarre même pas (facturation GitHub Actions)
Un job peut échouer à démarrer avec le message "recent account payments have failed or your spending limit needs to be increased" — ce n'est pas un problème de code, ça bloque la pipeline au niveau de l'organisation GitHub entière. Seul un owner/admin de l'organisation peut corriger ça dans les paramètres de facturation GitHub (Settings → Billing). Attention : le scope OAuth admin:org d'un token ne suffit pas — il faut le rôle admin/owner réel dans l'organisation, un axe séparé du scope OAuth. Procédure de contournement manuel (déploiement direct sur le VPS sans passer par GHCR) : voir runbook.md.
Secrets GitHub Actions¶
| Secret | Description |
|---|---|
SSH_PRIVATE_KEY |
Clé SSH pour accès au VPS |
VPS_IP |
Adresse IP du VPS |
VPS_USER |
Utilisateur SSH (root) |
ENV_FILE |
Contenu complet du fichier .env |
GITHUB_TOKEN |
Token pour GHCR (automatique) |
Rollback¶
Quand faire un rollback¶
- Le déploiement a cassé une fonctionnalité critique
- Le health check échoue après déploiement
- Régression détectée en production
Procédure¶
# 1. Se connecter au VPS
ssh root@VPS_IP
# 2. Aller dans le répertoire du projet
cd /root/projects/mydocs/epidocs-api
# 3. Voir les images disponibles
docker images ghcr.io/epitechafrik/epidocs-api --format "table {{.Tag}}\t{{.CreatedAt}}"
# 4. Modifier le tag dans docker-compose.yml
# Changer ghcr.io/epitechafrik/epidocs-api:deploy par le tag voulu
nano docker-compose.yml
# 5. Redéployer
docker compose pull
docker compose up -d
# 6. Vérifier
curl -s http://127.0.0.1:7010/
docker compose logs web --tail 30
Après le rollback¶
- Investiguer la cause sur la branche
deploy - Corriger et pusher le fix
- Le pipeline redéploiera automatiquement
Backups¶
PostgreSQL¶
La base de données utilise un volume Docker externe (epidocs-api_postgres_data). Les données persistent entre les redéploiements.
Backup manuel¶
# Depuis le VPS
cd /root/projects/mydocs/epidocs-api
# Dump complet
docker compose exec db pg_dump -U postgres epidocs_db > backup_$(date +%Y%m%d).sql
# Dump compressé
docker compose exec db pg_dump -U postgres -Fc epidocs_db > backup_$(date +%Y%m%d).dump
Restauration¶
# Restaurer un dump SQL
docker compose exec -T db psql -U postgres epidocs_db < backup_YYYYMMDD.sql
# Restaurer un dump compressé
docker compose exec -T db pg_restore -U postgres -d epidocs_db backup_YYYYMMDD.dump
Backup via l'API (superadmin)¶
# Export via l'API (nécessite cookie auth_token)
curl -b "auth_token=TOKEN" https://api.mydocs.epitools.bj/admin/backup/export -o backup.json
# Import
curl -b "auth_token=TOKEN" -X POST -F "file=@backup.json" https://api.mydocs.epitools.bj/admin/backup/import
Media (templates et documents générés)¶
Les fichiers media sont dans un bind mount ./media sur le VPS. Pour les sauvegarder :