{% extends 'sam/layout.html.twig' %} {% block title %}SAM — Paramètres{% endblock %} {% block sam_body %}
{% include '_molecules/page-header/page-header.html.twig' with { title: 'Paramètres SAM', subtitle: 'Clé API et référence complète des endpoints disponibles', icon: 'settings' } only %}
{# ── Onglets ── #}
{# ══════════════════════════════════════════════════════════════════════ ONGLET A — ACCÈS API ══════════════════════════════════════════════════════════════════════ #} {% if activeTab == 'api-key' %} {% include 'profile/_developer.html.twig' %} {% endif %} {# ══════════════════════════════════════════════════════════════════════ ONGLET B — DOCUMENTATION ══════════════════════════════════════════════════════════════════════ #} {% if activeTab == 'docs' %}
{# ── Nav sticky — colonne indépendante hors du card ── #} {# ── Card contenu principal ── #}
{% include '_atoms/icon/icon.html.twig' with { name: 'code', size: 'md', color: 'primary' } only %}

Référence API

Base URL : {{ app.request.schemeAndHttpHost }}/api/moodlev5

{% include '_atoms/icon/icon.html.twig' with { name: 'download', size: 'sm' } only %} Collection Postman
{# ════════ AUTHENTIFICATION ════════ #}

Authentification

Toutes les routes requièrent une clé API transmise via header HTTP.

Request Headers
X-API-Key: bubul_sk_xxxxxxxxxxxx # Alternative — Authorization header Authorization: Bearer bubul_sk_xxxxxxxxxxxx
401 Unauthorized — clé manquante ou invalide. 403 Forbidden — clé valide mais accès refusé (rôle insuffisant).
{# ════════ IDENTITÉ ════════ #}

Identité

Informations sur l'utilisateur authentifié et statut Moodle.

{# GET /me #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /me Informations du compte connecté

Aucun paramètre requis.

200 OK
{ "success": true, "data": { "user": { "id": 42, "email": "vous@entreprise.com", "first_name": "Prénom", "last_name": "Nom", "roles": ["ROLE_USER"], "company": { "uuid": "550e8400-e29b-41d4...", "name": "Mon Entreprise", "has_moodle_config": true, "has_active_contract": true } } } }
{# GET /status #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /status Statut de connexion Moodle

Admins : ajouter ?company={uuid}

200 OK
{ "success": true, "data": { "status": { "configured": true, "connected": true, "moodle_url": "https://moodle.entreprise.com", "site_name": "Ma Plateforme Moodle", "site_version": "4.3.0" } } }
{# ════════ CONNEXIONS ════════ #}

Connexions

Historique des connexions des apprenants par jour/mois.

{# POST /connections/refresh #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'POST' } only %} /connections/refresh Synchroniser + retourner 12 mois

Aucun body requis. Lance une sync depuis la DB locale, dispatche un job async pour les refreshs futurs.

200 OK
{ "success": true, "inserted": 42, "data": [ { "month": "2026-01", "label": "Janv.", "count": 125 }, ... ], "total": 348, "months": 12 }
{# GET /connections/company #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /connections/company Connexions mensuelles de l'entreprise
ParamètreTypeDescriptionRequis
periodintegerPériode en mois : 3, 6 ou 12Non (défaut: 12)
companyuuidUUID entreprise (admins uniquement)Non
200 OK
{ "data": [{ "month": "2026-01", "label": "Janv.", "count": 125 }, ...], "total": 348, "months": 12, "company": "Mon Entreprise" }
{# GET /connections/user/{id} #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /connections/user/{id} Historique d'un apprenant
ParamètreTypeDescriptionRequis
idintegerID interne de l'apprenantOui (path)
daysintegerNombre de jours (max 365)Non (défaut: 90)
200 OK
{ "data": [{ "day": "2026-01-15" }, ...], "total": 28, "days": 90, "user": "Jean Dupont" }
{# ════════ ENGAGEMENT ════════ #}

Engagement

Statistiques d'engagement des apprenants (statut actif/inactif).

{# POST /engagement/refresh #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'POST' } only %} /engagement/refresh Synchroniser + retourner les stats

Synchronise les utilisateurs depuis Moodle, puis retourne les stats d'engagement.

200 OK
{ "success": true, "stats": { "total": 150, "active": 95, "inactive2": 30, "inactive10": 8, "neverConnected": 2 } }
{# GET /engagement/company #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /engagement/company Stats engagement entreprise

Mêmes stats que refresh, sans déclencher de sync. Admins : ?company={uuid}

200 OK
{ "success": true, "stats": { "total": 150, "active": 95, "inactive2": 30, "inactive10": 8, "neverConnected": 2 }, "company": "Mon Entreprise" }
{# GET /engagement/user/{id} #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /engagement/user/{id} Statut d'engagement d'un apprenant
200 OK
{ "success": true, "user": "Jean Dupont", "lastAccessAt": "2026-03-06", "daysSince": 5, "status": "active" // active | inactive2 | inactive10 | neverConnected }
{# ════════ SOUMISSIONS ════════ #}

Soumissions

Complétions et soumissions récentes des apprenants.

{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /submissions Liste paginée des soumissions
ParamètreTypeDescriptionRequis
pageintegerNuméro de pageNon (défaut: 1)
limitintegerRésultats par page (max 50)Non (défaut: 10)
userstringFiltre par nom d'apprenantNon
typestringType d'activité : quiz, assign...Non
daysintegerPériode en jours (max 365)Non (défaut: 30)
200 OK
{ "data": [{ "userId": 42, "userName": "Jean Dupont", "action": "completed", "activityName": "Quiz final", "type": "quiz", "completedAt": "2026-03-10T14:30:00+00:00", "timeAgo": "il y a 1 jour" }], "total": 143 }
{# ════════ APPRENANTS ════════ #}

Apprenants

CRUD complet sur les utilisateurs Moodle. Les lectures se font depuis la DB locale (rapide), les écritures synchronisent vers Moodle. Le paramètre {id} est le moodle_id de l'apprenant (champ moodle_id retourné dans la liste).

{# GET /users #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /users Liste paginée
ParamètreTypeDescriptionRequis
searchstringRecherche par email ou nomNon
pageintegerNuméro de pageNon (défaut: 1)
per_pageintegerRésultats par page (max 100)Non (défaut: 20)
200 OK
{ "success": true, "data": { "items": [{ "moodle_id": 42, "username": "jdupont", "email": "jean@entreprise.com", "firstname": "Jean", "lastname": "Dupont", "fullname": "Jean Dupont", "suspended": false, "syncedAt": "2026-03-10T08:00:00+00:00", "createdAt": "2025-01-15T10:00:00+00:00", "updatedAt": "2026-03-10T08:00:00+00:00" }], "pagination": { "total": 150, "page": 1, "per_page": 20, "total_pages": 8, "has_more": true } } }
{# GET /users/{id} #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /users/{moodleId} Détail d'un apprenant

Retourne l'objet user complet.

{# POST /users #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'POST' } only %} /users Créer un apprenant
Champ JSONTypeDescriptionRequis
usernamestringIdentifiant Moodle uniqueOui
emailstringAdresse emailOui
firstNamestringPrénom (aussi accepté : first_name)Oui
lastNamestringNom (aussi accepté : last_name)Oui
passwordstringMot de passe (généré si absent)Non
authstringMéthode auth MoodleNon (défaut: manual)
Réponse : 201 Created avec l'objet user.
{# PUT /users/{id} #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'PUT' } only %} /users/{moodleId} Mettre à jour un apprenant

Champs acceptés (tous optionnels) : email, firstName, lastName, password, suspended (boolean).

{# DELETE /users/{id} #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'DELETE' } only %} /users/{moodleId} Supprimer (désactiver sur Moodle)

Réponse : 200 OK avec "message": "Utilisateur supprimé avec succès".

{# POST /users/{id}/toggle #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'POST' } only %} /users/{moodleId}/toggle Activer / Désactiver

Bascule l'état suspended de l'utilisateur. Retourne l'objet user mis à jour.

{# GET /users/{id}/courses #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /users/{moodleId}/courses Cours de l'apprenant (temps réel)

Données récupérées en direct depuis Moodle. Retourne la liste des cours auxquels l'utilisateur est inscrit.

{# POST /users/sync #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'POST' } only %} /users/sync Synchroniser depuis Moodle
200 OK
{ "success": true, "data": { "stats": { "total": 150, "created": 5, "updated": 20, "deleted": 0 } }, "message": "Synchronisation terminée : 150 utilisateurs" }
{# ════════ COURS ════════ #}

Cours

Accès aux cours Moodle et à leurs contenus (sections, activités, cohortes).

{% for endpoint in [ { method: 'GET', path: '/courses', desc: 'Liste paginée (params: search, page, per_page)' }, { method: 'GET', path: '/courses/{id}', desc: 'Détail d\'un cours' }, { method: 'GET', path: '/courses/{id}/users', desc: 'Apprenants inscrits avec progression (paginé)' }, { method: 'GET', path: '/courses/{id}/detail', desc: 'Détail complet : sections, activités, cohortes, stats' }, { method: 'GET', path: '/courses/{id}/sections', desc: 'Sections du cours' }, { method: 'GET', path: '/courses/{id}/cohorts', desc: 'Cohortes inscrites au cours (cache local)' } ] %}
{% include 'sam/_api_method_badge.html.twig' with { method: endpoint.method } only %} {{ endpoint.path|replace({'{id}': '{id}'})|raw }} {{ endpoint.desc }}
{% endfor %}
{# ════════ INSCRIPTIONS ════════ #}

Inscriptions

Inscrire ou désinscrire manuellement un apprenant à un cours. Rôle ROLE_COMPANY_MANAGER requis.

{# POST enroll #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'POST' } only %} /courses/{courseId}/enrollments Inscrire un apprenant
Champ JSONTypeDescriptionRequis
userIdintegerID Moodle de l'apprenant (aussi : user_id)Oui
roleIdintegerID rôle Moodle (aussi : role_id)Non (défaut: 5 = Étudiant)

Réponse : 201 Created.

{# DELETE unenroll #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'DELETE' } only %} /courses/{courseId}/enrollments/{userId} Désinscrire un apprenant
{# GET search #}
{% include 'sam/_api_method_badge.html.twig' with { method: 'GET' } only %} /courses/{courseId}/enrollments/search Rechercher des apprenants inscriptibles

Paramètres : q (string, min 2 chars, requis), limit (max 50, défaut 20). Retourne les utilisateurs non encore inscrits.

{# ════════ COHORTES ════════ #}

Cohortes

Gestion complète des cohortes Moodle et de leurs membres.

{% for endpoint in [ { method: 'GET', path: '/cohorts', desc: 'Liste paginée (params: page, per_page)' }, { method: 'POST', path: '/cohorts', desc: 'Créer une cohorte (body: name*, idNumber, description)' }, { method: 'GET', path: '/cohorts/{id}', desc: 'Détail d\'une cohorte' }, { method: 'PUT', path: '/cohorts/{id}', desc: 'Mettre à jour (body: name, idNumber, description)' }, { method: 'DELETE', path: '/cohorts/{id}', desc: 'Supprimer une cohorte' }, { method: 'GET', path: '/cohorts/{id}/members', desc: 'Liste des membres (paginé)' }, { method: 'POST', path: '/cohorts/{id}/members', desc: 'Ajouter des membres (body: userIds[])' }, { method: 'DELETE', path: '/cohorts/{id}/members', desc: 'Retirer des membres (body: userIds[])' }, { method: 'GET', path: '/cohorts/{id}/courses', desc: 'Cours accessibles via la cohorte (cache local)' } ] %}
{% include 'sam/_api_method_badge.html.twig' with { method: endpoint.method } only %} {{ endpoint.path|replace({'{id}': '{id}'})|raw }} {{ endpoint.desc }}
{% endfor %}
Exemple — POST /cohorts/{id}/members
// Request body { "userIds": [42, 43, 99] } // Response — 200 OK { "added": [42, 43], "already_members": [], "not_found": [99] } // DELETE /cohorts/{id}/members — réponse 200 OK { "removed": [42], "not_members": [43], "not_found": [] }
Exemple — POST /cohorts
// Request body — idNumber et id_number sont tous les deux acceptés { "name": "Promo 2026 - Groupe A", "idNumber": "PROMO-2026-A", "description": "Groupe A de la promotion 2026" } // Response — 201 Created { "success": true, "data": { "cohort": { "id": 7, "moodle_id": 12, "name": "Promo 2026 - Groupe A", "idnumber": "PROMO-2026-A", "description": "Groupe A de la promotion 2026", "members_count": 0 } }, "message": "Cohorte créée avec succès" }
{# ════════ PROGRESSION ════════ #}

Progression

Données de progression lues depuis la DB locale (SamMoodleCompletion + SamQuizAttempt). Synchroniser au préalable.

{% for endpoint in [ { method: 'GET', path: '/progress/users/{userId}', desc: 'Progression globale d\'un apprenant (tous ses cours)' }, { method: 'GET', path: '/progress/users/{userId}/courses/{courseId}', desc: 'Progression détaillée dans un cours' }, { method: 'GET', path: '/progress/courses/{courseId}', desc: 'Progression de tous les apprenants d\'un cours (paginé)' }, { method: 'GET', path: '/progress/courses/{courseId}/sections/{sectionId}', desc: 'Progression d\'une section (param optionnel: user_id)' }, { method: 'GET', path: '/progress/activities/{activityId}', desc: 'Métadonnées d\'une activité' }, { method: 'GET', path: '/progress/quizzes/{quizId}', desc: 'Tentatives d\'un quiz (param optionnel: user_id)' }, { method: 'GET', path: '/progress/quizzes/{quizId}/attempts/{attemptId}', desc: 'Détail d\'une tentative de quiz' } ] %}
{% include 'sam/_api_method_badge.html.twig' with { method: endpoint.method } only %} {{ endpoint.path|replace({'{userId}': '{userId}', '{courseId}': '{courseId}', '{sectionId}': '{sectionId}', '{activityId}': '{activityId}', '{quizId}': '{quizId}', '{attemptId}': '{attemptId}'})|raw }} {{ endpoint.desc }}
{% endfor %}
Exemple — GET /progress/users/42/courses/7
{ "success": true, "data": { "user_id": 42, "course_id": 7, "completion_rate": 68.5, "completed_activities": 11, "total_activities": 16, "last_activity_at": "2026-03-09T10:15:00+00:00" } }
{# ════════ ACTIVITÉS & SECTIONS ════════ #}

Activités & Sections

Accès unitaire à une activité ou section. Le paramètre course_id est obligatoire en query string (limitation API Moodle — une activité n'est identifiable qu'avec son cours).

{% for endpoint in [ { method: 'GET', path: '/activities/{id}?course_id={cid}', desc: 'Détail d\'une activité' }, { method: 'GET', path: '/activities/{id}/completion?course_id={cid}', desc: 'Complétion de l\'activité par tous les apprenants inscrits' }, { method: 'GET', path: '/sections/{id}?course_id={cid}', desc: 'Section avec ses activités' }, { method: 'GET', path: '/sections/{id}/activities?course_id={cid}', desc: 'Activités d\'une section' } ] %}
{% include 'sam/_api_method_badge.html.twig' with { method: endpoint.method } only %} {{ endpoint.path|replace({'{id}': '{id}', '{cid}': '{courseId}'})|raw }} {{ endpoint.desc }}
{% endfor %}
Exemple — GET /activities/18/completion?course_id=7
{ "success": true, "data": { "activity": { "id": 18, "name": "Quiz final", "type": "quiz" }, "completions": [{ "user_id": 42, "fullname": "Jean Dupont", "state": 1, "completed_at": "2026-03-10T14:30:00+00:00" }], "stats": { "total_users": 30, "completed": 24, "not_completed": 6, "completion_rate": 80.0 } } }
{# ════════ ERREURS ════════ #}

Format des erreurs

Toutes les erreurs retournent le même format.

{% for code, desc in { '400 Bad Request': 'Paramètre manquant ou invalide', '401 Unauthorized': 'Clé API manquante ou invalide', '403 Forbidden': 'Accès refusé (rôle insuffisant)', '404 Not Found': 'Ressource introuvable', '500 Internal Server Error': 'Erreur serveur interne' } %}
{{ code }} {{ desc }}
{% endfor %}
4xx / 5xx Error
{ "success": false, "error": "Ressource introuvable", "code": 404, "details": {} }
{# /divide-y #}
{# /card #}
{# /flex colonnes #} {% endif %}{# /docs #} {# ══════════════════════════════════════════════════════════════════════ ONGLET C — LOGS (admin uniquement) ══════════════════════════════════════════════════════════════════════ #} {% if activeTab == 'logs' %}
{% include '_atoms/icon/icon.html.twig' with { name: 'activity', size: 'md', color: 'default' } only %}

Logs API

Les 50 derniers appels enregistrés sur les routes SAM

{% if recentLogs is empty %}
Aucun log pour le moment.
{% else %} {% for log in recentLogs %} {% endfor %}
Date Méthode Endpoint Utilisateur Email Statut
{{ log.createdAt|format_datetime('short', 'short', locale='fr') }} {% include 'sam/_api_method_badge.html.twig' with { method: log.method } only %} {{ log.path }} {{ log.fullName ?? '—' }} {{ log.email ?? '—' }} {% include '_atoms/badge/badge.html.twig' with { label: log.statusCode, variant: log.statusCode < 300 ? 'success' : (log.statusCode < 500 ? 'warning' : 'error'), size: 'sm' } only %}
{% endif %}
{% endif %}{# /logs #} {% endblock %}