ajoute le portail Laravel : connexion prof/élève, session unique, gestion de classe, salle d'attente
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
aa31b6112f
commit
30e14b1c54
191 changed files with 25324 additions and 0 deletions
132
portail/app/Http/Controllers/Auth/ConnexionController.php
Normal file
132
portail/app/Http/Controllers/Auth/ConnexionController.php
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Eleve;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ConnexionController extends Controller
|
||||
{
|
||||
public function connexionProf(Request $request): RedirectResponse
|
||||
{
|
||||
$donnees = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$this->appliquerLimiteTentatives($request, 'prof:'.$donnees['email']);
|
||||
|
||||
if (! Auth::guard('web')->attempt($donnees)) {
|
||||
RateLimiter::hit($this->cleLimite($request, 'prof:'.$donnees['email']));
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => 'Identifiants incorrects.',
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->cleLimite($request, 'prof:'.$donnees['email']));
|
||||
$request->session()->regenerate();
|
||||
|
||||
/** @var User $prof */
|
||||
$prof = Auth::guard('web')->user();
|
||||
$avaitDejaUneSession = $prof->jeton_session_actif !== null;
|
||||
$request->session()->put('jeton_session_web', $prof->demarreNouvelleSession());
|
||||
|
||||
return redirect()->route('mes-classes.index')
|
||||
->with('session_precedente_fermee', $avaitDejaUneSession);
|
||||
}
|
||||
|
||||
public function connexionEleve(Request $request): RedirectResponse
|
||||
{
|
||||
$donnees = $request->validate([
|
||||
'identifiant' => ['required', 'string'],
|
||||
'code' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$this->appliquerLimiteTentatives($request, 'eleve:'.$donnees['identifiant']);
|
||||
|
||||
$succes = Auth::guard('eleve')->attempt([
|
||||
'identifiant' => $donnees['identifiant'],
|
||||
'password' => $donnees['code'],
|
||||
]);
|
||||
|
||||
if (! $succes) {
|
||||
RateLimiter::hit($this->cleLimite($request, 'eleve:'.$donnees['identifiant']));
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'identifiant' => 'Identifiant ou code secret incorrect.',
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->cleLimite($request, 'eleve:'.$donnees['identifiant']));
|
||||
$request->session()->regenerate();
|
||||
|
||||
/** @var Eleve $eleve */
|
||||
$eleve = Auth::guard('eleve')->user();
|
||||
$eleve->forceFill(['derniere_connexion_a' => now()])->save();
|
||||
$request->session()->put('jeton_session_eleve', $eleve->demarreNouvelleSession());
|
||||
|
||||
return redirect()->route('eleve');
|
||||
}
|
||||
|
||||
/**
|
||||
* Connexion directe par QR (à usage unique, voir App\Services\GenerateurCompteEleve).
|
||||
*/
|
||||
public function connexionParQr(Request $request, string $jeton): RedirectResponse
|
||||
{
|
||||
$eleve = Eleve::where('jeton_qr', $jeton)->first();
|
||||
|
||||
if ($eleve === null) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$eleve->forceFill(['jeton_qr' => null, 'derniere_connexion_a' => now()])->save();
|
||||
Auth::guard('eleve')->login($eleve);
|
||||
$request->session()->regenerate();
|
||||
$request->session()->put('jeton_session_eleve', $eleve->demarreNouvelleSession());
|
||||
|
||||
return redirect()->route('eleve');
|
||||
}
|
||||
|
||||
public function deconnexion(Request $request): RedirectResponse
|
||||
{
|
||||
if (Auth::guard('web')->check()) {
|
||||
Auth::guard('web')->user()->terminerSession();
|
||||
Auth::guard('web')->logout();
|
||||
}
|
||||
|
||||
if (Auth::guard('eleve')->check()) {
|
||||
Auth::guard('eleve')->user()->terminerSession();
|
||||
Auth::guard('eleve')->logout();
|
||||
}
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('accueil');
|
||||
}
|
||||
|
||||
private function cleLimite(Request $request, string $identifiant): string
|
||||
{
|
||||
return 'connexion:'.$identifiant.'|'.$request->ip();
|
||||
}
|
||||
|
||||
private function appliquerLimiteTentatives(Request $request, string $identifiant): void
|
||||
{
|
||||
$cle = $this->cleLimite($request, $identifiant);
|
||||
|
||||
if (RateLimiter::tooManyAttempts($cle, 5)) {
|
||||
$secondes = RateLimiter::availableIn($cle);
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'identifiant' => "Trop de tentatives. Réessaie dans {$secondes} secondes.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
131
portail/app/Http/Controllers/ClasseController.php
Normal file
131
portail/app/Http/Controllers/ClasseController.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\Eleve;
|
||||
use App\Services\GenerateurCompteEleve;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ClasseController extends Controller
|
||||
{
|
||||
public function __construct(private readonly GenerateurCompteEleve $generateur) {}
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$classes = Auth::guard('web')->user()->classes()->withCount('eleves')->latest()->get();
|
||||
|
||||
return view('mes-classes.index', ['classes' => $classes]);
|
||||
}
|
||||
|
||||
public function show(Classe $classe): View
|
||||
{
|
||||
$this->verifierProprietaire($classe);
|
||||
|
||||
return view('mes-classes.show', [
|
||||
'classe' => $classe->load('eleves'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeEleve(Request $request, Classe $classe): RedirectResponse
|
||||
{
|
||||
$this->verifierProprietaire($classe);
|
||||
|
||||
$donnees = $request->validate([
|
||||
'prenom' => ['required', 'string', 'max:100'],
|
||||
'nom' => ['nullable', 'string', 'max:100'],
|
||||
]);
|
||||
|
||||
$fiche = $this->creerEleve($classe, $donnees['prenom'], $donnees['nom'] ?? null);
|
||||
|
||||
return redirect()->route('mes-classes.show', $classe)
|
||||
->with('fiches', [$fiche]);
|
||||
}
|
||||
|
||||
public function importerCsv(Request $request, Classe $classe): RedirectResponse
|
||||
{
|
||||
$this->verifierProprietaire($classe);
|
||||
|
||||
$request->validate([
|
||||
'fichier' => ['required', 'file', 'mimes:csv,txt'],
|
||||
]);
|
||||
|
||||
$lignes = file($request->file('fichier')->getRealPath(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$fiches = [];
|
||||
|
||||
foreach ($lignes as $ligne) {
|
||||
$colonnes = str_getcsv($ligne);
|
||||
$prenom = trim($colonnes[0] ?? '');
|
||||
|
||||
if ($prenom === '' || Str::lower($prenom) === 'prenom') {
|
||||
continue; // ligne vide ou en-tête
|
||||
}
|
||||
|
||||
$nom = trim($colonnes[1] ?? '') ?: null;
|
||||
$fiches[] = $this->creerEleve($classe, $prenom, $nom);
|
||||
}
|
||||
|
||||
return redirect()->route('mes-classes.show', $classe)
|
||||
->with('fiches', $fiches)
|
||||
->with('nombre_importes', count($fiches));
|
||||
}
|
||||
|
||||
public function regenererCode(Classe $classe, Eleve $eleve): RedirectResponse
|
||||
{
|
||||
$this->verifierProprietaire($classe);
|
||||
abort_if($eleve->classe_id !== $classe->id, 404);
|
||||
|
||||
$codeSecret = $this->generateur->genererCodeSecret();
|
||||
$jetonQr = Str::random(32);
|
||||
|
||||
$eleve->forceFill([
|
||||
'code_secret' => Hash::make($codeSecret),
|
||||
'jeton_qr' => $jetonQr,
|
||||
])->save();
|
||||
|
||||
return redirect()->route('mes-classes.show', $classe)
|
||||
->with('fiches', [[
|
||||
'identifiant' => $eleve->identifiant,
|
||||
'prenom' => $eleve->prenom,
|
||||
'nom' => $eleve->nom,
|
||||
'code_secret' => $codeSecret,
|
||||
'jeton_qr' => $jetonQr,
|
||||
]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{identifiant: string, prenom: string, nom: ?string, code_secret: string, jeton_qr: string}
|
||||
*/
|
||||
private function creerEleve(Classe $classe, string $prenom, ?string $nom): array
|
||||
{
|
||||
$identifiant = $this->generateur->genererIdentifiant($prenom, $nom);
|
||||
$codeSecret = $this->generateur->genererCodeSecret();
|
||||
$jetonQr = Str::random(32);
|
||||
|
||||
$classe->eleves()->create([
|
||||
'prenom' => $prenom,
|
||||
'nom' => $nom,
|
||||
'identifiant' => $identifiant,
|
||||
'code_secret' => Hash::make($codeSecret),
|
||||
'jeton_qr' => $jetonQr,
|
||||
]);
|
||||
|
||||
return [
|
||||
'identifiant' => $identifiant,
|
||||
'prenom' => $prenom,
|
||||
'nom' => $nom,
|
||||
'code_secret' => $codeSecret,
|
||||
'jeton_qr' => $jetonQr,
|
||||
];
|
||||
}
|
||||
|
||||
private function verifierProprietaire(Classe $classe): void
|
||||
{
|
||||
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
}
|
||||
}
|
||||
8
portail/app/Http/Controllers/Controller.php
Normal file
8
portail/app/Http/Controllers/Controller.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EleveTableauDeBordController extends Controller
|
||||
{
|
||||
public function show(): View
|
||||
{
|
||||
$eleve = Auth::guard('eleve')->user()->load('classe');
|
||||
|
||||
return view('eleve', ['eleve' => $eleve]);
|
||||
}
|
||||
}
|
||||
63
portail/app/Http/Controllers/InscriptionController.php
Normal file
63
portail/app/Http/Controllers/InscriptionController.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\Etablissement;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class InscriptionController extends Controller
|
||||
{
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$donnees = $request->validate([
|
||||
'prenom' => ['required', 'string', 'max:100'],
|
||||
'nom' => ['required', 'string', 'max:100'],
|
||||
'email' => ['required', 'email', 'unique:users,email'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
'etablissement_nom_liste' => ['required', 'string'],
|
||||
'etablissement_nom' => ['required_if:etablissement_nom_liste,autre', 'nullable', 'string', 'max:150'],
|
||||
'etablissement_ville' => ['required_if:etablissement_nom_liste,autre', 'nullable', 'string', 'max:100'],
|
||||
'etablissement_uai' => ['nullable', 'string', 'max:20'],
|
||||
'formation' => ['nullable', 'string', 'max:100'],
|
||||
'classe' => ['required', 'string', 'max:100'],
|
||||
]);
|
||||
|
||||
[$prof, $classe] = DB::transaction(function () use ($donnees) {
|
||||
$etablissement = $donnees['etablissement_nom_liste'] === 'autre'
|
||||
? Etablissement::firstOrCreate(
|
||||
['nom' => $donnees['etablissement_nom']],
|
||||
['ville' => $donnees['etablissement_ville'], 'uai' => $donnees['etablissement_uai'] ?? null],
|
||||
)
|
||||
: Etablissement::firstOrCreate(['nom' => $donnees['etablissement_nom_liste']]);
|
||||
|
||||
$prof = User::create([
|
||||
'name' => trim($donnees['prenom'].' '.$donnees['nom']),
|
||||
'email' => $donnees['email'],
|
||||
'password' => Hash::make($donnees['password']),
|
||||
'etablissement_id' => $etablissement->id,
|
||||
]);
|
||||
|
||||
$classe = Classe::create([
|
||||
'etablissement_id' => $etablissement->id,
|
||||
'user_id' => $prof->id,
|
||||
'nom' => $donnees['classe'],
|
||||
'formation' => $donnees['formation'] ?? null,
|
||||
]);
|
||||
|
||||
return [$prof, $classe];
|
||||
});
|
||||
|
||||
Auth::guard('web')->login($prof);
|
||||
$request->session()->regenerate();
|
||||
$request->session()->put('jeton_session_web', $prof->demarreNouvelleSession());
|
||||
|
||||
return redirect()->route('mes-classes.show', $classe)
|
||||
->with('classe_creee', true);
|
||||
}
|
||||
}
|
||||
85
portail/app/Http/Controllers/SeanceController.php
Normal file
85
portail/app/Http/Controllers/SeanceController.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\LabSession;
|
||||
use App\Models\Seance;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SeanceController extends Controller
|
||||
{
|
||||
public function store(Request $request, Classe $classe): RedirectResponse
|
||||
{
|
||||
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
|
||||
$donnees = $request->validate([
|
||||
'lab_id' => ['required', 'exists:labs,id'],
|
||||
]);
|
||||
|
||||
$seance = $classe->seances()->create([
|
||||
'lab_id' => $donnees['lab_id'],
|
||||
'statut' => Seance::SALLE_ATTENTE,
|
||||
'demarree_a' => now(),
|
||||
]);
|
||||
|
||||
return redirect()->route('seances.attente', $seance);
|
||||
}
|
||||
|
||||
public function attente(Seance $seance): View
|
||||
{
|
||||
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
|
||||
return view('seances.attente', [
|
||||
'seance' => $seance->load(['classe', 'lab', 'labSessions.eleve']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function admettre(Seance $seance, LabSession $labSession): RedirectResponse
|
||||
{
|
||||
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
abort_if($labSession->seance_id !== $seance->id, 404);
|
||||
|
||||
$labSession->update(['etat' => LabSession::ADMIS]);
|
||||
|
||||
return redirect()->route('seances.attente', $seance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un élève rejoint la séance : il patiente jusqu'à ce que le prof l'admette.
|
||||
*/
|
||||
public function rejoindre(Request $request, Seance $seance): RedirectResponse
|
||||
{
|
||||
$eleve = Auth::guard('eleve')->user();
|
||||
abort_if($eleve->classe_id !== $seance->classe_id, 403);
|
||||
|
||||
LabSession::firstOrCreate(
|
||||
['seance_id' => $seance->id, 'eleve_id' => $eleve->id],
|
||||
['lab_id' => $seance->lab_id, 'etat' => LabSession::EN_ATTENTE],
|
||||
);
|
||||
|
||||
return redirect()->route('seances.eleve.attente', $seance);
|
||||
}
|
||||
|
||||
public function attenteEleve(Seance $seance): View|RedirectResponse
|
||||
{
|
||||
$eleve = Auth::guard('eleve')->user();
|
||||
abort_if($eleve->classe_id !== $seance->classe_id, 403);
|
||||
|
||||
$labSession = LabSession::where('seance_id', $seance->id)
|
||||
->where('eleve_id', $eleve->id)
|
||||
->first();
|
||||
|
||||
if ($labSession === null) {
|
||||
return redirect()->route('eleve');
|
||||
}
|
||||
|
||||
return view('seances.attente-eleve', [
|
||||
'seance' => $seance->load('lab'),
|
||||
'labSession' => $labSession,
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
portail/app/Http/Middleware/VerifieSessionUnique.php
Normal file
36
portail/app/Http/Middleware/VerifieSessionUnique.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Une seule session active par compte (étape 8). Si le jeton que porte cette
|
||||
* session ne correspond plus à celui enregistré sur le compte, c'est qu'une
|
||||
* connexion plus récente a eu lieu ailleurs : on déconnecte et on le dit.
|
||||
*/
|
||||
class VerifieSessionUnique
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string $garde = 'web'): Response
|
||||
{
|
||||
$utilisateur = Auth::guard($garde)->user();
|
||||
|
||||
if ($utilisateur !== null) {
|
||||
$cleSession = "jeton_session_{$garde}";
|
||||
|
||||
if (! $utilisateur->sessionEstEncoreValide($request->session()->get($cleSession))) {
|
||||
Auth::guard($garde)->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login')
|
||||
->with('session_coupee', 'Vous avez été déconnecté : une nouvelle connexion a eu lieu sur votre compte.');
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue