++ add backup totale applicativo

This commit is contained in:
2026-07-29 12:34:38 +02:00
parent 2fa76d5073
commit c6abef977d
14 changed files with 864 additions and 11 deletions

View File

@@ -248,6 +248,60 @@ Tre ritocchi minori ma con qualche implicazione:
`permission:settings.manage` e il link in sidebar è sotto
`@can('settings.manage')`.
### Backup completo `.zip`: dati + file, e cambio password forzato al restore
L'export XML da solo non ricostruisce un'installazione: `pdf_path`,
`thumbnail_path` e `logo_path` sono percorsi sul disco `public`, quindi un
ripristino su volumi vuoti lascia ogni territorio che punta a un file
inesistente — link pubblici a 404 e miniature rotte. Il pacchetto `.zip`
(`Services/BackupArchive`, `Http/Controllers/Settings/BackupController`) mette
insieme `manifest.json`, `data.xml` (lo stesso XML dell'export) e `storage/…`
con **tutti** i file del disco `public`. L'export XML resta, etichettato come
parziale.
Scelte non ovvie:
- **Rotte HTTP, non Livewire.** Lo `streamDownload` di Livewire passa il file
base64 dentro la risposta JSON: un archivio con i PDF dei territori non ci
sta. `backup/download` e `backup/restore` sono due rotte normali nel gruppo
`settings.manage`. Per lo stesso motivo `ZipArchive::addFile()` (che legge
dal disco in `close()`) e gli stream in estrazione: nessun PDF passa intero
per la memoria del processo.
- **Ordine del restore: staging → database → file.** I file si estraggono
prima in `storage/app/backup-restore/<uuid>`, così un archivio corrotto o
fuori limite fallisce mentre i dati correnti sono ancora intatti; poi
`importXmlFromContent()` in transazione; poi la sostituzione dei file. Il
disco `public` viene **svuotato**, non fuso: i percorsi vivono nelle righe
appena riscritte, e tenere i vecchi file lascerebbe PDF orfani che nessuna
schermata mostra e nessuna cancellazione raggiunge.
- **Limiti sull'archivio in ingresso.** Un `.zip` è compresso: senza tetti su
numero di entry, dimensione della singola entry e totale decompresso, pochi
MB possono riempire il volume (zip bomb). I nomi delle entry sono
normalizzati e ogni `..` o percorso assoluto viene rifiutato (zip slip) —
senza, un archivio ostile scriverebbe fuori dalla cartella di destinazione.
- **Dopo un restore tutti cambiano password.** Il pacchetto contiene gli hash
bcrypt di ogni utente: chi lo ha avuto in mano ha potuto tentarne il crack
offline, quindi da lì in poi nessuna vecchia password è più un segreto. Il
restore accende `password_change_required` su **tutte** le righe di `users`
(amministratore che sta ripristinando compreso) e azzera i `remember_token`,
che sono credenziali persistenti e stanno pure loro nel backup. Il
middleware `RequirePasswordChange` (in coda al gruppo `web`, quindi copre
anche `/livewire/update` — Livewire segue le risposte redirezionate) dirotta
su `password.change` tutto tranne quella pagina, il suo invio e il logout.
- **La schermata di cambio password non è Livewire e non chiede la password
attuale.** Non è Livewire perché deve restare raggiungibile mentre il
middleware respinge anche le POST di Livewire, e distinguere quale
componente sta dietro una `/livewire/update` è fragile; due rotte HTTP si
escludono per nome. Non chiede la password attuale perché il restore ha
appena sovrascritto gli hash con quelli del backup: la "password attuale" di
un account è quella dell'epoca del backup, che l'utente — l'amministratore
per primo — può benissimo non ricordare. La garanzia è la sessione: a quella
pagina ci si arriva già autenticati.
### Export/Import XML: backup completo e round-trip
`Livewire/Settings/XmlExchange` esporta e reimporta **tutti** i dati gestionali,