Skip to content

Référence API REST

English version
Retour au sommaire


Conventions générales

URL de base

http://localhost:3000   (développement)
https://[votre-domaine] (production)

Tous les endpoints sont sous le préfixe /api/.

Format des réponses

Succès :

{ "success": true, "data": { ... } }

Erreur :

{ "error": "Message d'erreur explicite" }

Codes HTTP

Code Signification
200 Succès (GET, PATCH, DELETE)
201 Ressource créée (POST)
400 Requête invalide (champs manquants, format incorrect)
401 Non authentifié
403 Accès interdit (ressource d'un autre utilisateur)
404 Ressource non trouvée
429 Trop de requêtes (rate limiting)
500 Erreur serveur interne

Authentification

Trois modes d'authentification coexistent selon l'endpoint :

Type Mécanisme Endpoints
Admin Cookie auth-token (JWT HS256, 7 jours) /api/admin/*, /api/auth/*
Speaker Code dans le body (lookup DB, session localStorage) /api/speaker/login, /api/audio/upload
Public Aucune /api/languages, /api/languages/[id]/content, /api/statistics/track, /api/quiz/*, /api/contact, /api/health/*

Santé (Health)

GET /api/health/live

Liveness probe — vérifie uniquement que le process Node.js répond. Utilisé par le HEALTHCHECK Docker. Ne dépend d'aucun service externe (pas de connexion BDD).

curl http://localhost:3000/api/health/live

Réponse 200 :

{ "status": "ok" }

⚠️ Cet endpoint répond toujours 200 tant que Next.js tourne. Il ne vérifie PAS que PostgreSQL ou MinIO sont disponibles.


Authentification Admin

POST /api/auth/login

Authentifie un administrateur. Retourne un cookie JWT auth-token valide 7 jours.

curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@racines.app","password":"motdepasse123"}' \
  -c cookies.txt

Body :

{
  "email": "admin@racines.app",
  "password": "motdepasse123"
}

Réponse 200 :

{
  "success": true,
  "message": "Authentification réussie",
  "admin": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "admin@racines.app"
  }
}

Le cookie auth-token est positionné dans les headers Set-Cookie de la réponse (HttpOnly, Secure, SameSite=Lax).

Erreurs : | Cas | Code | Message | |---|---|---| | Champs manquants | 400 | "Email et password requis" | | Email invalide | 400 | "Email invalide" | | Mot de passe < 8 caractères | 400 | "Le password doit avoir au moins 8 caractères" | | Identifiants incorrects | 401 | "Email ou password incorrect" |

Note sécurité : En cas d'email inexistant ou de mauvais mot de passe, la réponse est identique (pas de fuite d'information sur l'existence du compte).


GET /api/auth/login

Vérifie si le cookie de session admin est valide. Utilisé par le frontend pour détecter une session expirée.

curl http://localhost:3000/api/auth/login \
  -b cookies.txt

Réponse si authentifié :

{
  "authenticated": true,
  "admin": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "admin@racines.app"
  }
}

Réponse si non authentifié :

{ "authenticated": false }

POST /api/auth/logout

Invalide le cookie de session admin (cookie mis à expiration immédiate).

curl -X POST http://localhost:3000/api/auth/logout \
  -b cookies.txt

Réponse 200 :

{ "success": true }

Authentification Speaker

POST /api/speaker/login

Vérifie le code d'accès d'un locuteur. La session est gérée côté client dans localStorage (aucun cookie serveur).

curl -X POST http://localhost:3000/api/speaker/login \
  -H "Content-Type: application/json" \
  -d '{"accessCode":"RACINES-ABCDE-2025"}'

Body :

{
  "accessCode": "RACINES-ABCDE-2025"
}

Le code est normalisé en majuscules côté serveur avant la vérification en base de données.

Réponse 200 :

{
  "success": true,
  "speaker": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Amadou Diallo",
    "access_code": "RACINES-ABCDE-2025",
    "language_id": "uuid-langue",
    "language_code": "pul",
    "language_name": "Pular"
  }
}

Erreurs : | Cas | Code | Message | |---|---|---| | Code manquant | 400 | "Code d'accès requis" | | Code invalide ou speaker inactif | 401 | "Code d'accès invalide ou speaker inactif" |


Langues

GET /api/languages

Récupère la liste des langues actives. Peut être filtré par code.

# Toutes les langues actives
curl http://localhost:3000/api/languages

# Filtrer par code
curl "http://localhost:3000/api/languages?code=pul"

Query params : | Param | Type | Description | |---|---|---| | code | string | Optionnel — filtre par code langue (pul, wol, etc.) |

Réponse 200 :

{
  "success": true,
  "languages": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Pular",
      "code": "pul",
      "access_code": "RACINES-XXXXX-2025",
      "is_active": true,
      "display_order": 1,
      "primary_color": "#D97706",
      "icon_url": "https://[MINIO_PUBLIC_URL]/audios/icons/pul.png",
      "created_at": "2025-01-15T10:00:00Z"
    }
  ]
}

Note : speaker_access_code et speaker_name ne sont PAS retournés par cet endpoint public (informations réservées aux admins).


POST /api/languages

Auth admin requise.

Crée une nouvelle langue avec son premier locuteur.

curl -X POST http://localhost:3000/api/languages \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "name": "Wolof",
    "code": "wol",
    "access_code": "RACINES-WOLOF-2025",
    "speaker_access_code": "RACINES-SPK-WOL-2025",
    "speaker_name": "Fatou Diop",
    "primary_color": "#2563EB",
    "is_active": false,
    "display_order": 2
  }'

Body :

{
  "name": "Wolof",
  "code": "wol",
  "access_code": "RACINES-WOLOF-2025",
  "speaker_access_code": "RACINES-SPK-WOL-2025",
  "speaker_name": "Fatou Diop",
  "primary_color": "#2563EB",
  "is_active": false,
  "display_order": 2
}

Réponse 201 :

{
  "success": true,
  "data": {
    "id": "uuid-nouveau",
    "name": "Wolof",
    "code": "wol",
    "access_code": "RACINES-WOLOF-2025",
    "is_active": false,
    "display_order": 2,
    "primary_color": "#2563EB",
    "created_at": "2026-01-15T10:00:00Z"
  }
}

PATCH /api/languages/[id]

Auth admin requise.

Met à jour une ou plusieurs propriétés d'une langue. Tous les champs sont optionnels.

curl -X PATCH http://localhost:3000/api/languages/550e8400-e29b-41d4-a716-446655440000 \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{"is_active": true}'

Body (tous les champs optionnels) :

{
  "name": "Pular (Fuuta Tooro)",
  "is_active": true,
  "primary_color": "#B45309",
  "display_order": 1,
  "speaker_name": "Amadou Diallo",
  "speaker_access_code": "RACINES-SPK-NEW-2025",
  "icon_url": "https://[MINIO]/audios/icons/pul-v2.png"
}

Réponse 200 :

{
  "success": true,
  "data": { /* langue mise à jour complète */ }
}

DELETE /api/languages/[id]

Auth admin requise.

Supprime une langue et toutes ses dépendances en cascade (cartes, items, speakers, statistiques). Action irréversible. Les fichiers audio dans MinIO ne sont pas supprimés.

curl -X DELETE http://localhost:3000/api/languages/550e8400-e29b-41d4-a716-446655440000 \
  -b cookies.txt

Réponse 200 :

{
  "success": true,
  "message": "Langue supprimée avec succès"
}

POST /api/languages/verify-code

Vérifie qu'un code d'accès joueur correspond à une langue donnée. Utilisé lors du déverrouillage d'une langue.

curl -X POST http://localhost:3000/api/languages/verify-code \
  -H "Content-Type: application/json" \
  -d '{"languageId":"uuid-langue","code":"RACINES-XXXXX-2025"}'

Body :

{
  "languageId": "550e8400-e29b-41d4-a716-446655440000",
  "code": "RACINES-XXXXX-2025"
}

Le code est normalisé en majuscules côté serveur avant la comparaison.

Réponse 200 :

{ "valid": true }

ou

{ "valid": false }

Cet endpoint ne révèle jamais si la langue existe ou non. Une langue inconnue renvoie simplement { "valid": false }.


GET /api/languages/[id]/content

Récupère le contenu complet d'une langue pour le téléchargement offline (IndexedDB). Retourne la langue, toutes ses cartes et tous ses items en une seule requête.

