++ 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
|
pericolosa (`avviaAzzeramento`) crea *solo* il backup e si ferma in
|
||||||
`attesa-download`; la cancellazione parte da un secondo clic esplicito
|
`attesa-download`; la cancellazione parte da un secondo clic esplicito
|
||||||
(`confermaAzzeramento`), che rifiuta se lo stato non registra un
|
(`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
|
`streamDownload`, "dopo l'ultimo byte" — **non funziona**: con
|
||||||
`fastcgi_buffering on` (il default, e `docker/nginx/default.conf` non lo
|
`fastcgi_buffering on` (il default, e `docker/nginx/default.conf` non lo
|
||||||
disattiva) nginx accetta l'intera risposta dal processo PHP, che quindi
|
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,
|
*spedito* l'archivio, non che sia arrivato: quella verifica è dell'operatore,
|
||||||
e per questo la conferma è umana.
|
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
|
- **Nell'azzeramento i file si cancellano prima del database.** Svuotare il
|
||||||
disco è l'unico passo che può fallire per cause esterne (permessi sul
|
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
|
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:
|
`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'è.
|
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
|
2. L'operazione **si ferma e aspetta**: controlli il file scaricato, digiti la
|
||||||
con un secondo clic. Fino a lì non viene toccato nulla, e puoi annullare.
|
**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,
|
3. Solo dopo la conferma vengono eliminati territori, proclamatori,
|
||||||
assegnazioni, campagne, zone, tipologie, anni di servizio, il registro
|
assegnazioni, campagne, zone, tipologie, anni di servizio, il registro
|
||||||
attività e tutti i file caricati; le impostazioni tornano ai default e i
|
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
|
territori, miniature, logo) in un unico pacchetto scaricabile e ripristinabile
|
||||||
- Barra di avanzamento reale su backup e ripristino (lavoro svolto, non tempo)
|
- Barra di avanzamento reale su backup e ripristino (lavoro svolto, non tempo)
|
||||||
- Ripristino da pacchetto, con cambio password obbligatorio per tutti gli utenti
|
- Ripristino da pacchetto, con cambio password obbligatorio per tutti gli utenti
|
||||||
- **Zona pericolosa**: azzeramento completo con backup obbligatorio scaricato
|
- **Zona pericolosa**: azzeramento completo con backup obbligatorio scaricato,
|
||||||
e confermato prima di procedere
|
password dell'amministratore e conferma esplicita prima di procedere
|
||||||
- Import ed export XML dell'intero set di dati gestionali (senza i file)
|
- Import ed export XML dell'intero set di dati gestionali (senza i file)
|
||||||
- Conversione di dump SQL legacy in XML compatibile
|
- Conversione di dump SQL legacy in XML compatibile
|
||||||
- Import massivo dei PDF dei territori da cartella o archivio ZIP
|
- 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\CreateBackupArchive;
|
||||||
use App\Jobs\ResetApplication;
|
use App\Jobs\ResetApplication;
|
||||||
use App\Services\BackupOperationState;
|
use App\Services\BackupOperationState;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,8 +28,26 @@ class BackupManager extends Component
|
|||||||
/** Frase da digitare per sbloccare l'azzeramento. */
|
/** Frase da digitare per sbloccare l'azzeramento. */
|
||||||
public const CONFERMA_AZZERAMENTO = 'AZZERA TUTTO';
|
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 = '';
|
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
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->authorize('settings.manage');
|
$this->authorize('settings.manage');
|
||||||
@@ -79,7 +99,7 @@ class BackupManager extends Component
|
|||||||
* resterebbe indistinguibile da uno riuscito. Chiedere all'operatore e'
|
* resterebbe indistinguibile da uno riuscito. Chiedere all'operatore e'
|
||||||
* l'unico controllo che copre anche quel caso.
|
* l'unico controllo che copre anche quel caso.
|
||||||
*/
|
*/
|
||||||
public function confermaAzzeramento(): void
|
public function confermaAzzeramento(string $password = ''): void
|
||||||
{
|
{
|
||||||
$this->authorize('settings.manage');
|
$this->authorize('settings.manage');
|
||||||
|
|
||||||
@@ -97,6 +117,10 @@ class BackupManager extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->passwordConfermata($password)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
app(BackupOperationState::class)->update($this->operationId, function (array $state) {
|
||||||
$state['fase'] = 'Azzeramento in avvio';
|
$state['fase'] = 'Azzeramento in avvio';
|
||||||
$state['processed'] = 0;
|
$state['processed'] = 0;
|
||||||
@@ -110,6 +134,56 @@ class BackupManager extends Component
|
|||||||
$this->aggiorna();
|
$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
|
* Annulla un azzeramento fermo in attesa di conferma. Il backup resta
|
||||||
* scaricabile: e' comunque un backup valido.
|
* scaricabile: e' comunque un backup valido.
|
||||||
|
|||||||
@@ -26,6 +26,23 @@
|
|||||||
uploadInCorso: false,
|
uploadInCorso: false,
|
||||||
uploadPercento: 0,
|
uploadPercento: 0,
|
||||||
erroreUpload: '',
|
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) {
|
inviaRipristino(event) {
|
||||||
const form = event.target;
|
const form = event.target;
|
||||||
@@ -115,6 +132,22 @@
|
|||||||
ed è l’ultima possibilità di tornare indietro.
|
ed è l’ultima possibilità di tornare indietro.
|
||||||
</p>
|
</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">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<a href="{{ route('backup.file', $operationId) }}"
|
<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;">
|
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>
|
</a>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
wire:click="confermaAzzeramento"
|
x-on:click="azzera()"
|
||||||
wire:confirm="Hai verificato il backup scaricato? Confermando, i dati vengono cancellati definitivamente."
|
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;">
|
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
|
Ho salvato il backup, azzera tutto
|
||||||
</button>
|
</button>
|
||||||
@@ -262,7 +296,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ol class="text-xs text-gray-600 list-decimal list-inside space-y-1">
|
<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><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>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>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>
|
<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