++ fix e controlli
This commit is contained in:
251
app/Livewire/Settings/ZonaPericolosa.php
Normal file
251
app/Livewire/Settings/ZonaPericolosa.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user