curl "http://localhost:3000/api/languages/550e8400-e29b-41d4-a716-446655440000/content"

Réponse 200 :

{
  "success": true,
  "language": {
    "id": "uuid",
    "name": "Pular",
    "code": "pul",
    "primary_color": "#D97706",
    "icon_url": "..."
  },
  "cards": [
    {
      "id": "uuid-carte",
      "language_id": "uuid",
      "type": "word",
      "card_number": 1,
      "theme": "Famille"
    }
  ],
  "items": [
    {
      "id": "uuid-item",
      "card_id": "uuid-carte",
      "position": 1,
      "original_text": "Baaba",
      "translation": "Père",
      "audio_url": "https://[CDN]/audios/pul/uuid-item/audio.mp3",
      "lot_number": null,
      "item_type": null
    }
  ]
}

Cet endpoint est utilisé exclusivement pour le téléchargement offline. Le contenu est stocké dans IndexedDB v3.


Cartes

POST /api/cards

Auth admin requise.

Crée une nouvelle carte pour une langue.

curl -X POST http://localhost:3000/api/cards \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "language_id": "uuid-langue",
    "type": "word",
    "card_number": 81,
    "theme": "Animaux"
  }'

Body :

{
  "language_id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "word",
  "card_number": 81,
  "theme": "Animaux"
}

type doit être "word" ou "phrase".

Réponse 201 :

{
  "success": true,
  "data": {
    "id": "uuid-nouvelle-carte",
    "language_id": "uuid-langue",
    "type": "word",
    "card_number": 81,
    "theme": "Animaux",
    "created_at": "2026-01-15T10:00:00Z"
  }
}

POST /api/cards/import

Auth admin requise.

Import en masse via fichier Excel (.xlsx). Crée les cartes et leurs items depuis un fichier structuré.

curl -X POST http://localhost:3000/api/cards/import \
  -b cookies.txt \
  -F "file=@contenu_pular.xlsx" \
  -F "languageId=uuid-langue" \
  -F "type=word"

Form data : | Champ | Type | Description | |---|---|---| | file | File | Fichier .xlsx | | languageId | string | UUID de la langue | | type | string | "word" ou "phrase" |

Pour la structure exacte du fichier Excel, voir 06-import-excel.md.

Réponse 200 :

{
  "success": true,
  "cardsCreated": 80,
  "itemsCreated": 480
}

Items

POST /api/items

Auth admin requise.

Crée un ou plusieurs items. Supporte l'insertion individuelle ou en masse.

Insertion individuelle :

curl -X POST http://localhost:3000/api/items \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "card_id": "uuid-carte",
    "position": 1,
    "original_text": "Baaba",
    "translation": "Père"
  }'

Insertion en masse :

curl -X POST http://localhost:3000/api/items \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "items": [
      {"card_id": "uuid-carte", "position": 1, "original_text": "Baaba", "translation": "Père"},
      {"card_id": "uuid-carte", "position": 2, "original_text": "Yaye", "translation": "Mère"},
      {"card_id": "uuid-carte", "position": 3, "original_text": "Tonton", "translation": "Oncle"},
      {"card_id": "uuid-carte", "position": 4, "original_text": "Tata", "translation": "Tante"},
      {"card_id": "uuid-carte", "position": 5, "original_text": "Aan", "translation": "Sœur aînée"},
      {"card_id": "uuid-carte", "position": 6, "original_text": "Kaaño", "translation": "Frère cadet"}
    ]
  }'

