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'); } }