ajoute l'API publique /api/v1 (bloc B, premier chantier)

Catalogue en lecture seule (labs, banque de quiz officielle, cours) -
choix d'Adam pour la première version, zéro donnée d'élève. Laravel
Sanctum, un jeton Bearer par développeur créé depuis /jetons-api (un
prof), portée catalogue:lire seulement. Quota par jeton (pas par
compte ni par IP) dans Redis via un limiteur nommé, 60 requêtes/minute.
Documentation : spécification OpenAPI 3.0 et page publique /api/docs.

Désactive /sanctum/csrf-cookie (config/sanctum.php) : l'API est en
jeton Bearer seulement, jamais en cookie de SPA.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-14 01:23:23 +02:00
parent 3b961209f5
commit ecf3dfb21d
22 changed files with 874 additions and 2 deletions

View file

@ -0,0 +1,64 @@
@extends('layouts.vitrine')
@section('titre', "API publique")
@section('contenu')
<div class="mx-auto max-w-4xl px-5 py-14 lg:px-10">
<h1 class="text-4xl font-extrabold tracking-tight">API publique Syllabus</h1>
<p class="mt-4 text-lg font-semibold">Catalogue en lecture seule : labs, banque de quiz officielle, cours. Aucune donnée d'élève.</p>
<section class="mt-8 rounded-2xl border border-[#e3e5e8] bg-white p-6">
<h2 class="text-2xl font-extrabold">Authentification</h2>
<p class="mt-2 font-semibold">Crée un jeton depuis un compte prof : <a href="{{ route('login') }}" class="font-bold underline underline-offset-4">connexion</a> puis « Mon profil → Jetons de l'API ». Envoie-le dans l'en-tête :</p>
<pre class="mt-3 overflow-x-auto rounded-xl bg-[#13233A] p-4 font-mono text-sm text-white">Authorization: Bearer &lt;ton-jeton&gt;</pre>
<p class="mt-3 font-semibold">60 requêtes par minute et par jeton (en-têtes <code class="font-mono">X-RateLimit-*</code> sur chaque réponse).</p>
</section>
<section class="mt-8 rounded-2xl border border-[#e3e5e8] bg-white p-6">
<h2 class="text-2xl font-extrabold">Points d'accès</h2>
<div class="mt-4 grid gap-4">
<div>
<p class="font-mono font-bold">GET /api/v1/labs</p>
<p class="font-semibold">Liste des labs du catalogue.</p>
</div>
<div>
<p class="font-mono font-bold">GET /api/v1/labs/{identifiant}</p>
<p class="font-semibold">Détail d'un lab (ex. <code class="font-mono">08-deux-lan-routeur</code>).</p>
</div>
<div>
<p class="font-mono font-bold">GET /api/v1/quiz</p>
<p class="font-semibold">Liste des quiz de la banque officielle (titre, thème, nombre de questions — jamais les questions ni les réponses).</p>
</div>
<div>
<p class="font-mono font-bold">GET /api/v1/quiz/{id}</p>
<p class="font-semibold">Détail d'un quiz (métadonnées).</p>
</div>
<div>
<p class="font-mono font-bold">GET /api/v1/cours</p>
<p class="font-semibold">Liste des cours du catalogue.</p>
</div>
<div>
<p class="font-mono font-bold">GET /api/v1/cours/{identifiant}</p>
<p class="font-semibold">Détail d'un cours, avec son contenu complet.</p>
</div>
</div>
</section>
<section class="mt-8 rounded-2xl border border-[#e3e5e8] bg-white p-6">
<h2 class="text-2xl font-extrabold">Exemple</h2>
<pre class="mt-3 overflow-x-auto rounded-xl bg-[#13233A] p-4 font-mono text-sm text-white">curl -H "Authorization: Bearer &lt;ton-jeton&gt;" \
https://syllabuslab.fr/api/v1/labs/08-deux-lan-routeur</pre>
<pre class="mt-3 overflow-x-auto rounded-xl border border-[#e3e5e8] bg-[#f8f9fa] p-4 font-mono text-sm">{
"data": {
"identifiant": "08-deux-lan-routeur",
"titre": "Deux LAN et un routeur",
"niveau": "decouverte",
"duree_min": 45,
"competences": ["adressage-ipv4", "routage-statique"]
}
}</pre>
</section>
<p class="mt-8 font-semibold"><a href="{{ asset('openapi/v1.yaml') }}" class="font-bold underline underline-offset-4">Spécification OpenAPI (v1.yaml) →</a></p>
</div>
@endsection