Champs du body : | Champ | Type | Requis | Description | |---|---|---|---| | card_id | string | ✅ | UUID de la carte parente | | position | integer | ✅ | 1–6 pour mots, 1–4 pour phrases | | original_text | string | ✅ | Texte en langue africaine | | translation | string | ✅ | Traduction en français | | audio_url | string | ❌ | URL vers MinIO (null par défaut) | | lot_number | integer | ❌ | 1 ou 2 (phrases uniquement) | | item_type | string | ❌ | "question" ou "answer" (phrases uniquement) |

Réponse 201 (insertion en masse) :

{
  "success": true,
  "data": [
    {
      "id": "uuid-item-1",
      "card_id": "uuid-carte",
      "position": 1,
      "original_text": "Baaba",
      "translation": "Père",
      "audio_url": null,
      "lot_number": null,
      "item_type": null,
      "created_at": "2026-01-15T10:00:00Z"
    }
  ]
}

PATCH /api/items/[id]

Auth admin requise.

Met à jour un item. Tous les champs sont optionnels.

curl -X PATCH http://localhost:3000/api/items/uuid-item \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{"original_text": "Baaba (correction)", "translation": "Père (formel)"}'

Body :

{
  "original_text": "Baaba (correction)",
  "translation": "Père (formel)",
  "audio_url": "https://[CDN]/audios/pul/uuid-item/audio.mp3"
}

Réponse 200 :

{
  "success": true,
  "data": { /* item mis à jour */ }
}

Locuteurs (Speakers)

POST /api/speakers

Auth admin requise.

Crée un nouveau locuteur pour une langue.

curl -X POST http://localhost:3000/api/speakers \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{
    "language_id": "uuid-langue",
    "name": "Mariama Baldé",
    "access_code": "RACINES-SPK-MRB-2025",
    "is_active": true
  }'

Body :

{
  "language_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Mariama Baldé",
  "access_code": "RACINES-SPK-MRB-2025",
  "is_active": true
}

Réponse 201 :

{
  "success": true,
  "data": {
    "id": "uuid-speaker",
    "language_id": "uuid-langue",
    "name": "Mariama Baldé",
    "access_code": "RACINES-SPK-MRB-2025",
    "is_active": true,
    "created_at": "2026-01-15T10:00:00Z"
  }
}

Audio

POST /api/audio/upload

Auth speaker requise (code passé dans les headers ou cookie de session).

Upload un fichier audio pour un item. Le fichier est converti automatiquement en MP3 mono 96kbps via FFmpeg, puis stocké dans MinIO.

curl -X POST http://localhost:3000/api/audio/upload \
  -H "X-Speaker-Code: RACINES-SPK-MRB-2025" \
  -F "audio=@enregistrement.wav" \
  -F "itemId=uuid-item"

Form data : | Champ | Type | Description | |---|---|---| | audio | File | Fichier audio (MP3, WAV, WebM, OGG, M4A, FLAC) | | itemId | string | UUID de l'item cible |

Limites : - Taille maximale : 50 MB - Formats acceptés : MP3, WAV, WebM, OGG, M4A, FLAC

Traitement FFmpeg automatique : - Conversion vers MP3 - Bitrate 96 kbps - Mono (1 canal) - 44 100 Hz sample rate - Normalisation du volume (EBU R128 loudnorm)

Clé MinIO générée : [code_langue]/[uuid_item]/audio.mp3

Réponse 200 :

{
  "success": true,
  "audioUrl": "https://[MINIO_PUBLIC_URL]/audios/pul/uuid-item/audio.mp3"
}

La colonne items.audio_url est mise à jour automatiquement en base de données.

Erreurs : | Cas | Code | Message | |---|---|---| | Speaker non authentifié | 401 | "Authentification requise" | | Fichier trop volumineux | 400 | "Fichier trop volumineux (max 50 MB)" | | Format non supporté | 400 | "Format audio non supporté" | | Item non trouvé | 404 | "Item non trouvé" |


GET /api/audio/serve/[[...path]]

Proxy MinIO — utilisé uniquement en développement local ou quand AUDIO_CDN_URL est absent.

curl "http://localhost:3000/api/audio/serve/pul/uuid-item/audio.mp3"

Protection path traversal : Les chemins contenant .. ou commençant par / sont rejetés avec une erreur 400.

Réponse 200 :
Stream binaire audio/mpeg depuis MinIO.

En production, les audios sont servis directement depuis le CDN MinIO (AUDIO_CDN_URL) sans passer par ce proxy. Voir 05-audio-minio.md.


Statistiques

POST /api/statistics/track

Enregistre un événement utilisateur. Crée ou incrémente une entrée journalière dans la table statistics (upsert par date).

curl -X POST http://localhost:3000/api/statistics/track \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "play_audio",
    "languageId": "uuid-langue",
    "cardId": "uuid-carte",
    "itemId": "uuid-item"
  }'

Body :

{
  "eventType": "play_audio",
  "languageId": "550e8400-e29b-41d4-a716-446655440000",
  "cardId": "uuid-carte",
  "itemId": "uuid-item"
}

Types d'événements :

eventType Déclencheur cardId itemId
unlock_language Déverrouillage d'une langue null null
view_card Affichage d'une carte requis null
play_audio Lecture d'un audio requis requis

Réponse 200 :

{
  "success": true,
  "count": 42
}

count = nombre d'occurrences cumulées pour ce jour.

Comportement offline : Les événements sont mis en file d'attente dans IndexedDB (statistics-queue) quand le réseau est indisponible, puis synchronisés automatiquement à la reconnexion.


GET /api/statistics/by-language

Auth admin requise.

Récupère les statistiques agrégées par langue pour le tableau de bord.

curl "http://localhost:3000/api/statistics/by-language?from=2026-01-01&to=2026-01-31" \
  -b cookies.txt

Query params : | Param | Type | Description | |---|---|---| | from | string | Format YYYY-MM-DD (optionnel) | | to | string | Format YYYY-MM-DD (optionnel) |

Réponse 200 :

[
  {
    "language_id": "uuid",
    "language_name": "Pular",
    "language_code": "pul",
    "unlock_count": 245,
    "view_count": 1830,
    "audio_count": 956
  }
]

GET /api/statistics/top-cards

Auth admin requise.

Récupère les cartes les plus consultées.

curl "http://localhost:3000/api/statistics/top-cards?languageId=uuid&limit=10" \
  -b cookies.txt

Query params : | Param | Type | Description | |---|---|---| | languageId | string | Optionnel — filtre par langue | | limit | integer | Nombre de résultats (défaut : 10) | | from | string | Format YYYY-MM-DD (optionnel) | | to | string | Format YYYY-MM-DD (optionnel) |

Réponse 200 :

[
  {
    "card_id": "uuid",
    "card_number": 5,
    "type": "word",
    "theme": "Famille",
    "language_name": "Pular",
    "view_count": 312,
    "audio_count": 187
  }
]

Quiz (Révision QCM)

POST /api/quiz/start

Démarre une session de quiz. Crée une entrée dans quiz_sessions avec completed_at = null.

curl -X POST http://localhost:3000/api/quiz/start \
  -H "Content-Type: application/json" \
  -d '{
    "languageId": "uuid-langue",
    "categorySlug": "famille",
    "categoryName": "Famille",
    "totalQuestions": 15
  }'

Body :

{
  "languageId": "550e8400-e29b-41d4-a716-446655440000",
  "categorySlug": "famille",
  "categoryName": "Famille",
  "totalQuestions": 15
}

totalQuestions : 15, 18 ou 20 selon le nombre d'items disponibles dans la catégorie (calculé par getQuestionCount()).

Réponse 201 :

{
  "sessionId": "uuid-session"
}

POST /api/quiz/complete

Enregistre le score final d'une session de quiz. Met à jour quiz_sessions.score et completed_at.

curl -X POST http://localhost:3000/api/quiz/complete \
  -H "Content-Type: application/json" \
  -d '{"sessionId": "uuid-session", "score": 12}'

Body :

{
  "sessionId": "uuid-session",
  "score": 12
}

