++ fix richiesta password prima di azzeramento applicazione
This commit is contained in:
26
HANDOFF.md
26
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
|
||||
|
||||
10
README.md
10
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
|
||||
{{-- 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. --}}
|
||||
<div class="max-w-xs">
|
||||
<label for="passwordConferma" class="block text-xs font-medium text-amber-900">
|
||||
Conferma con la tua password
|
||||
</label>
|
||||
<input type="password" id="passwordConferma" autocomplete="current-password"
|
||||
x-model="passwordAzzeramento"
|
||||
x-on:keydown.enter.prevent="azzera()"
|
||||
class="mt-1 block w-full rounded-lg border-amber-300 shadow-sm focus:ring-red-500 focus:border-red-500 text-sm">
|
||||
@error('passwordConferma') <p class="mt-1 text-xs font-medium text-red-700">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<a href="{{ route('backup.file', $operationId) }}"
|
||||
style="display:inline-flex;align-items:center;gap:6px;background:#fff;color:#92400e;border:1px solid #d97706;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:500;text-decoration:none;">
|
||||
@@ -122,8 +155,9 @@
|
||||
</a>
|
||||
|
||||
<button type="button"
|
||||
wire:click="confermaAzzeramento"
|
||||
wire:confirm="Hai verificato il backup scaricato? Confermando, i dati vengono cancellati definitivamente."
|
||||
x-on:click="azzera()"
|
||||
x-bind:disabled="!passwordAzzeramento"
|
||||
x-bind:style="!passwordAzzeramento ? 'opacity:.5;cursor:not-allowed;' : ''"
|
||||
style="display:inline-flex;align-items:center;gap:6px;background:#b91c1c;color:#fff;border:1px solid #7f1d1d;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;">
|
||||
Ho salvato il backup, azzera tutto
|
||||
</button>
|
||||
@@ -262,7 +296,7 @@
|
||||
</p>
|
||||
<ol class="text-xs text-gray-600 list-decimal list-inside space-y-1">
|
||||
<li><strong>Viene creato e scaricato un backup completo</strong> di tutto ciò che c’è adesso.</li>
|
||||
<li>L’operazione si ferma e aspetta: <strong>controlli il file scaricato e confermi</strong>. Fino a quel clic non viene toccato nulla, e puoi annullare.</li>
|
||||
<li>L’operazione si ferma e aspetta: <strong>controlli il file scaricato, digiti la tua password e confermi</strong>. Fino a quel clic non viene toccato nulla, e puoi annullare.</li>
|
||||
<li>Vengono eliminati territori, proclamatori, assegnazioni, campagne, zone, tipologie, anni di servizio, il registro attività e <strong>tutti i file caricati</strong>.</li>
|
||||
<li>Le impostazioni tornano ai valori di default e i ruoli vengono riseminati.</li>
|
||||
<li><strong>Tutti gli altri utenti vengono eliminati.</strong> Resta solo il tuo account di amministratore, con la password attuale — come un’installazione nuova, che ha un amministratore solo.</li>
|
||||
|
||||
Reference in New Issue
Block a user