Files
termanager2/app/Jobs/ResetApplication.php
Francesco Picone 93bc9cf2fe ++ gruppi: vista del sorvegliante compilabile, azzeramento completo, test sistemati
Nella vista Il mio gruppo un @php(...) in linea prima di un blocco
@php ... @endphp mandava in errore il compilatore di Blade. L'azzeramento
dell'applicazione ora cancella anche i gruppi di servizio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:59:31 +02:00

206 lines
8.7 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\AnnoTeocratico;
use App\Models\Assegnazione;
use App\Models\Campagna;
use App\Models\GruppoServizio;
use App\Models\Proclamatore;
use App\Models\Setting;
use App\Models\Territorio;
use App\Models\TerritorioMappa;
use App\Models\TerritorioPdfVersione;
use App\Models\Tipologia;
use App\Models\User;
use App\Models\Zona;
use App\Services\BackupArchive;
use App\Services\BackupOperationState;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
/**
* Riporta l'installazione allo stato di primo avvio: nessun dato gestionale,
* nessun file caricato, impostazioni ai valori di default, ruoli e permessi
* riseminati.
*
* Non e' reversibile e non fa backup da solo: parte solo dopo che l'archivio
* generato da CreateBackupArchive risulta scaricato per intero (vedi
* BackupController::file), cosi' non esiste una via che azzeri senza che una
* copia sia gia' al sicuro sul computer di chi ha premuto il pulsante.
*/
class ResetApplication implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public int $tries = 1;
public int $timeout = 570;
/**
* @param int $actorId L'amministratore che ha chiesto l'azzeramento.
* E' l'unico account che sopravvive: un'installazione
* appena creata ha un amministratore, e cancellarli
* tutti lascerebbe l'applicazione senza nessuno che
* possa accedere fino al riavvio del container.
*/
public function __construct(
public string $operationId,
public int $actorId,
) {
}
public function handle(BackupOperationState $stato, BackupArchive $archivio): void
{
$stato->markRunning($this->operationId);
try {
// I file per primi, prima di toccare il database: svuotare il disco
// e' l'unico passo che puo' fallire per cause esterne (permessi sul
// volume), e fallire qui non ha ancora cancellato niente. Nell'ordine
// inverso un errore sui file lascerebbe un'applicazione con il
// database azzerato e il disco pieno di PDF che non appartengono
// piu' a nessun territorio.
$stato->fase($this->operationId, 'Cancellazione dei file caricati', 1);
$fileRimossi = $archivio->svuotaDiscoPubblico();
$stato->avanza($this->operationId, 1);
$stato->log($this->operationId, $fileRimossi . ' file rimossi dal disco.');
$stato->fase($this->operationId, 'Cancellazione dei dati gestionali', 11);
DB::transaction(function () use ($stato) {
$passi = [
'Assegnazioni' => fn () => Assegnazione::query()->forceDelete(),
'Campagne' => fn () => Campagna::query()->delete(),
// Prima dei territori, e per esteso: le piantine hanno la
// chiave a null on delete, quindi sopravvivevano scollegate
// e con l'immagine che puntava a un file appena cancellato.
'Piantine' => fn () => TerritorioMappa::query()->delete(),
'Storico dei PDF' => fn () => TerritorioPdfVersione::query()->delete(),
'Territori' => fn () => Territorio::query()->withTrashed()->forceDelete(),
'Gruppi di servizio' => fn () => GruppoServizio::query()->delete(),
'Proclamatori' => fn () => Proclamatore::query()->withTrashed()->forceDelete(),
'Tipologie' => fn () => Tipologia::query()->delete(),
'Zone' => fn () => Zona::query()->delete(),
'Anni teocratici' => fn () => AnnoTeocratico::query()->delete(),
'Impostazioni' => fn () => Setting::query()->delete(),
];
$fatti = 0;
foreach ($passi as $etichetta => $azione) {
$azione();
$stato->avanza($this->operationId, ++$fatti);
$stato->log($this->operationId, $etichetta . ': azzerate.');
}
});
// Il mass delete non emette eventi, quindi l'istanza memoizzata di
// Setting punta ancora alla riga cancellata: senza questo, il
// firstOrCreate piu' sotto tornerebbe il fantasma invece di
// ricreare la riga.
Setting::flushCache();
// Area della città e anteprime delle pubblicazioni stanno sul disco
// privato, che svuotaDiscoPubblico() non tocca: restavano lì e
// l'editor ripartiva con la città di prima.
Storage::disk('local')->deleteDirectory('piantine');
Storage::disk('local')->deleteDirectory('piantine-anteprime');
$stato->log($this->operationId, 'Area della città e anteprime delle piantine rimosse.');
$stato->fase($this->operationId, 'Rimozione degli altri utenti');
$utentiRimossi = $this->rimuoviAltriUtenti($stato);
// Il registro si svuota *dopo* le cancellazioni, non prima: gli
// altri utenti sono modelli con LogsActivity, quindi eliminarli
// scrive righe nuove che una pulizia anticipata non intercetta e
// che resterebbero li' come unica traccia — un residuo, non una
// scelta. Svuotato per ultimo, l'unica riga che sopravvive e'
// quella scritta apposta qui sotto.
$stato->fase($this->operationId, 'Pulizia del registro attivita\'', 1);
DB::table('activity_log')->delete();
activity()
->causedBy(User::find($this->actorId))
->log('Applicazione azzerata e riportata allo stato iniziale');
$stato->avanza($this->operationId, 1);
$stato->fase($this->operationId, 'Ripristino di ruoli e impostazioni', 3);
// Ruoli e permessi non vengono cancellati, ma il seeder e' additivo
// e idempotente: rieseguirlo garantisce il set di partenza anche se
// qualcuno ne aveva rimosso qualcuno a mano.
(new RolesAndPermissionsSeeder())->run();
$stato->avanza($this->operationId, 1);
Setting::instance();
$stato->avanza($this->operationId, 2);
$attore = User::find($this->actorId);
if ($attore && ! $attore->hasRole('amministratore')) {
$attore->assignRole('amministratore');
}
$stato->avanza($this->operationId, 3);
} catch (\Throwable $e) {
Log::error('Azzeramento dell\'applicazione non riuscito.', ['exception' => $e]);
$stato->markFailed($this->operationId, $e->getMessage());
return;
}
$messaggio = sprintf(
'Applicazione azzerata: dati gestionali rimossi, %d file cancellati, %d utenti eliminati. '
. 'Resta il tuo account di amministratore.',
$fileRimossi,
$utentiRimossi
);
$stato->log($this->operationId, $messaggio);
$stato->markCompleted($this->operationId, ['messaggio' => $messaggio]);
}
/**
* Vedi CreateBackupArchive::failed(): chiude l'operazione quando il job
* muore per cause che handle() non intercetta, liberando il posto unico.
*/
public function failed(?\Throwable $e): void
{
app(BackupOperationState::class)->markFailed(
$this->operationId,
($e?->getMessage() ?: 'Azzeramento interrotto (timeout o errore del worker).')
. ' Controlla lo stato dei dati prima di ritentare.'
);
}
/**
* Cancella ogni utente tranne chi ha chiesto l'azzeramento.
*
* Uno alla volta e via Eloquent, non con un delete di massa: il trait
* HasRoles di Spatie stacca ruoli e permessi nell'evento `deleting`, che un
* delete di massa non fa scattare — resterebbero righe orfane in
* model_has_roles pronte a essere riassegnate al primo utente che riceve un
* id gia' usato.
*/
private function rimuoviAltriUtenti(BackupOperationState $stato): int
{
$altri = User::query()->where('id', '!=', $this->actorId)->get();
$stato->avanza($this->operationId, 0, $altri->count());
$rimossi = 0;
foreach ($altri as $utente) {
$utente->delete();
$stato->avanza($this->operationId, ++$rimossi);
}
return $rimossi;
}
}