API HTTP
JSON sur HTTP, connexion par email et mot de passe, jeton porté par un en-tête ou un cookie. Le schéma OpenAPI complet est servi en mode debug.
Authentification#
Toute route est fermée par défaut. Seules POST /auth/login,
POST /auth/logout, /, /health, /ready
et /metrics répondent sans connexion.
POST /auth/login reçoit l'email dans le champ username et le mot
de passe, au format formulaire (flux OAuth2 password). Il renvoie un jeton JWT,
valable APP_ACCESS_TOKEN_EXPIRE_MINUTES (7 jours par défaut, sans
rafraîchissement). Le jeton s'envoie de deux façons :
| Transport | Clients | Détail |
En-tête Authorization: Bearer <jeton> | Mobile, scripts, Swagger | Prioritaire s'il est présent |
Cookie access_token | Front web | Posé par /auth/login : HttpOnly, SameSite=Lax, Secure hors localhost. Le navigateur l'envoie aussi pour <video> et les liens de téléchargement |
Avec le cookie, les écritures (POST, PUT, PATCH,
DELETE) exigent en plus l'en-tête X-Requested-With, de valeur
libre : c'est la protection contre les requêtes forgées depuis un autre site. Sans lui,
la réponse est 403.
TOKEN=$(curl -s localhost:8000/auth/login \
-d username=admin@example.com --data-urlencode 'password=votre-mot-de-passe' \
| jq -r .access_token)
curl -s localhost:8000/progress/formations -H "Authorization: Bearer $TOKEN"
Rôles#
| Rôle | Droits |
Apprenant (is_admin=false) | Catalogue, vidéos, documents, assistant, ses notes, sa progression et sa consommation |
Admin (is_admin=true) | Tout, plus le studio (toutes les écritures du catalogue) et la gestion des comptes |
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 à la requête suivante. Notes
et progression restent propres à chaque compte, admin compris.
| Code | Signification |
401 | Pas de jeton, jeton invalide ou expiré, compte désactivé |
403 | Connecté, mais route réservée aux admins (ou en-tête anti-CSRF manquant) |
429 | Trop d'échecs de connexion : 5 par email ou par adresse IP sur 15 minutes. En-tête Retry-After |
Avec APP_DEBUG=true, l'interface OpenAPI est servie sur
/api-docs, /api-redoc et /openapi.json. Son bouton
Authorize se connecte via /auth/login : saisir l'email dans
username. Le mode debug ne contourne pas l'authentification.
Les listes ne sont pas paginées, sauf les comptes : GET /formations renvoie le
catalogue complet, chapitres et leçons compris.
Santé et session#
| Méthode | Route | Accès | Effet |
| GET | /health | Public | {"status": "ok"} |
| GET | /ready | Public | Vérifie PostgreSQL |
| POST | /auth/login | Public | Formulaire username + password → {"access_token", "token_type", "expires_in"} et cookie. 401 au message unique, que l'email ou le mot de passe soit faux |
| POST | /auth/logout | Public | 204, efface le cookie |
| GET | /auth/me | Apprenant | {"id", "email", "full_name", "is_admin"} |
| PATCH | /auth/me/password | Apprenant | Corps {"current_password", "new_password"} ; 400 si l'actuel est faux, 422 hors 8–128 caractères |
Comptes (admin)#
| Méthode | Route | Effet |
| GET | /admin/users | ?offset=0&limit=50 (limite ≤ 200) → {"items", "total"} |
| POST | /admin/users | Corps {"email", "password", "full_name"?, "is_admin"?} ; email déjà pris → 409 |
| PATCH | /admin/users/{id} | Corps {"full_name"?, "is_admin"?, "is_active"?, "password"?} ; champ absent = inchangé |
| DELETE | /admin/users/{id} | Supprime le compte, ses notes, sa progression et sa consommation |
Un compte renvoyé contient id, email, full_name,
is_admin, is_active et created_at, jamais le mot de passe
ni son empreinte. Un admin ne peut ni se retirer le rôle admin, ni se désactiver, ni se
supprimer : 409.
| Méthode | Route | Effet |
| GET | /formations | Catalogue complet : chapitres, vidéos, documents, statuts, active_jobs |
| GET | /formations/{id} | Une formation |
| POST | /formations/{id}/ask | Corps {"question"} → {"answer", "citations"} |
Studio (admin)#
Toutes les écritures du catalogue, y compris celles des sections Vidéos
et Documents marquées « Admin ». Un apprenant reçoit 403.
| Méthode | Route | Effet |
| POST | /formations/{id}/index | 202, job rag_index_formation |
| POST | /formations | Corps {"name"} |
| PATCH | /formations/{id} | Renommer |
| DELETE | /formations/{id} | Supprimer |
| PUT | /formations/{id}/chapters/order | Corps {"chapter_ids": [...]} |
| POST | /formations/{id}/chapters | Corps {"name"} |
| PATCH | /chapters/{id} | Renommer |
| DELETE | /chapters/{id} | Supprimer |
| POST | /chapters/{id}/videos | Multipart title + file ; conversion en job si nécessaire |
| PUT | /chapters/{id}/videos/order | Corps {"video_ids": [...]} |
| PATCH | /chapters/{source}/{target}/{video_id} | Déplacer ; position ou after_video_id facultatifs |
| PATCH | /videos/{id} | JSON {"title"}, ou multipart title? + file? |
| DELETE | /videos/{id} | Supprimer |
| POST | /chapters/{id}/docs | Multipart title + file + video_id? ; 422 si extension refusée |
Vidéos#
| Méthode | Route | Accès | Effet |
| GET | /videos/{id}/stream | Apprenant | Flux avec support Range |
| GET | /videos/{id}/file | Apprenant | Fichier complet |
| GET | /videos/{id}/transcription | Apprenant | {"content"} |
| POST | /videos/{id}/transcription | Admin | 202, job transcription |
| GET | /videos/{id}/summary | Apprenant | {"summary"} |
| PUT | /videos/{id}/summary | Admin | Corps {"summary"} ; remplace le texte |
| POST | /videos/{id}/summary/generate | Admin | 202, job summary |
| POST | /videos/{id}/conversion | Admin | 202, relance ffmpeg |
Champs d'état d'une 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[] | id, kind, status (queued | running), progress 0–100, message |
Progression et notes (apprenant)#
| Méthode | Route | Effet |
| GET | /progress/formations | Progression de toutes les formations |
| GET | /progress/formation/{id} | Détail par chapitre et par vidéo |
| GET | /progress/{video_id} | Position d'une vidéo |
| POST | /progress/{video_id} | Corps {"last_position"} en secondes |
| GET | /notes/{video_id} | Notes du compte connecté sur la vidéo |
| POST | /notes/{video_id} | Corps {"timecode", "content"} |
| PUT | /notes/{note_id} | Corps {"content"} |
| DELETE | /notes/{note_id} | Supprimer |
Documents#
| Méthode | Route | Accès | Effet |
| GET | /docs/chapters/{chapter_id} | Apprenant | Documents du chapitre |
| GET | /docs/{id}/file | Apprenant | Téléchargement ; ?download=true force l'enregistrement |
| PATCH | /docs/{id} | Admin | Corps {"title"?, "video_id"?} ; video_id: null détache |
| DELETE | /docs/{id} | Admin | Supprimer |
Consommation IA (apprenant)#
| Méthode | Route | Effet |
| GET | /usage?days=30 | Tokens du compte connecté (1 à 365 jours) : period, all_time, by_kind, by_model, daily (jours UTC) |
Suivre un traitement
Après un 202, interrogez GET /formations/{id} toutes les
quelques secondes tant qu'un statut vaut processing, et affichez
active_jobs[].progress.