Dossier personnel de l'élève monté comme lecteur réseau : rclone sert le dossier local, l'authentification est déléguée au portail via auth_request Nginx (mêmes identifiants que la connexion), sans nouveau système de comptes. Nettoyé par la purge RGPD comme les autres données d'élève. Corrige au passage un vrai bug d'isolation trouvé en testant avec deux comptes jetables : rewrite injectait la variable posée par auth_request_set, mais rewrite s'exécute dans une phase Nginx antérieure à cette variable, donc toujours vide - un élève pouvait lire le fichier d'un autre en devinant son nom. Corrigé par une location à regex nommée + proxy_pass. Détails : ADR 0008. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
3 KiB
PHP
76 lines
3 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Classe;
|
|
use App\Models\RenduDevoir;
|
|
use App\Services\CacheTableauDeBordProf;
|
|
use App\Services\Stockage\StockageTravauxEleves;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
/**
|
|
* RGPD (CLAUDE.md, étape 14) : supprime les classes archivées depuis
|
|
* 90 jours, avec en cascade leurs élèves, séances, sessions de lab, machines,
|
|
* corrections, quiz et devoirs maison (chantier « vrai cockpit »), ainsi que
|
|
* les annonces, observations, le chat élève ↔ prof (séparation vitrine/
|
|
* application, 2026-10-13), les événements d'examen (2026-10-14) et le
|
|
* dossier WebDAV de chaque élève (bloc C, 2026-10-14). Le compte du prof
|
|
* est conservé. Le journal ne garde que des identifiants et des nombres,
|
|
* jamais de nom d'élève.
|
|
*
|
|
* Les sauvegardes chiffrées contenant encore ces données expirent d'elles-
|
|
* mêmes au plus 4 semaines plus tard (rotation, docs/runbooks/sauvegardes.md).
|
|
*/
|
|
class PurgerClassesArchivees extends Command
|
|
{
|
|
protected $signature = 'classes:purger-archivees';
|
|
|
|
protected $description = 'Supprime les classes archivées depuis 90 jours et les données de leurs élèves';
|
|
|
|
public function handle(): void
|
|
{
|
|
$classes = Classe::whereNotNull('archivee_a')
|
|
->where('archivee_a', '<=', now()->subDays(Classe::DELAI_PURGE_JOURS))
|
|
->withCount('eleves')
|
|
->with('eleves:id,classe_id')
|
|
->get();
|
|
|
|
$purgees = 0;
|
|
|
|
foreach ($classes as $classe) {
|
|
// Ne peut arriver qu'en cas d'anomalie (un lab expire en 45 min) :
|
|
// on ne supprime pas une session dont la stack Heat vivrait encore.
|
|
if ($classe->aDesLabsEnCours()) {
|
|
Log::warning('Purge RGPD reportée : lab en cours', ['classe_id' => $classe->id]);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Les fichiers d'Object Storage et les dossiers WebDAV ne sont
|
|
// pas liés par une clé étrangère : à supprimer nous-mêmes avant
|
|
// la cascade en base.
|
|
RenduDevoir::whereHas('devoir', fn ($q) => $q->where('classe_id', $classe->id))
|
|
->whereNotNull('fichier_chemin')
|
|
->pluck('fichier_chemin')
|
|
->each(fn (string $chemin) => StockageTravauxEleves::supprimer($chemin));
|
|
|
|
foreach ($classe->eleves as $eleve) {
|
|
Storage::disk('webdav')->deleteDirectory((string) $eleve->id);
|
|
}
|
|
|
|
DB::transaction(fn () => $classe->delete());
|
|
CacheTableauDeBordProf::vider($classe->user_id);
|
|
Log::info('Purge RGPD : classe supprimée', [
|
|
'classe_id' => $classe->id,
|
|
'eleves' => $classe->eleves_count,
|
|
'archivee_a' => $classe->archivee_a->toDateString(),
|
|
]);
|
|
$purgees++;
|
|
}
|
|
|
|
$this->info("{$purgees} classe(s) purgée(s).");
|
|
}
|
|
}
|