From fd15903c9c3da78de61e8221c5abbd19f7f5a294 Mon Sep 17 00:00:00 2001
From: Francesco Picone
Date: Wed, 29 Jul 2026 15:27:11 +0200
Subject: [PATCH] ++ fix richiesta password prima di azzeramento applicazione
---
HANDOFF.md | 26 ++++++-
README.md | 10 ++-
app/Livewire/Settings/BackupManager.php | 76 ++++++++++++++++++-
.../settings/backup-manager.blade.php | 40 +++++++++-
4 files changed, 143 insertions(+), 9 deletions(-)
diff --git a/HANDOFF.md b/HANDOFF.md
index fc72718..39ca0bd 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -291,7 +291,8 @@ Scelte non ovvie:
pericolosa (`avviaAzzeramento`) crea *solo* il backup e si ferma in
`attesa-download`; la cancellazione parte da un secondo clic esplicito
(`confermaAzzeramento`), che rifiuta se lo stato non registra un
- `servito_at`. Il primo tentativo faceva partire il reset dalla coda dello
+ `servito_at` e se non riceve la password dell'amministratore. Il primo
+ tentativo faceva partire il reset dalla coda dello
`streamDownload`, "dopo l'ultimo byte" — **non funziona**: con
`fastcgi_buffering on` (il default, e `docker/nginx/default.conf` non lo
disattiva) nginx accetta l'intera risposta dal processo PHP, che quindi
@@ -300,6 +301,29 @@ Scelte non ovvie:
*spedito* l'archivio, non che sia arrivato: quella verifica è dell'operatore,
e per questo la conferma è umana.
+- **La password si richiede al secondo clic, e non passa dallo snapshot.** La
+ sessione è già autenticata, quindi la password non serve a stabilire *chi*
+ sia: serve a stabilire che davanti allo schermo ci sia ancora quella persona
+ — una scheda lasciata aperta su una postazione condivisa è lo scenario contro
+ cui protegge. Viaggia come **parametro** di `confermaAzzeramento($password)`,
+ non come proprietà `wire:model`: le proprietà pubbliche di Livewire vengono
+ serializzate nello snapshot e rispedite al browser a ogni risposta, e una
+ password non ha motivo di fare quel viaggio di ritorno.
+
+ Attenzione al dettaglio che costa un pomeriggio: la proprietà
+ `$passwordConferma` **esiste comunque, e resta sempre vuota**. Livewire
+ scarta dalla risposta ogni errore la cui chiave non corrisponda a una
+ proprietà del componente (`SupportValidation::dehydrate()` filtra su
+ `Utils::hasProperty`), quindi senza quella riga `addError('passwordConferma',
+ …)` sparisce in silenzio: il pulsante sembra non fare nulla, mentre il
+ controllo sta funzionando. Non collegarla al campo con `wire:model`, o la
+ password torna nello snapshot.
+
+ Cinque tentativi sbagliati in 5 minuti bloccano l'azione (`RateLimiter`,
+ chiave per utente). Senza, la richiesta di password su una sessione dirottata
+ sarebbe solo un rallentamento. Il blocco è sull'operazione, non sull'account:
+ nessuno resta chiuso fuori dall'applicazione.
+
- **Nell'azzeramento i file si cancellano prima del database.** Svuotare il
disco è l'unico passo che può fallire per cause esterne (permessi sul
volume), e fallire lì non ha ancora toccato niente. Nell'ordine inverso un
diff --git a/README.md b/README.md
index beec95c..ba873c7 100644
--- a/README.md
+++ b/README.md
@@ -433,8 +433,10 @@ l'applicazione allo stato di installazione appena eseguita. Va scritto
`AZZERA TUTTO` per sbloccare il pulsante, e l'operazione procede in due tempi:
1. Viene creato e scaricato un backup completo di tutto ciò che c'è.
-2. L'operazione **si ferma e aspetta**: controlli il file scaricato e confermi
- con un secondo clic. Fino a lì non viene toccato nulla, e puoi annullare.
+2. L'operazione **si ferma e aspetta**: controlli il file scaricato, digiti la
+ **tua password** e confermi con un secondo clic. Fino a lì non viene toccato
+ nulla, e puoi annullare. Dopo cinque password sbagliate l'azzeramento resta
+ bloccato per 5 minuti (l'accesso all'applicazione no).
3. Solo dopo la conferma vengono eliminati territori, proclamatori,
assegnazioni, campagne, zone, tipologie, anni di servizio, il registro
attività e tutti i file caricati; le impostazioni tornano ai default e i
@@ -624,8 +626,8 @@ docker compose --profile dev up -d --build
territori, miniature, logo) in un unico pacchetto scaricabile e ripristinabile
- Barra di avanzamento reale su backup e ripristino (lavoro svolto, non tempo)
- Ripristino da pacchetto, con cambio password obbligatorio per tutti gli utenti
-- **Zona pericolosa**: azzeramento completo con backup obbligatorio scaricato
- e confermato prima di procedere
+- **Zona pericolosa**: azzeramento completo con backup obbligatorio scaricato,
+ password dell'amministratore e conferma esplicita prima di procedere
- Import ed export XML dell'intero set di dati gestionali (senza i file)
- Conversione di dump SQL legacy in XML compatibile
- Import massivo dei PDF dei territori da cartella o archivio ZIP
diff --git a/app/Livewire/Settings/BackupManager.php b/app/Livewire/Settings/BackupManager.php
index 6d13bf6..6cebd00 100644
--- a/app/Livewire/Settings/BackupManager.php
+++ b/app/Livewire/Settings/BackupManager.php
@@ -5,6 +5,8 @@ namespace App\Livewire\Settings;
use App\Jobs\CreateBackupArchive;
use App\Jobs\ResetApplication;
use App\Services\BackupOperationState;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
/**
@@ -26,8 +28,26 @@ class BackupManager extends Component
/** 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');
@@ -79,7 +99,7 @@ class BackupManager extends Component
* resterebbe indistinguibile da uno riuscito. Chiedere all'operatore e'
* l'unico controllo che copre anche quel caso.
*/
- public function confermaAzzeramento(): void
+ public function confermaAzzeramento(string $password = ''): void
{
$this->authorize('settings.manage');
@@ -97,6 +117,10 @@ class BackupManager extends Component
return;
}
+ if (! $this->passwordConfermata($password)) {
+ return;
+ }
+
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
$state['fase'] = 'Azzeramento in avvio';
$state['processed'] = 0;
@@ -110,6 +134,56 @@ class BackupManager extends Component
$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.
+ *
+ * La password arriva come parametro del metodo e non come proprieta' del
+ * componente: le proprieta' pubbliche di Livewire vengono serializzate
+ * nello snapshot e rispedite al browser a ogni risposta, e una password
+ * non ha motivo di fare quel viaggio di ritorno.
+ */
+ 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.
diff --git a/resources/views/livewire/settings/backup-manager.blade.php b/resources/views/livewire/settings/backup-manager.blade.php
index 836fa17..802b16e 100644
--- a/resources/views/livewire/settings/backup-manager.blade.php
+++ b/resources/views/livewire/settings/backup-manager.blade.php
@@ -26,6 +26,23 @@
uploadInCorso: false,
uploadPercento: 0,
erroreUpload: '',
+ passwordAzzeramento: '',
+
+ azzera() {
+ if (!this.passwordAzzeramento) {
+ return;
+ }
+
+ if (!confirm('Hai verificato il backup scaricato? Confermando, i dati vengono cancellati definitivamente.')) {
+ return;
+ }
+
+ const password = this.passwordAzzeramento;
+ // Svuotato subito: se la password è sbagliata si ridigita, e nel
+ // frattempo non resta nel campo di una scheda lasciata aperta.
+ this.passwordAzzeramento = '';
+ $wire.confermaAzzeramento(password);
+ },
inviaRipristino(event) {
const form = event.target;
@@ -115,6 +132,22 @@
ed è l’ultima possibilità di tornare indietro.
+ {{-- La password sta in Alpine, non in una proprietà del
+ componente: le proprietà pubbliche di Livewire tornano
+ al browser dentro lo snapshot a ogni risposta, e una
+ password non ha motivo di fare quel viaggio. Passata
+ come parametro del metodo, viaggia solo in andata. --}}
+
Viene creato e scaricato un backup completo di tutto ciò che c’è adesso.
-
L’operazione si ferma e aspetta: controlli il file scaricato e confermi. Fino a quel clic non viene toccato nulla, e puoi annullare.
+
L’operazione si ferma e aspetta: controlli il file scaricato, digiti la tua password e confermi. Fino a quel clic non viene toccato nulla, e puoi annullare.
Vengono eliminati territori, proclamatori, assegnazioni, campagne, zone, tipologie, anni di servizio, il registro attività e tutti i file caricati.
Le impostazioni tornano ai valori di default e i ruoli vengono riseminati.
Tutti gli altri utenti vengono eliminati. Resta solo il tuo account di amministratore, con la password attuale — come un’installazione nuova, che ha un amministratore solo.