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:
Adam Belkacemi 2026-10-08 02:01:58 +02:00
parent aa31b6112f
commit 30e14b1c54
191 changed files with 25324 additions and 0 deletions

View file

@ -272,3 +272,45 @@ La bonne nouvelle de cette étape, c'est que Kolla avait déjà fait le plus gro
**Suite**
Étape 8 : portail Laravel (Blade, Vite, MariaDB/Redis pour l'app, modèles, connexion prof/élève, salle d'attente). Le dossier `design/` doit exister : à vérifier avant de commencer.
## 2026-10-07 — Étape 8 : portail Laravel
**Fait**
- Projet Laravel 13 créé dans `portail/` (développé en local sur le Mac : PHP 8.4/Composer/Node 22 y étaient déjà installés), plateforme Composer forcée à PHP 8.3 pour que les dépendances restent installables sur le serveur (PHP 8.3, via Sury). Pest 4 (Pest 5 exige PHP 8.4). `concurrently` retiré du starter (2 vulnérabilités critiques via `shell-quote`, et en réalité inutilisé : `php artisan dev` de Laravel 13 gère déjà le lancement parallèle nativement).
- Vues, routes, JS, CSS, images de `design/` repris tels quels dans `portail/resources` et `portail/public`. Toutes les pages vitrine (une cinquantaine) fonctionnent sans rien construire de plus.
- Modèles + migrations : `Etablissement`, `User` (+ `etablissement_id`, `jeton_session_actif`), `Classe` (code unique auto-généré), `Eleve` (identifiant unique, `code_secret` haché, `jeton_qr` à usage unique, `jeton_session_actif`), `Lab`, `Seance`, `LabSession`.
- Deux guards d'authentification (`config/auth.php`) : `web` (profs, email/mot de passe) et `eleve` (identifiant/code secret, sans e-mail — minimisation des données mineurs). `App\Models\Eleve` authentifiable via le trait `Illuminate\Auth\Authenticatable` et `getAuthPasswordName()`.
- **Session unique par compte** : trait `GereSessionUnique` (un jeton en base, recréé à chaque connexion) + middleware `VerifieSessionUnique` (`session.unique:web`/`session.unique:eleve`), qui déconnecte et prévient si le jeton de la session en cours ne correspond plus à celui, plus récent, enregistré en base.
- Connexion (`/connexion`, un seul écran, deux formulaires), inscription (`/inscription` : crée établissement + prof + première classe en une transaction), gestion de classe (`/mes-classes/...` : ajout rapide, import CSV, génération identifiant + code à 6 caractères + QR à usage unique via `bacon/bacon-qr-code` en SVG, fiches imprimables), salle d'attente de séance (`/seances/...`, rafraîchissement automatique, le prof admet).
- Service `GenerateurCompteEleve` : identifiant `prenom.initiale` (suffixe numérique si homonyme), code secret à 6 caractères sans caractères ambigus (`0/O`, `1/I/L`).
- 11 tests Pest (connexion prof, connexion élève, session unique × 2, création de classe × 3) : tous passent, base SQLite en mémoire.
- Déploiement : rôle Ansible `portail` (PHP 8.3 via Sury, Nginx, Composer avec vérification de signature officielle, Node 22 LTS via NodeSource, MariaDB+Redis de l'app en Docker Compose sur `127.0.0.1:3307`/`127.0.0.1:6380` — jamais sur `10.10.0.1`, réservé à Kolla), synchronisation du code par `rsync`, `.env` de production généré une seule fois (secrets jamais dans Git).
**Deux décisions de design tranchées par Adam**
1. Champ mot de passe ajouté au formulaire d'inscription (absent de la maquette) : sans lui, la connexion prof décrite dans le prompt n'aurait pas pu fonctionner sans serveur mail.
2. Écrans sans maquette (gestion des élèves, salle d'attente) : design minimal sobre, mêmes jetons visuels que le reste du site, pas de recherche d'effet « wow ».
**Problèmes rencontrés et solution**
1. `@auth` (sans argument) dans le layout partagé retournait `true` pour un **élève** connecté, affichant le menu du prof. Cause : le middleware `auth:eleve` appelle `Auth::shouldUse('eleve')`, ce qui change le *guard par défaut* pour le reste de la requête — `@auth` nu teste alors ce guard-là, pas forcément `web`. Corrigé en précisant partout `@auth('web')` / `auth('eleve')`.
2. Déploiement : j'ai modifié `kolla/globals.yml` et le script de sauvegarde dans le dépôt sans les repousser sur le serveur avant de relancer les outils correspondants (déjà rencontré à l'étape 7 — même piège, maintenant documenté deux fois).
3. `php artisan key:generate` échouait : lancé avant `composer install`, donc avant que `vendor/autoload.php` existe. Réordonné : code → composer install → clé/`.env` → npm → migrations.
4. `php artisan view:cache` échouait (« View path not found ») : `storage/framework/views` avait été exclu du `rsync` mais n'était créé par rien d'autre. Plus largement, **`storage/` et `bootstrap/cache/` ne doivent jamais être synchronisés depuis le poste de dev** (ce sont de l'état et du cache runtime, pas du code — un `rsync --delete` y effacerait un jour les travaux déposés par les élèves dans `storage/app/`). Corrigé : leur structure est créée une fois sur le serveur, propriété `www-data` dès le départ, et ils sont exclus en bloc du `rsync` (pas juste leurs sous-dossiers).
5. **Un handler Ansible qui notifie un autre handler ne s'est pas propagé avant la fin du play** : Nginx a continué de servir sa page par défaut malgré un déploiement sans erreur apparente. Corrigé en fusionnant les deux handlers (`nginx -t` puis `systemctl reload nginx`) en un seul. Détecté uniquement parce que le critère de fin a été vérifié sur le serveur réel, pas seulement sur le code local.
6. Vérification Playwright visuelle (demandée par le prompt) non faite : l'extension Chrome n'était pas connectée dans cette session. Vérification faite autrement : les mêmes fichiers Blade que `design/` sont repris sans changement de structure, et le flux complet a été vérifié par capture HTML (`curl`) côté connexion, tableau de bord et gestion de classe.
**Critère vérifié (sur le serveur réel, pas seulement en local)**
- 11/11 tests Pest passent (en local, avant déploiement).
- Un élève fictif (`lea.m`) s'est connecté sur `http://163.172.4.195/connexion` et a vu son tableau de bord (« Salut Léa », nom de sa classe) — données de test supprimées après coup (accord d'Adam).
- `nft list tables` après déploiement : toujours seulement `inet filter` + `ip nat` (+ `f2b-table`) — Nginx/PHP-FPM n'ont rien ajouté au pare-feu.
**Comprendre**
Cette étape a un piège intéressant pour la suite : Laravel a deux systèmes de comptes (profs et élèves), et un mécanisme Laravel conçu pour un seul système (« quel est l'utilisateur connecté ? ») suppose souvent qu'il n'y en a qu'un — le bug du menu qui affichait « Mes classes » à un élève connecté vient exactement de là. Chaque fois qu'on ajoutera un rôle (établissement, formateur, opérateur), il faudra se souvenir d'être explicite sur le guard, partout. L'autre leçon, plus générale : un déploiement qui se termine « sans erreur » ne veut pas dire que le résultat est correct — c'est en testant le vrai critère de fin sur le serveur réel (pas juste en relisant les logs Ansible) qu'on a trouvé le bug du handler Nginx.
**Suite**
Étape 9 : proxy console Go et xterm.js (jeton Redis à usage unique, relais WebSocket vers `nova-serialproxy`, lecture seule pour le prof).

97
docs/runbooks/portail.md Normal file
View file

@ -0,0 +1,97 @@
# Runbook — Portail Laravel
## Où sont les choses
| Élément | Emplacement |
|---|---|
| Code de l'application | `/var/www/portail` (propriétaire `adam`, sauf `storage/` et `bootstrap/cache/` → `www-data`) |
| Secrets Laravel (`.env`) | `/var/www/portail/.env`, `640 adam:www-data`, généré une seule fois, jamais dans Git |
| Secrets MariaDB/Redis de l'app | `/etc/portail/.env`, `600 root:root`, générés une seule fois |
| Config Docker Compose (MariaDB + Redis app) | `/etc/portail/docker-compose.yml` |
| Vhost Nginx | `/etc/nginx/sites-available/portail.conf` |
| PHP | 8.3 (dépôt Sury), FPM sur `/run/php/php8.3-fpm.sock` |
Déployé par `ansible/portail.yml` (rôle `portail`) :
```
cd ansible && script -q /dev/null ansible-playbook portail.yml -i inventory.ini
```
## Architecture de l'app
- Laravel 13, Blade + Vite (Tailwind v4 via le plugin Vite officiel).
- Deux comptes distincts, deux guards (`config/auth.php`) :
- `web` → `User` (profs), email + mot de passe.
- `eleve` → `Eleve` (élèves), identifiant (`prenom.initiale`, suffixe numérique si homonyme) + code secret à 6 caractères (haché).
- **Une seule session active par compte** (`App\Models\Concerns\GereSessionUnique` + middleware `session.unique`) : un jeton est stocké en session ET en base à chaque connexion ; s'ils ne correspondent plus, la session est coupée à la requête suivante avec un message.
- Vues reprises telles quelles depuis `design/` (voir le journal du 2026-10-07, étape 8, pour les deux écarts assumés : mot de passe ajouté au formulaire d'inscription, sélecteur d'établissement retiré du formulaire de connexion).
- Écrans sans maquette, construits sobres avec les jetons visuels existants : `resources/views/mes-classes/*` (gestion des élèves, import CSV, fiches QR) et `resources/views/seances/*` (salle d'attente).
## Base de données et cache de l'app
MariaDB et Redis **dédiés à l'app**, en Docker Compose, liés à `127.0.0.1` uniquement (jamais sur `10.10.0.1`, qui est le réseau de Kolla) :
- MariaDB : port `3307` (celui de Kolla est sur `10.10.0.1:3306`).
- Redis : port `6380`.
```
cd /etc/portail && docker compose ps
```
## Déployer une mise à jour du code
```
cd ansible && script -q /dev/null ansible-playbook portail.yml -i inventory.ini
```
Le rôle synchronise le code (`rsync`, sans `vendor/`, `node_modules/`, `.env`,
ni `storage/`/`bootstrap/cache/` — ce sont de l'état runtime, jamais du code),
puis `composer install --no-dev`, `npm ci && npm run build`,
`php artisan migrate --force`, et recrée les caches
(`config:cache`, `route:cache`, `view:cache`).
**Après toute modification de code qui touche les routes ou la config**, les
caches doivent être régénérés (le rôle le fait automatiquement). Si on modifie
une vue sans repasser par Ansible, penser à `php artisan view:clear` à la main.
## Lancer les tests (en local, avant tout déploiement)
```
cd portail && ./vendor/bin/pest
```
11 tests : connexion prof, connexion élève, session unique (prof et élève),
création de classe + établissement. Base de test : SQLite en mémoire
(`phpunit.xml`), aucun rapport avec MariaDB de prod.
## Écarts au design assumés le 2026-10-07
- `auth/inscription.blade.php` : champ mot de passe ajouté (absent de la
maquette), confirmé par Adam — sans lui, la connexion prof décrite dans le
prompt de déploiement n'aurait pas pu fonctionner sans serveur mail.
- `auth/connexion.blade.php` : le sélecteur d'établissement du formulaire
enseignant a été retiré (un prof qui se connecte a déjà un établissement en
base ; ce sélecteur n'avait de sens que pour l'inscription).
- `paillasse-contenus/README.md` corrigé : `tree` n'est pas préinstallé dans
l'image (le lab 02 demande à l'élève de l'installer lui-même).
- Gestion des élèves (ajout, import CSV, fiches QR) et salle d'attente :
aucune maquette fournie, design minimal sobre construit avec les jetons
visuels existants (confirmé par Adam).
## Piège vécu le 2026-10-07
Un handler Ansible qui notifie un autre handler (`recharger nginx` →
`redémarrer nginx`) ne s'est **pas** propagé avant la fin du play : Nginx a
continué de servir sa page par défaut malgré un déploiement « réussi ».
Corrigé en fusionnant les deux handlers en un seul
(`nginx -t && systemctl reload nginx`). Si un jour un changement Nginx semble
sans effet, vérifier d'abord `sudo nginx -T` sur le serveur, puis forcer
`sudo systemctl reload nginx` à la main.
## Vérifié le 2026-10-07
- `curl http://163.172.4.195/connexion` : les deux formulaires s'affichent.
- Un élève fictif (`lea.m` / code à 6 caractères) s'est connecté et a vu son
tableau de bord (« Salut Léa », nom de sa classe) — données de test
supprimées juste après (accord d'Adam).
- `nft list tables` après déploiement : toujours seulement `inet filter` et
`ip nat` (+ `f2b-table` de fail2ban) — Nginx n'a rien ajouté au pare-feu.