Toute route est fermée par défaut. Seules POST /auth/login, POST /auth/logout,
/, /health, /ready et /metrics sont publiques.
Le jeton (JWT HS256, sub = id utilisateur, durée APP_ACCESS_TOKEN_EXPIRE_MINUTES)
est accepté de deux façons :
| Transport | Clients | Détail |
|---|---|---|
Authorization: Bearer <token> |
mobile, Swagger, scripts | prioritaire si présent |
cookie access_token |
web | posé par /auth/login : HttpOnly, SameSite=Lax, Secure hors localhost |
Avec le cookie, les requêtes d'écriture (POST, PUT, PATCH, DELETE) doivent aussi
porter le header X-Requested-With (valeur libre, ex. XMLHttpRequest) : protection CSRF.
Sans lui → 403. Les lectures (<video src>, liens de téléchargement) n'en ont pas besoin.
Le rôle n'est pas lu dans le jeton : le compte est relu en base à chaque requête, un retrait du rôle admin ou une désactivation prend donc effet immédiatement.
| Rôle | Droits |
|---|---|
| anonyme | routes publiques uniquement |
apprenant (is_admin=false) |
catalogue, vidéos, documents, RAG, ses notes, sa progression, sa consommation |
admin (is_admin=true) |
tout, plus le studio (écritures) et /admin/users |
Codes : 401 = pas de jeton, jeton invalide ou expiré, compte désactivé ;
403 = rôle insuffisant (ou header CSRF manquant) ; 429 = trop d'échecs de connexion
(5 par email ou par IP sur 15 min, header Retry-After).
Notes et progression restent cloisonnées par utilisateur, admin compris.
Premier admin : créé au démarrage depuis APP_FIRST_ADMIN_EMAIL / APP_FIRST_ADMIN_PASSWORD
s'il n'existe pas (idempotent, le mot de passe n'est jamais réécrit). Autres admins :
uv run e-learning-cli create-admin --email <email> ou POST /admin/users.
Les réponses catalogue / studio exposent sur chaque vidéo :
| Champ | Valeurs |
|---|---|
kind |
video | audio |
processing_status |
ready | processing | failed |
transcription_status |
none | processing | ready | failed |
summary_status |
none | processing | ready | failed |
active_jobs |
liste des jobs actifs (queued / running) |
{
"id": "<uuid>",
"kind": "media_conversion|transcription|summary|rag_index_video|rag_index_formation",
"status": "queued|running",
"progress": 0,
"message": "…"
}progress : 0..100. Poller GET /formations (ou la formation) toutes les ~3 s tant qu’un statut est processing pour suivre l’évolution.
Les jobs terminés (succeeded / failed) ne figurent plus dans active_jobs ; les colonnes *_status restent la projection métier.
GET /→{"message": "health ok"}GET /health→{"status": "ok"}GET /ready→ vérifie Postgres
POST /auth/login(public) —application/x-www-form-urlencodedusername(email) +password→{"access_token", "token_type": "bearer", "expires_in"}+ cookieaccess_token.401au message unique (email ou mot de passe), compte désactivé →401,429si bloqué.POST /auth/logout(public) →204, efface le cookieGET /auth/me→{"id", "email", "full_name", "is_admin"}PATCH /auth/me/passwordbody{"current_password", "new_password"}→204(400si mot de passe actuel faux,422si le nouveau fait moins de 8 ou plus de 128 caractères)
GET /admin/users?offset=0&limit=50(limit ≤ 200) →{"items": [UserResponse], "total"}POST /admin/usersbody{"email", "password", "full_name"?, "is_admin"?}→201; email déjà pris →409PATCH /admin/users/{id}body{"full_name"?, "is_admin"?, "is_active"?, "password"?}— champ absent = inchangé,full_name: nullefface le nomDELETE /admin/users/{id}→204, supprime aussi ses notes, sa progression et sa consommation
UserResponse : {"id", "email", "full_name", "is_admin", "is_active", "created_at"} (jamais de mot
de passe ni de hash). Un admin ne peut ni se retirer le rôle admin, ni se désactiver, ni se
supprimer lui-même → 409.
GET /formations→ catalogue (idUUID,name,slug,chapters[].videos[]/chapters[].documents[]avecposition, statuts +active_jobs)GET /formations/{formation_id}POST /formations/{formation_id}/askbody{"question"}→{"answer", "citations": [{"video_id","title","source","excerpt"}]}(RAG)
Toutes les routes de cette section et les écritures listées sous Videos et Documents
sont réservées aux admins (403 pour un apprenant).
-
POST /formations/{formation_id}/index→202indexation RAG formation (jobrag_index_formation) -
POST /formationsbody{"name"} -
PATCH /formations/{id}body{"name"} -
DELETE /formations/{id} -
POST /formations/{id}/chaptersbody{"name"} -
PATCH /chapters/{id}body{"name"} -
DELETE /chapters/{id} -
POST /chapters/{id}/videosmultiparttitle+file— si conversion nécessaire :processing_status=processing+ jobmedia_conversion(queue RabbitMQ) -
PATCH /videos/{id}JSON{"title"}ou multiparttitle?+file?(remplacement) -
DELETE /videos/{id} -
PUT /chapters/{id}/videos/orderbody{"video_ids": [...]}— met à jourpositionen base -
PUT /formations/{id}/chapters/orderbody{"chapter_ids": [...]}— réordonne les chapitres (positionDB, slugs FS inchangés) -
PATCH /chapters/{source}/{target}/{video_id}body optionnelposition/after_video_id
GET /videos/{id}/stream(Range)GET /videos/{id}/fileGET /videos/{id}/summary→{"summary"}PUT /videos/{id}/summarybody{"summary"}(admin)POST /videos/{id}/summary/generate(admin) →202+VideoResponse(summary_status=processing, jobsummary)GET /videos/{id}/transcription→{"content"}POST /videos/{id}/transcription(admin) →202+VideoResponse(transcription_status=processing, jobtranscription)POST /videos/{id}/conversion(admin) →202+VideoResponse(reprise / relance ffmpeg, jobmedia_conversion)
Après transcription ou résumé réussis, un job rag_index_video est enchaîné automatiquement (best-effort).
GET /progress/formationsGET /progress/formation/{formation_id}GET /progress/{video_id}POST /progress/{video_id}body{"last_position"}
GET /notes/{video_id}POST /notes/{video_id}body{"timecode","content"}PUT /notes/{note_id}body{"content"}DELETE /notes/{note_id}
Extensions acceptées à l'upload : .pdf, .md, .txt, .csv, .doc, .docx, .ppt, .pptx, .xls, .xlsx, .odt, .ods, .odp, .png, .jpg, .jpeg, .webp, .gif, .svg (pas de fichier sans extension).
GET /docs/chapters/{chapter_id}POST /chapters/{id}/docsmultiparttitle+file+video_id?(admin) —422si extension refuséePATCH /docs/{document_id}body{"title"?,"video_id"?}—video_id: nulldétache (admin)DELETE /docs/{document_id}(admin)GET /docs/{document_id}/file
/api-docs,/api-redoc,/openapi.json— bouton Authorize branché sur/auth/login(saisir l'email dans le champusername)