252 lines
8.7 KiB
PHP
252 lines
8.7 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Settings;
|
|
|
|
use App\Jobs\CreateBackupArchive;
|
|
use App\Jobs\ResetApplication;
|
|
use App\Livewire\Concerns\SegueOperazioneBackup;
|
|
use App\Services\BackupOperationState;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Livewire\Component;
|
|
|
|
/**
|
|
* Azzeramento dell'applicazione: l'unica operazione irreversibile della pagina,
|
|
* e per questo l'ultima zona, dopo tutte le altre.
|
|
*
|
|
* E' un componente separato da BackupManager per una ragione di sola
|
|
* disposizione: la zona pericolosa deve stare in fondo alla pagina, mentre le
|
|
* sezioni di backup e ripristino stanno in cima. Rendendola dallo stesso
|
|
* componente finiva a meta' pagina, in mezzo alle sezioni di import, che e'
|
|
* l'ultimo posto in cui si vuole trovare un pulsante che cancella tutto.
|
|
*
|
|
* Lo stato dell'operazione resta condiviso con BackupManager attraverso
|
|
* BackupOperationState (il trait), quindi i due non hanno bisogno di parlarsi per
|
|
* restare coerenti: il posto unico e' uno solo per entrambi.
|
|
*
|
|
* L'azzeramento ha due tempi, separati dal download dell'archivio:
|
|
*
|
|
* 1. avviaAzzeramento() crea il backup e si ferma in attesa-download
|
|
* 2. confermaAzzeramento() cancella, solo su conferma umana e con la password
|
|
*
|
|
* Il primo tentativo faceva partire la cancellazione dalla coda dello
|
|
* streamDownload, "dopo l'ultimo byte": non funziona. Con fastcgi_buffering
|
|
* attivo (il default) nginx accetta l'intera risposta dal processo PHP, che
|
|
* prosegue anche se il browser ha chiuso la connessione al primo byte, quindi un
|
|
* download annullato azzerava lo stesso. Il server puo' verificare di aver
|
|
* *spedito* l'archivio, non che sia arrivato: quella verifica e' dell'operatore,
|
|
* e per questo la conferma e' umana.
|
|
*/
|
|
class ZonaPericolosa extends Component
|
|
{
|
|
use SegueOperazioneBackup;
|
|
|
|
/** Frase da digitare per sbloccare l'azzeramento. */
|
|
public const CONFERMA_AZZERAMENTO = 'AZZERA TUTTO';
|
|
|
|
/** Password sbagliate ammesse prima di bloccare l'azzeramento, e per quanto. */
|
|
private const TENTATIVI_PASSWORD = 5;
|
|
private const FINESTRA_TENTATIVI = 300;
|
|
|
|
public string $confermaReset = '';
|
|
|
|
/**
|
|
* Esiste solo per dare un nome agli errori del campo password: Livewire
|
|
* scarta dal messaggio di risposta ogni errore la cui chiave non
|
|
* corrisponda a una proprieta' del componente (SupportValidation::dehydrate),
|
|
* quindi senza questa riga "Password non corretta" non arriverebbe mai a
|
|
* schermo e il pulsante sembrerebbe non fare nulla.
|
|
*
|
|
* NON collegarla al campo con wire:model: la password viaggia come
|
|
* parametro di confermaAzzeramento() proprio per non finire nello
|
|
* snapshot, che Livewire rimanda al browser a ogni risposta. Qui resta
|
|
* sempre stringa vuota.
|
|
*/
|
|
public string $passwordConferma = '';
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->authorize('settings.manage');
|
|
|
|
$this->agganciaOperazioneCorrente();
|
|
}
|
|
|
|
/**
|
|
* Solo l'azzeramento: backup e ripristino li racconta BackupManager. Vedi
|
|
* SegueOperazioneBackup::tipiSeguiti().
|
|
*/
|
|
protected function tipiSeguiti(): array
|
|
{
|
|
return [BackupOperationState::TIPO_RESET];
|
|
}
|
|
|
|
/**
|
|
* Primo tempo: si costruisce il backup e lo si scarica. La cancellazione non
|
|
* parte da qui.
|
|
*/
|
|
public function avviaAzzeramento(): void
|
|
{
|
|
$this->authorize('settings.manage');
|
|
|
|
$this->validate(
|
|
['confermaReset' => 'required|in:' . self::CONFERMA_AZZERAMENTO],
|
|
['confermaReset.in' => 'Scrivi esattamente "' . self::CONFERMA_AZZERAMENTO . '" per confermare.',
|
|
'confermaReset.required' => 'Scrivi "' . self::CONFERMA_AZZERAMENTO . '" per confermare.']
|
|
);
|
|
|
|
if (! $this->postoLibero()) {
|
|
return;
|
|
}
|
|
|
|
$this->confermaReset = '';
|
|
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_RESET);
|
|
|
|
CreateBackupArchive::dispatch($this->operationId, true);
|
|
|
|
$this->aggiorna();
|
|
|
|
// La sezione di backup deve sapere che il posto e' occupato, altrimenti
|
|
// continuerebbe a offrire il proprio pulsante durante l'azzeramento.
|
|
// Indirizzato e non gridato: senza ->to() l'evento tornerebbe anche qui.
|
|
$this->dispatch('operazione-avviata')->to(BackupManager::class);
|
|
}
|
|
|
|
/**
|
|
* Secondo tempo: parte solo su conferma esplicita, dopo che l'archivio e'
|
|
* stato servito.
|
|
*/
|
|
public function confermaAzzeramento(string $password = ''): void
|
|
{
|
|
$this->authorize('settings.manage');
|
|
|
|
$this->aggiorna();
|
|
|
|
if (! $this->attesaDownload) {
|
|
$this->addError('confermaReset', 'Nessun azzeramento in attesa di conferma.');
|
|
|
|
return;
|
|
}
|
|
|
|
if (($this->operazione['result']['servito_at'] ?? null) === null) {
|
|
$this->addError('confermaReset', 'Scarica prima il backup: senza, l\'azzeramento non parte.');
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $this->passwordConfermata($password)) {
|
|
return;
|
|
}
|
|
|
|
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
|
$state['fase'] = 'Azzeramento in avvio';
|
|
$state['processed'] = 0;
|
|
$state['total'] = 0;
|
|
|
|
return $state;
|
|
});
|
|
|
|
ResetApplication::dispatch($this->operationId, (int) auth()->id());
|
|
|
|
$this->aggiorna();
|
|
}
|
|
|
|
/**
|
|
* Verifica la password dell'amministratore prima della cancellazione.
|
|
*
|
|
* A questo punto la sessione e' gia' autenticata, quindi la password non
|
|
* serve a stabilire *chi* sia: serve a stabilire che davanti allo schermo
|
|
* ci sia ancora quella persona. E' l'ultimo passo prima di un'operazione
|
|
* irreversibile, e una scheda lasciata aperta su una postazione condivisa e'
|
|
* lo scenario esatto contro cui protegge.
|
|
*/
|
|
private function passwordConfermata(string $password): bool
|
|
{
|
|
$utente = auth()->user();
|
|
$chiave = 'azzeramento-password:' . $utente->id;
|
|
|
|
// Senza un limite, la richiesta di password su una sessione dirottata
|
|
// sarebbe solo un rallentamento: si proverebbero le password comuni a
|
|
// raffica. Il blocco e' sull'azione, non sull'account, quindi nessuno
|
|
// resta chiuso fuori dall'applicazione.
|
|
if (RateLimiter::tooManyAttempts($chiave, self::TENTATIVI_PASSWORD)) {
|
|
$this->addError('passwordConferma', sprintf(
|
|
'Troppi tentativi. Riprova fra %d secondi.',
|
|
RateLimiter::availableIn($chiave)
|
|
));
|
|
|
|
return false;
|
|
}
|
|
|
|
if ($password === '') {
|
|
$this->addError('passwordConferma', 'Inserisci la tua password per confermare.');
|
|
|
|
return false;
|
|
}
|
|
|
|
if (! Hash::check($password, $utente->getAuthPassword())) {
|
|
RateLimiter::hit($chiave, self::FINESTRA_TENTATIVI);
|
|
$this->addError('passwordConferma', 'Password non corretta.');
|
|
|
|
return false;
|
|
}
|
|
|
|
RateLimiter::clear($chiave);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Annulla un azzeramento fermo in attesa di conferma. Il backup resta
|
|
* scaricabile: e' comunque un backup valido.
|
|
*/
|
|
public function annullaAzzeramento(): void
|
|
{
|
|
$this->authorize('settings.manage');
|
|
|
|
if (! $this->attesaDownload) {
|
|
return;
|
|
}
|
|
|
|
app(BackupOperationState::class)->markCompleted($this->operationId, [
|
|
'messaggio' => 'Azzeramento annullato. Il backup appena creato resta scaricabile.',
|
|
]);
|
|
|
|
// Da qui in poi e' un backup come un altro: la sezione di backup mostra il
|
|
// pulsante di download, non piu' la conferma di cancellazione.
|
|
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
|
$state['tipo'] = BackupOperationState::TIPO_BACKUP;
|
|
|
|
return $state;
|
|
});
|
|
|
|
$this->aggiorna();
|
|
$this->dispatch('operazione-avviata')->to(BackupManager::class);
|
|
}
|
|
|
|
/**
|
|
* Un azzeramento avviato altrove non esiste, ma il posto puo' essere occupato
|
|
* da un backup o da un ripristino: la zona deve saperlo per non offrire un
|
|
* pulsante che verrebbe rifiutato.
|
|
*/
|
|
#[\Livewire\Attributes\On('operazione-avviata')]
|
|
public function agganciaAltrove(): void
|
|
{
|
|
$this->agganciaOperazioneCorrente();
|
|
}
|
|
|
|
/**
|
|
* Vero quando questa zona sta seguendo un azzeramento (non un backup o un
|
|
* ripristino avviati altrove).
|
|
*/
|
|
public function getAzzeramentoInCorsoProperty(): bool
|
|
{
|
|
return $this->operationId !== null
|
|
&& ($this->operazione['tipo'] ?? null) === BackupOperationState::TIPO_RESET;
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.settings.zona-pericolosa');
|
|
}
|
|
}
|