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:
parent
3b961209f5
commit
ecf3dfb21d
22 changed files with 874 additions and 2 deletions
64
portail/resources/views/api-docs.blade.php
Normal file
64
portail/resources/views/api-docs.blade.php
Normal 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 <ton-jeton></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 <ton-jeton>" \
|
||||
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
|
||||
48
portail/resources/views/jetons-api.blade.php
Normal file
48
portail/resources/views/jetons-api.blade.php
Normal 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 <jeton></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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue