Paillasse/portail/routes/web.php
Adam Belkacemi 91d7cde4c1 ajoute Moodle LTI 1.3 (V2), une 6e façon de rejoindre une classe
LTI authentifie l'élève et le fait rejoindre sa classe (même code que
le code/QR existant), pas lancer un lab précis - relecture du
CLAUDE.md qui liste LTI avec les autres façons d'ajouter des élèves.
packbackbooks/lti-1p3-tool (Apache-2.0, recommandé par le CLAUDE.md),
trois ponts vers Redis/cookies/MariaDB, élève retrouvé par identité
Moodle stable pour ne jamais créer de doublon. Enregistrement d'une
plateforme/d'un déploiement par commande Artisan (opération rare,
faite à la main par Adam), jamais une page d'administration.

Corrige au passage un vrai bug préexistant trouvé en vérifiant en réel
sur le serveur : withoutMiddleware(ValidateCsrfToken::class) sur une
route ne retire jamais le vrai middleware CSRF (ValidateCsrfToken est
un alias déprécié de PreventRequestForgery, pas la classe réellement
enregistrée) - /csp-rapport avait le même défaut depuis l'étape 14,
invisible faute de surveillance. Corrigé pour les deux par la bonne
méthode (validateCsrfTokens except, dans bootstrap/app.php).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-14 13:51:47 +02:00

257 lines
19 KiB
PHP