View file

@ -0,0 +1,48 @@
@extends('layouts.app')
@section('titre', 'Jetons de l\'API')
@section('contenu')
<div class="mx-auto max-w-2xl px-5 py-10">
<p class="font-mono text-sm font-semibold"><a href="{{ route('profil') }}" class="underline" style="color:var(--sy-muted)">← Mon profil</a></p>
<h1 class="mt-2 text-3xl font-extrabold" style="color:var(--sy-ink)">Jetons de l'API publique</h1>
<p class="mt-2 font-semibold" style="color:var(--sy-muted)">Un jeton donne accès en lecture au catalogue (labs, banque de quiz officielle, cours) via <code class="font-mono">/api/v1</code>. 60 requêtes par minute et par jeton.</p>
@if (session('jeton_cree'))
<div class="mt-6 rounded-2xl border p-5" style="border-color:#A63D2A;background:#fce8e6">
<p class="font-extrabold" style="color:var(--sy-ink)">Ton jeton (copie-le maintenant, il ne sera plus jamais affiché) :</p>
<p class="mt-2 break-all rounded-xl p-3 font-mono text-sm font-bold" style="background:var(--sy-surface);color:var(--sy-ink)">{{ session('jeton_cree') }}</p>
<p class="mt-2 text-sm font-semibold" style="color:var(--sy-muted)">À utiliser dans l'en-tête <code class="font-mono">Authorization: Bearer &lt;jeton&gt;</code>.</p>
</div>
@endif
<form action="{{ route('jetons-api.store') }}" method="post" class="mt-6 flex flex-wrap items-end gap-3 rounded-2xl border p-5" style="border-color:var(--sy-line);background:var(--sy-surface)">
@csrf
<label class="grid gap-1 font-bold" style="color:var(--sy-ink)">Nom du jeton<input name="nom" required maxlength="80" placeholder="Ex. : script de révision" class="rounded-xl border px-3 py-2 font-semibold" style="border-color:var(--sy-line);background:var(--sy-bg);color:var(--sy-ink)"></label>
<button type="submit" class="inline-flex h-11 items-center rounded-full px-5 font-bold text-white" style="background:var(--sy-blue)">Créer un jeton</button>
</form>
<section class="mt-8">
<h2 class="text-xl font-extrabold" style="color:var(--sy-ink)">Jetons actifs</h2>
@if ($jetons->isEmpty())
<p class="mt-3 font-semibold" style="color:var(--sy-muted)">Aucun jeton pour l'instant.</p>
@else
<ul class="mt-3 grid gap-2">
@foreach ($jetons as $jeton)
<li class="flex flex-wrap items-center justify-between gap-3 rounded-xl border p-3" style="border-color:var(--sy-line);background:var(--sy-surface)">
<span class="font-bold" style="color:var(--sy-ink)">{{ $jeton->name }}
<span class="font-mono text-xs font-semibold" style="color:var(--sy-muted)">· créé le {{ $jeton->created_at->translatedFormat('d/m/Y') }}@if($jeton->last_used_at) · utilisé le {{ $jeton->last_used_at->translatedFormat('d/m/Y') }}@endif</span>
</span>
<form action="{{ route('jetons-api.destroy', $jeton) }}" method="post" onsubmit="return confirm('Révoquer ce jeton ? Il arrêtera de fonctionner immédiatement.')">
@csrf
<button type="submit" class="font-bold underline" style="color:#A63D2A">Révoquer</button>
</form>
</li>
@endforeach
</ul>
@endif
</section>
<p class="mt-8 text-sm font-semibold" style="color:var(--sy-muted)">Documentation : <a href="{{ route('api.docs') }}" class="underline" style="color:var(--sy-blue)">/api/docs</a>.</p>
</div>
@endsection

View file

@ -39,5 +39,9 @@
</div>
</dl>
</div>
@if ($role === 'prof')
<p class="mt-6"><a href="{{ route('jetons-api.index') }}" class="font-bold underline" style="color:var(--sy-blue)">Jetons de l'API publique →</a></p>
@endif
</div>
@endsection