Dépôt de fichier en plus du texte libre pour les devoirs maison, stocké sur Hetzner Object Storage (jamais sur le disque du serveur), masqué côté élève tant que le bucket n'est pas configuré. Purge RGPD étendue pour supprimer aussi les fichiers d'Object Storage. Ansible lit les identifiants depuis un fichier que seul Adam peut créer sur le serveur (runbook docs/runbooks/stockage-s3.md) ; le bloc B du CLAUDE.md est maintenant complet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
2.6 KiB
PHP
68 lines
2.6 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;
|
|
|
|
/**
|
|
* 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) et les événements d'examen (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')
|
|
->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 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));
|
|
|
|
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).");
|
|
}
|
|
}
|