<?php
use App\Http\Controllers\AideController;
use App\Http\Controllers\AnnonceController;
use App\Http\Controllers\Auth\ConnexionController;
use App\Http\Controllers\BanqueQuizController;
use App\Http\Controllers\BibliothequeProfController;
use App\Http\Controllers\ChatEleveController;
use App\Http\Controllers\ChatProfController;
use App\Http\Controllers\ClasseChatController;
use App\Http\Controllers\ClasseController;
use App\Http\Controllers\ConsoleController;
use App\Http\Controllers\CorrectionController;
use App\Http\Controllers\DevoirController;
use App\Http\Controllers\DevoirEleveController;
use App\Http\Controllers\EleveTableauDeBordController;
use App\Http\Controllers\EmploiDuTempsController;
use App\Http\Controllers\EvenementExamenController;
use App\Http\Controllers\InscriptionController;
use App\Http\Controllers\LtiClesController;
use App\Http\Controllers\LtiConnexionController;
use App\Http\Controllers\LtiLancementController;
use App\Http\Controllers\MaClasseController;
use App\Http\Controllers\MesCoursController;
use App\Http\Controllers\MesDevoirsController;
use App\Http\Controllers\MesFichiersController;
use App\Http\Controllers\MesNotesController;
use App\Http\Controllers\ObservationController;
use App\Http\Controllers\ParametresController;
use App\Http\Controllers\ProfilController;
use App\Http\Controllers\QuizController;
use App\Http\Controllers\QuizEleveController;
use App\Http\Controllers\RaccourciController;
use App\Http\Controllers\RapportCspController;
use App\Http\Controllers\RechercheController;
use App\Http\Controllers\RejoindreClasseController;
use App\Http\Controllers\ResultatsController;
use App\Http\Controllers\SeanceController;
use App\Http\Controllers\SeancesEnDirectController;
use App\Http\Controllers\SessionQuizController;
use App\Http\Controllers\SignalementController;
use App\Http\Controllers\TokenApiController;
use App\Http\Controllers\VisioController;
use App\Http\Controllers\WebdavAuthController;
use Illuminate\Support\Facades\Route;
Route::view('/', 'accueil')->name('accueil');
Route::view('/labs', 'labs.index')->name('labs.index');
Route::view('/labs/essai', 'labs.essai')->name('labs.essai');
Route::view('/cours', 'cours.index')->name('cours.index');
Route::view('/cours/adressage-ipv4', 'cours.show')->name('cours.show');
Route::view('/etablissements', 'etablissements')->name('etablissements');
Route::view('/tarifs', 'tarifs')->name('tarifs');
Route::view('/connexion', 'auth.connexion')->name('login');
Route::view('/inscription', 'auth.inscription')->name('register');
// Démo marketing (données fictives) : la vraie gestion de classe est sous /mes-classes.
Route::view('/classes/suivi', 'classes.suivi')->name('suivi');
Route::view('/examen', 'examen')->name('examen');
// À brancher sur l'envoi d'e-mail (Mail::to('adam@belkacemi.me')->send(...)).
// Envoyé par les navigateurs eux-mêmes (pas de jeton CSRF possible) :
// exclusion dans bootstrap/app.php (validateCsrfTokens), pas ici —
// "withoutMiddleware" sur la route elle-même ne suffit pas (419 constaté
// le 2026-10-14 en vérifiant /lti/lancement en réel).
Route::post('/csp-rapport', RapportCspController::class)
->middleware('throttle:30,1')
->name('csp.rapport');
// Moodle LTI 1.3 (V2, CLAUDE.md section 6) : postés directement par Moodle
// depuis son propre domaine, jamais de jeton CSRF à notre nom — la
// sécurité du lancement vient de la signature du jeton LTI lui-même.
// Exclusion dans bootstrap/app.php, voir le commentaire ci-dessus.
Route::match(['get', 'post'], '/lti/connexion', [LtiConnexionController::class, 'connecter'])
->name('lti.connexion');
Route::post('/lti/lancement/{codeClasse}', [LtiLancementController::class, 'lancer'])
->name('lti.lancement');
Route::get('/lti/cles.json', [LtiClesController::class, 'cles'])->name('lti.cles');
Route::post('/contact', fn () => back()->with('contact_ok', true))->name('contact');
Route::view('/fonctionnalites', 'fonctionnalites')->name('fonctionnalites');
Route::view('/legal', 'legal')->name('legal');
Route::view('/confidentialite', 'confidentialite')->name('confidentialite');
Route::redirect('/statut', 'https://status.syllabuslab.fr')->name('statut');
Route::view('/cours/adressage-ipv4/cm', 'cours.cm')->name('cours.cm');
Route::view('/banque', 'banque')->name('banque');
Route::view('/quiz/projection', 'quiz.projection')->name('quiz.projection');
Route::view('/quiz', 'quiz.eleve')->name('quiz.eleve');
Route::view('/ide', 'ide')->name('ide');
Route::view('/bibliotheque', 'bibliotheque')->name('bibliotheque');
Route::view('/generateur', 'generateur')->name('generateur');
Route::view('/labs/web', 'labs.web')->name('labs.web');
Route::view('/maison', 'maison')->name('maison');
Route::view('/devoirs', 'devoirs')->name('devoirs');
Route::view('/td/imprimer', 'td.imprimable')->name('td.imprimable');
Route::view('/cockpit', 'cockpit')->name('cockpit');
// Démo marketing (données fictives), distincte de /eleve (vrai tableau de bord).
Route::view('/mon-espace', 'espace-eleve')->name('espace.eleve');
Route::view('/vie-scolaire', 'vie-scolaire')->name('vie.scolaire');
Route::view('/eleves/fiche', 'fiche-eleve')->name('fiche.eleve');
Route::view('/correction', 'correction')->name('correction');
Route::view('/sujets/creer', 'sujet-creer')->name('sujet.creer');
Route::view('/messagerie', 'messagerie')->name('messagerie');
Route::view('/exports', 'export-ent')->name('export.ent');
Route::view('/aide', 'aide')->name('aide');
Route::view('/api/docs', 'api-docs')->name('api.docs');
Route::view('/academie', 'academie')->name('academie');
Route::view('/paiement', 'paiement')->name('paiement');
Route::view('/paiement/confirme', 'paiement-ok')->name('paiement.ok');
Route::view('/compte/facturation', 'facturation')->name('facturation');
Route::view('/cours/fiche', 'cours.fiche')->name('cours.fiche');
Route::view('/etablissement', 'espace-etablissement')->name('espace.etablissement');
Route::view('/integration', 'integration')->name('integration');
Route::view('/cohortes', 'cohortes')->name('cohortes');
Route::view('/ressources/outil', 'ressource')->name('ressource');
Route::view('/visio', 'visio')->name('visio');
// --- Authentification (étape 8) ---
Route::post('/connexion/prof', [ConnexionController::class, 'connexionProf'])->name('connexion.prof');
Route::post('/connexion/eleve', [ConnexionController::class, 'connexionEleve'])->name('connexion.eleve');
Route::get('/connexion/qr/{jeton}', [ConnexionController::class, 'connexionParQr'])->name('connexion.qr');
Route::post('/inscription', [InscriptionController::class, 'store'])->name('inscription.store');
// Un élève rejoint une classe avec son code ; le prof accepte (aucun e-mail).
Route::get('/rejoindre', [RejoindreClasseController::class, 'formulaire'])->name('rejoindre');
Route::post('/rejoindre', [RejoindreClasseController::class, 'demander'])->middleware('throttle:10,1')->name('rejoindre.demander');
Route::get('/rejoindre/attente', [RejoindreClasseController::class, 'attente'])->name('rejoindre.attente');
Route::get('/rejoindre/etat', [RejoindreClasseController::class, 'etat'])->name('rejoindre.etat');
Route::get('/rejoindre/bienvenue', [RejoindreClasseController::class, 'bienvenue'])->name('rejoindre.bienvenue');
Route::post('/deconnexion', [ConnexionController::class, 'deconnexion'])->name('deconnexion');
// --- Commun aux deux espaces (séparation vitrine/application, 2026-10-13) ---
Route::middleware(['auth:eleve,web', 'session.unique:eleve', 'session.unique:web', 'maj.activite'])->group(function () {
Route::get('/profil', [ProfilController::class, 'show'])->name('profil');
Route::get('/parametres', [ParametresController::class, 'show'])->name('parametres');
Route::post('/parametres', [ParametresController::class, 'update'])->name('parametres.update');
Route::post('/signalements', [SignalementController::class, 'store'])->name('signalements.store');
Route::get('/aide-app', [AideController::class, 'show'])->name('aide.app');
Route::post('/raccourcis', [RaccourciController::class, 'store'])->name('raccourcis.store');
Route::post('/raccourcis/{raccourci}/supprimer', [RaccourciController::class, 'destroy'])->name('raccourcis.destroy');
Route::get('/emploi-du-temps', [EmploiDuTempsController::class, 'show'])->name('emploi-du-temps');
Route::get('/mes-cours/{cours:identifiant}', [MesCoursController::class, 'montrer'])->name('mes-cours.show');
Route::get('/recherche', [RechercheController::class, 'chercher'])->middleware('throttle:30,1')->name('recherche');
});
// --- Espace élève (protégé, session unique) ---
Route::middleware(['auth:eleve', 'session.unique:eleve', 'maj.activite'])->group(function () {
Route::get('/eleve', [EleveTableauDeBordController::class, 'show'])->name('eleve');
Route::get('/ma-classe', [MaClasseController::class, 'show'])->name('ma-classe');
Route::get('/mes-devoirs', [MesDevoirsController::class, 'show'])->name('mes-devoirs');
Route::get('/mes-cours', [MesCoursController::class, 'show'])->name('mes-cours');
Route::get('/mes-notes', [MesNotesController::class, 'show'])->name('mes-notes');
Route::get('/chat/etat', [ChatEleveController::class, 'etat'])->name('chat.eleve.etat');
Route::post('/chat/envoyer', [ChatEleveController::class, 'envoyer'])->middleware('throttle:30,1')->name('chat.eleve.envoyer');
Route::post('/seances/{seance}/rejoindre', [SeanceController::class, 'rejoindre'])->name('seances.rejoindre');
Route::get('/seances/{seance}/attente-eleve', [SeanceController::class, 'attenteEleve'])->name('seances.eleve.attente');
Route::get('/seances/{seance}/etat-eleve', [SeanceController::class, 'etatEleve'])->name('seances.eleve.etat');
Route::get('/seances/{seance}/visio/eleve', [VisioController::class, 'eleve'])->name('seances.visio.eleve');
Route::get('/seances/{seance}/visio/eleve/etat', [VisioController::class, 'etatEleve'])->name('seances.visio.eleve.etat');
Route::post('/seances/{seance}/visio/eleve/main', [VisioController::class, 'main'])->name('seances.visio.eleve.main');
Route::post('/seances/{seance}/visio/eleve/participant', [VisioController::class, 'participant'])->name('seances.visio.eleve.participant');
Route::post('/lab-sessions/{labSession}/corriger', [CorrectionController::class, 'corriger'])
->middleware('throttle:6,1')
->name('lab-sessions.corriger');
Route::post('/lab-sessions/{labSession}/machines/{labSessionMachine}/console', [ConsoleController::class, 'pourEleve'])->name('console.eleve');
// Chantier « vrai cockpit », dernière étape : Surveillance d'examen.
Route::post('/lab-sessions/{labSession}/evenements-examen', [EvenementExamenController::class, 'store'])
->middleware('throttle:20,1')
->name('lab-sessions.evenements-examen');
// Chantier « vrai cockpit », étape 2 : Quiz flash.
Route::get('/quiz-flash/etat', [QuizEleveController::class, 'etat'])->name('quiz.eleve.etat');
Route::post('/sessions-quiz/{session}/repondre', [QuizEleveController::class, 'repondre'])
->middleware('throttle:30,1')
->name('sessions-quiz.repondre');
// Chantier « vrai cockpit », étape 3 : Devoirs maison.
Route::get('/devoirs/{devoir}', [DevoirEleveController::class, 'show'])->name('devoirs.eleve.show');
Route::post('/devoirs/{devoir}/rendre', [DevoirEleveController::class, 'rendre'])->name('devoirs.eleve.rendre');
// Bloc C : dossier personnel en WebDAV.
Route::get('/mes-fichiers', [MesFichiersController::class, 'show'])->name('mes-fichiers');
});
// Sous-requête interne de Nginx seulement (location "internal", voir
// ansible/roles/portail/templates/nginx-portail.conf.j2) : jamais appelable
// depuis l'extérieur. Pas de middleware de session : chaque appel s'identifie
// par en-tête HTTP Basic (mêmes identifiants que la connexion élève).
Route::get('/interne/webdav-auth', [WebdavAuthController::class, 'verifier'])->name('interne.webdav-auth');
// --- Espace prof (protégé, session unique) ---
Route::middleware(['auth:web', 'session.unique:web'])->group(function () {
Route::get('/mes-classes', [ClasseController::class, 'index'])->name('mes-classes.index');
Route::get('/mes-classes/{classe}', [ClasseController::class, 'show'])->name('mes-classes.show');
Route::post('/mes-classes/{classe}/eleves', [ClasseController::class, 'storeEleve'])->name('mes-classes.eleves.store');
Route::post('/mes-classes/{classe}/eleves/import', [ClasseController::class, 'importerCsv'])->name('mes-classes.eleves.importer');
Route::post('/mes-classes/{classe}/eleves/{eleve}/regenerer-code', [ClasseController::class, 'regenererCode'])->name('mes-classes.eleves.regenererCode');
Route::post('/mes-classes/{classe}/demandes/{demande}/accepter', [ClasseController::class, 'accepterDemande'])->name('mes-classes.demandes.accepter');
Route::post('/mes-classes/{classe}/demandes/{demande}/refuser', [ClasseController::class, 'refuserDemande'])->name('mes-classes.demandes.refuser');
Route::get('/mes-classes/{classe}/demandes/nombre', [ClasseController::class, 'nombreDemandes'])->name('mes-classes.demandes.nombre');
Route::post('/mes-classes/{classe}/fiches', [ClasseController::class, 'nouvellesFiches'])->name('mes-classes.fiches');
Route::post('/mes-classes/{classe}/archiver', [ClasseController::class, 'archiver'])->name('mes-classes.archiver');
Route::post('/mes-classes/{classe}/desarchiver', [ClasseController::class, 'desarchiver'])->name('mes-classes.desarchiver');
Route::post('/mes-classes/{classe}/seances', [SeanceController::class, 'store'])->name('mes-classes.seances.store');
Route::get('/seances/{seance}/attente', [SeanceController::class, 'attente'])->name('seances.attente');
Route::get('/seances/{seance}/visio', [VisioController::class, 'prof'])->name('seances.visio.prof');
Route::post('/seances/{seance}/visio/ouvrir', [VisioController::class, 'ouvrir'])->name('seances.visio.ouvrir');
Route::post('/seances/{seance}/visio/fermer', [VisioController::class, 'fermer'])->name('seances.visio.fermer');
Route::get('/seances/{seance}/visio/etat', [VisioController::class, 'etatProf'])->name('seances.visio.etat');
Route::post('/seances/{seance}/visio/eleves/{eleve}/parole', [VisioController::class, 'parole'])->name('seances.visio.parole');
Route::post('/seances/{seance}/visio/chat', [VisioController::class, 'chat'])->name('seances.visio.chat');
Route::post('/seances/{seance}/ouvrir', [SeanceController::class, 'ouvrir'])->name('seances.ouvrir');
Route::post('/seances/{seance}/admettre/{labSession}', [SeanceController::class, 'admettre'])->name('seances.admettre');
Route::get('/seances/{seance}/labsessions/{labSession}/console', [SeanceController::class, 'consoleProf'])->name('seances.console.prof.page');
Route::post('/seances/{seance}/labsessions/{labSession}/machines/{labSessionMachine}/console', [ConsoleController::class, 'pourProf'])->name('console.prof');
// Chantier « vrai cockpit », étape 2 : Quiz flash.
Route::get('/mes-classes/{classe}/quiz', [QuizController::class, 'index'])->name('mes-classes.quiz.index');
Route::post('/mes-classes/{classe}/quiz', [QuizController::class, 'store'])->name('mes-classes.quiz.store');
Route::get('/mes-classes/{classe}/quiz/{quiz}', [QuizController::class, 'show'])->name('mes-classes.quiz.show');
Route::post('/mes-classes/{classe}/quiz/{quiz}/supprimer', [QuizController::class, 'destroy'])->name('mes-classes.quiz.destroy');
Route::post('/mes-classes/{classe}/quiz/{quiz}/questions', [QuizController::class, 'storeQuestion'])->name('mes-classes.quiz.questions.store');
Route::post('/mes-classes/{classe}/quiz/{quiz}/questions/{question}/supprimer', [QuizController::class, 'destroyQuestion'])->name('mes-classes.quiz.questions.destroy');
Route::post('/mes-classes/{classe}/quiz/{quiz}/lancer', [SessionQuizController::class, 'lancer'])->name('mes-classes.quiz.lancer');
Route::get('/sessions-quiz/{session}', [SessionQuizController::class, 'show'])->name('sessions-quiz.show');
Route::get('/sessions-quiz/{session}/etat', [SessionQuizController::class, 'etat'])->name('sessions-quiz.etat');
Route::post('/sessions-quiz/{session}/suivante', [SessionQuizController::class, 'suivante'])->name('sessions-quiz.suivante');
Route::post('/sessions-quiz/{session}/terminer', [SessionQuizController::class, 'terminer'])->name('sessions-quiz.terminer');
Route::get('/banque-quiz', [BanqueQuizController::class, 'index'])->name('banque-quiz.index');
Route::post('/mes-classes/{classe}/quiz/{quiz}/partager', [BanqueQuizController::class, 'partager'])->name('mes-classes.quiz.partager');
Route::post('/banque-quiz/{quiz}/dupliquer', [BanqueQuizController::class, 'dupliquer'])->name('banque-quiz.dupliquer');
// Chantier « vrai cockpit », étape 3 : Devoirs maison.
Route::get('/mes-classes/{classe}/devoirs', [DevoirController::class, 'index'])->name('mes-classes.devoirs.index');
Route::post('/mes-classes/{classe}/devoirs', [DevoirController::class, 'store'])->name('mes-classes.devoirs.store');
Route::get('/mes-classes/{classe}/devoirs/{devoir}', [DevoirController::class, 'show'])->name('mes-classes.devoirs.show');
Route::post('/mes-classes/{classe}/devoirs/{devoir}/supprimer', [DevoirController::class, 'destroy'])->name('mes-classes.devoirs.destroy');
Route::post('/mes-classes/{classe}/devoirs/{devoir}/rendus/{rendu}/noter', [DevoirController::class, 'noter'])->name('mes-classes.devoirs.noter');
Route::get('/mes-classes/{classe}/devoirs/{devoir}/rendus/{rendu}/telecharger', [DevoirController::class, 'telechargerRendu'])->name('mes-classes.devoirs.telecharger');
// Séparation vitrine/application (2026-10-13) : Ma classe (annonces),
// Résultats (observations), Séances en direct, Bibliothèque, chat.
Route::post('/mes-classes/{classe}/annonces', [AnnonceController::class, 'store'])->name('mes-classes.annonces.store');
Route::post('/mes-classes/{classe}/annonces/{annonce}/supprimer', [AnnonceController::class, 'destroy'])->name('mes-classes.annonces.destroy');
Route::post('/mes-classes/{classe}/observations', [ObservationController::class, 'store'])->name('mes-classes.observations.store');
Route::post('/mes-classes/{classe}/observations/{observation}/supprimer', [ObservationController::class, 'destroy'])->name('mes-classes.observations.destroy');
Route::post('/mes-classes/{classe}/chat/basculer', [ClasseChatController::class, 'basculer'])->name('mes-classes.chat.basculer');
Route::get('/seances-en-direct', [SeancesEnDirectController::class, 'show'])->name('seances-en-direct');
Route::get('/bibliotheque-app', [BibliothequeProfController::class, 'show'])->name('bibliotheque.app');
Route::get('/resultats', [ResultatsController::class, 'show'])->name('resultats');
Route::get('/chat', [ChatProfController::class, 'index'])->name('chat.prof.index');
Route::get('/chat/{fil}/etat', [ChatProfController::class, 'etat'])->name('chat.prof.etat');
Route::post('/chat/{fil}/envoyer', [ChatProfController::class, 'envoyer'])->middleware('throttle:30,1')->name('chat.prof.envoyer');
// API publique (bloc B) : jetons Sanctum du prof.
Route::get('/jetons-api', [TokenApiController::class, 'index'])->name('jetons-api.index');
Route::post('/jetons-api', [TokenApiController::class, 'store'])->name('jetons-api.store');
Route::post('/jetons-api/{jeton}/revoquer', [TokenApiController::class, 'destroy'])->name('jetons-api.destroy');
});