score = nombre de bonnes réponses (entre 0 et totalQuestions).

Réponse 200 :

{ "success": true }

Les sessions non complétées (completed_at = null) sont considérées comme abandonnées et ne sont pas comptabilisées dans les statistiques.


Contact

POST /api/contact

Envoie un message de contact. Persiste le message en base de données, envoie un email aux admins via SMTP, et envoie une confirmation à l'expéditeur.

curl -X POST http://localhost:3000/api/contact \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jean Dupont",
    "email": "jean@example.com",
    "subject": "Question sur l'\''app",
    "message": "Bonjour, j'\''ai une question..."
  }'

Body :

{
  "name": "Jean Dupont",
  "email": "jean@example.com",
  "subject": "Question sur l'app",
  "message": "Bonjour, j'ai une question...",
  "_t": 5000
}

Champs : | Champ | Type | Requis | Description | |---|---|---|---| | name | string | ✅ | Nom de l'expéditeur | | email | string | ✅ | Email de l'expéditeur | | subject | string | ✅ | Sujet du message | | message | string | ✅ | Corps du message | | website | string | ❌ | Honeypot — doit rester vide | | _t | number | ❌ | Timestamp de début de saisie (anti-spam timing) |

Protection anti-spam (4 couches) :

  1. Honeypot (website) : Si rempli, réponse 200 silencieuse (faux succès)
  2. Timing minimum (_t) : Rejet silencieux si soumission < 4 secondes
  3. Rate limiting : Maximum 3 messages par heure par IP (via X-Forwarded-For)
  4. Domaines jetables : Rejet des emails issus de domaines temporaires

Réponse 200 :

{ "success": true, "message": "Emails envoyés avec succès" }

Erreurs : | Cas | Code | Message | |---|---|---| | Champs manquants | 400 | "Tous les champs obligatoires doivent être remplis" | | Domaine email jetable | 400 | "Veuillez utiliser une adresse email permanente." | | Rate limit atteint | 429 | "Trop de messages envoyés. Veuillez réessayer dans une heure." | | Erreur SMTP | 500 | "Erreur lors de l'envoi des emails" |


Messages de contact (Admin)

GET /api/admin/contact

Auth admin requise.

Récupère les messages de contact avec pagination et filtres.

curl "http://localhost:3000/api/admin/contact?page=1&status=unread&search=dupont" \
  -b cookies.txt

Query params : | Param | Type | Description | |---|---|---| | page | integer | Numéro de page (défaut : 1, 20 messages/page) | | status | string | Filtre : unread, read, replied, archived | | search | string | Recherche dans nom, email, sujet |

Réponse 200 :

{
  "success": true,
  "messages": [
    {
      "id": "uuid",
      "name": "Jean Dupont",
      "email": "jean@example.com",
      "subject": "Question sur l'app",
      "message": "Bonjour...",
      "status": "unread",
      "admin_notes": null,
      "created_at": "2026-01-15T10:00:00Z"
    }
  ],
  "total": 47,
  "page": 1,
  "totalPages": 3
}

PATCH /api/admin/contact/[id]

Auth admin requise.

Met à jour le statut ou les notes d'un message.

curl -X PATCH http://localhost:3000/api/admin/contact/uuid-message \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{"status": "replied", "admin_notes": "Répondu par email le 15/01"}'

Body :

{
  "status": "replied",
  "admin_notes": "Répondu par email le 15/01"
}

Valeurs de status : unread, read, replied, archived

Réponse 200 :

{ "success": true }

DELETE /api/admin/contact/[id]

Auth admin requise.

Supprime définitivement un message de contact.

curl -X DELETE http://localhost:3000/api/admin/contact/uuid-message \
  -b cookies.txt

Réponse 200 :

{ "success": true }

Statistiques Admin

GET /api/admin/statistics

Auth admin requise.

Compte le nombre de lignes dans la table statistics selon des filtres.

curl "http://localhost:3000/api/admin/statistics?languageId=uuid&from=2026-01-01&to=2026-01-31" \
  -b cookies.txt

Query params : | Param | Type | Description | |---|---|---| | languageId | string | Optionnel — filtre par langue | | from | string | Format YYYY-MM-DD | | to | string | Format YYYY-MM-DD |

Réponse 200 :

{ "count": 12450 }

DELETE /api/admin/statistics

Auth admin requise.

Supprime des lignes de statistiques selon des filtres. Action irréversible.

# Supprimer les stats d'une langue sur une période
curl -X DELETE \
  "http://localhost:3000/api/admin/statistics?languageId=uuid&from=2025-01-01&to=2025-12-31" \
  -b cookies.txt

# Purge totale (header requis)
curl -X DELETE http://localhost:3000/api/admin/statistics \
  -H "x-delete-all-confirmed: true" \
  -b cookies.txt

Query params : identiques à GET /api/admin/statistics

Protection purge totale : Sans aucun filtre, le header x-delete-all-confirmed: true est obligatoire pour éviter les suppressions accidentelles.

Réponse 200 :

{ "success": true, "deleted": 12450 }

GET /api/admin/quiz/stats

Auth admin requise.

Récupère les statistiques des sessions de quiz (complètes et abandonnées).

curl "http://localhost:3000/api/admin/quiz/stats?languageId=uuid&from=2026-01-01" \
  -b cookies.txt

Réponse 200 :

{
  "total": 320,
  "completed": 215,
  "abandoned": 105,
  "averageScore": 11.3,
  "completionRate": 67.2,
  "byCategory": [
    {
      "categoryName": "Famille",
      "sessions": 45,
      "avgScore": 12.1
    }
  ]
}

Erreurs côté client

POST /api/client-errors

Réception des erreurs JavaScript côté client pour logging serveur. Utilisé par l'AudioPlayer pour signaler les erreurs de lecture audio.

curl -X POST http://localhost:3000/api/client-errors \
  -H "Content-Type: application/json" \
  -d '{"request":"HEAD https://cdn/audio.mp3","response":"403 Forbidden"}'

Body :

{
  "request": "HEAD https://[CDN]/audios/pul/uuid-item/audio.mp3",
  "response": "403 Forbidden"
}

Réponse 200 :

{ "ok": true }

Les erreurs sont loggées côté serveur sans données personnelles identifiables.


Tableau récapitulatif

Endpoint Méthode Auth Description
/api/health/live GET Aucune Liveness probe
/api/auth/login POST Login admin
/api/auth/login GET Admin Vérifier session
/api/auth/logout POST Admin Déconnexion
/api/speaker/login POST Login speaker
/api/languages GET Aucune Liste des langues actives
/api/languages POST Admin Créer une langue
/api/languages/[id] PATCH Admin Modifier une langue
/api/languages/[id] DELETE Admin Supprimer (cascade)
/api/languages/verify-code POST Aucune Vérifier code joueur
/api/languages/[id]/content GET Aucune Contenu complet (offline)
/api/cards POST Admin Créer une carte
/api/cards/import POST Admin Import Excel
/api/items POST Admin Créer un ou des items
/api/items/[id] PATCH Admin Modifier un item
/api/speakers POST Admin Créer un locuteur
/api/audio/upload POST Speaker Upload audio
/api/audio/serve/[[...path]] GET Aucune Proxy MinIO (dev)
/api/statistics/track POST Aucune Enregistrer un événement
/api/statistics/by-language GET Admin Stats par langue
/api/statistics/top-cards GET Admin Top cartes
/api/quiz/start POST Aucune Démarrer un quiz
/api/quiz/complete POST Aucune Terminer un quiz
/api/contact POST Aucune Envoyer un message
/api/admin/contact GET Admin Liste messages contact
/api/admin/contact/[id] PATCH Admin Modifier message
/api/admin/contact/[id] DELETE Admin Supprimer message
/api/admin/statistics GET Admin Compter stats
/api/admin/statistics DELETE Admin Supprimer stats
/api/admin/quiz/stats GET Admin Stats quiz
/api/client-errors POST Aucune Erreurs JS client

Étapes suivantes