From 034329471b9e26406397ef0c023d1de104a4bdd9 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Mon, 19 Oct 2026 15:58:35 +0200 Subject: [PATCH] ++ informativa privacy aggiornata, valori dalle impostazioni - Dati trattati attuali: email e telefono cifrati, link alla piantina, stato dell'invio, conferma email, log senza nomi. - Valori letti al momento: congregazione, conservazione del log, scadenza del link, download del PDF, server di posta, cifratura, durata della sessione, indirizzo/HTTPS, ruoli con i permessi veri, conteggi dei dati. - Servizi esterni: WhatsApp, posta, OpenStreetMap. - Dichiarato che backup ed export XML contengono i dati in chiaro. README e HANDOFF aggiornati. Co-Authored-By: Claude Opus 5.5 --- HANDOFF.md | 26 +++++ README.md | 21 +++- app/Livewire/Privacy.php | 76 +++++++++++++- resources/views/livewire/privacy.blade.php | 81 ++++++++++++--- tests/Feature/PrivacyTest.php | 111 +++++++++++++++++++++ 5 files changed, 291 insertions(+), 24 deletions(-) create mode 100644 tests/Feature/PrivacyTest.php diff --git a/HANDOFF.md b/HANDOFF.md index 918375f..6887b0d 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -127,6 +127,32 @@ email di prova e di conferma arrivate, link aperto senza login, account sbloccato; riquadro dopo l'assegnazione con WhatsApp e scelta. Configurazione e dati di prova rimossi dopo. +### Informativa privacy aggiornata e dinamica (19 ottobre 2026) + +Richiesta dell'utente: aggiornare la sezione privacy con i dati attuali e +prendere i parametri dalle impostazioni. Il testo era fermo a una versione +precedente (solo nome e cognome, "tre ruoli: Admin, Servitore di Territorio, +Proclamatore Semplificato" che non esistono). + +- `App\Livewire\Privacy` legge al momento: congregazione, + `audit_retention_days`, `giorni_per_smarrito` (scadenza del link), + `pdf_viewer_show_download`, server di posta (`ConfigurazionePosta`), + `config('app.cipher')`, `config('session.lifetime')`, `APP_URL` (avvisa se + non è HTTPS), i ruoli con i permessi veri (`Privacy::PERMESSI` li spiega) e + quanti dati sono conservati (contati in SQL: i campi cifrati vuoti restano + NULL, non serve decifrare). `Privacy::REVISIONE` è la data del testo. +- Aggiunte: email/telefono cifrati, link alla piantina, invio via WhatsApp + (parte dal telefono di chi assegna) ed email (server di posta configurato), + mappe OpenStreetMap, conferma email, blocco dopo troppi tentativi. +- Titolare e base giuridica sono rimasti com'erano: sono valutazioni legali, + da far rivedere a chi ne ha la competenza. +- **Aperto — backup in chiaro**: il pacchetto di backup usa l'export XML, che + legge i proclamatori dal modello e quindi esporta nome, cognome, email e + telefono **decifrati**. L'informativa ora lo dice; valutare se cifrare + l'archivio (per esempio con una password scelta al download). +- `PrivacyTest` verifica che i valori cambino con le impostazioni, i ruoli, + i conteggi senza nomi e l'accesso solo amministratore. + ### Pagina Assegna più leggera; "Vedi tutti" in testata nella Home (19 ottobre 2026) Segnalazione dell'utente: la pagina Assegnazioni era lenta. Misurato su dev diff --git a/README.md b/README.md index 3282ff3..392acd0 100644 --- a/README.md +++ b/README.md @@ -794,8 +794,8 @@ una revoca ha effetto immediato senza bisogno di ricaricare la pagina. ## Sicurezza e GDPR ### Sicurezza -- **Cifratura a riposo**: nome e cognome dei proclamatori cifrati con AES-256-CBC - tramite `APP_KEY` +- **Cifratura a riposo**: nome, cognome e i facoltativi email e telefono dei + proclamatori cifrati con AES-256-CBC tramite `APP_KEY` - **Password**: hash bcrypt - **RBAC**: middleware `permission:` sulle rotte, riapplicato anche alle richieste Livewire @@ -811,13 +811,24 @@ una revoca ha effetto immediato senza bisogno di ricaricare la pagina. restituisce 404 ### GDPR -- **Minimizzazione**: dei proclamatori si conservano solo nome, cognome e stato +- **Informativa** (menu *Privacy*, solo amministratore): i valori che possono + cambiare — congregazione, conservazione del log, scadenza dei link, + download del PDF, server di posta, durata della sessione, indirizzo e HTTPS, + ruoli con i loro permessi, quanti dati sono conservati — si leggono al + momento da impostazioni e configurazione (`App\Livewire\Privacy`) +- **Minimizzazione**: dei proclamatori si conservano nome, cognome, stato e, + solo se servono per mandare il link, email e telefono - **Cifratura a riposo**: i dati sono illeggibili anche con accesso diretto al DB - **Audit trail**: registro completo delle operazioni -- **Diritto all'oblio**: funzione di anonimizzazione irreversibile +- **Diritto all'oblio**: funzione di anonimizzazione irreversibile, che + cancella anche email e telefono - **Retention**: cancellazione automatica dei log oltre il periodo configurato - **No dati sensibili nei log**: l'audit registra gli identificativi, non i nomi - in chiaro + in chiaro; l'email con il link parte subito, mai in coda, perché il job + salverebbe l'indirizzo in chiaro +- **Attenzione ai backup**: il pacchetto `.zip` e l'export XML contengono i dati + dei proclamatori **in chiaro** (l'export li legge decifrati). Vanno custoditi + in un luogo protetto e cancellati quando non servono --- diff --git a/app/Livewire/Privacy.php b/app/Livewire/Privacy.php index a9cb2f0..a97aaf0 100644 --- a/app/Livewire/Privacy.php +++ b/app/Livewire/Privacy.php @@ -2,18 +2,86 @@ namespace App\Livewire; -use Livewire\Component; +use App\Models\Assegnazione; +use App\Models\Proclamatore; use App\Models\Setting; +use App\Models\User; +use App\Services\ConfigurazionePosta; +use Livewire\Component; +use Spatie\Activitylog\Models\Activity; +use Spatie\Permission\Models\Role; +/** + * Informativa privacy (solo amministratore). + * + * Tutto ciò che può cambiare si legge qui, al momento: impostazioni + * (congregazione, conservazione del log, scadenza dei link, download del + * PDF, server di posta), configurazione (indirizzo, cifratura, durata della + * sessione), ruoli con i loro permessi reali e quanti dati sono conservati. + * Il testo resta nella vista; i numeri non vanno mai scritti a mano. + */ class Privacy extends Component { + /** Data dell'ultima revisione del testo dell'informativa. */ + public const REVISIONE = '19 ottobre 2026'; + + /** I permessi spiegati a chi legge l'informativa. */ + public const PERMESSI = [ + 'settings.manage' => 'impostazioni, utenti, backup e import', + 'proclamatori.manage' => 'anagrafica dei proclamatori, contatti compresi', + 'territori.manage' => 'gestione dei territori e delle piantine', + 'campagne.manage' => 'campagne', + 'registro.view' => 'consultazione del registro delle assegnazioni', + 'registro.export' => 'esportazione del registro', + 'territori.assign' => 'assegnazione dei territori', + 'territori.return' => 'rientro dei territori', + 'audit.view' => 'consultazione del registro delle attività', + 'audit.export' => 'esportazione del registro delle attività', + ]; + public function render() { - $settings = Setting::instance(); + $impostazioni = Setting::instance(); + $posta = app(ConfigurazionePosta::class); + + $hostPosta = $posta->configurataNellApp() + ? (string) $impostazioni->mail_host + : (string) config('mail.mailers.smtp.host'); + + $indirizzo = (string) config('app.url'); return view('livewire.privacy', [ - 'congregazione' => $settings->congregazione_nome ?? 'Congregazione', - 'auditRetention' => $settings->audit_retention_days ?? 365, + 'revisione' => self::REVISIONE, + 'congregazione' => $impostazioni->congregazione_nome ?: 'Congregazione', + 'indirizzo' => $indirizzo, + 'https' => str_starts_with($indirizzo, 'https://'), + 'cifratura' => (string) config('app.cipher'), + 'durataSessione' => (int) config('session.lifetime'), + 'auditRetention' => (int) ($impostazioni->audit_retention_days ?: 365), + 'scadenzaLink' => max(1, (int) ($impostazioni->giorni_per_smarrito ?: 120)), + 'downloadPdf' => (bool) $impostazioni->pdf_viewer_show_download, + 'postaAttiva' => $posta->funzionante(), + 'hostPosta' => $hostPosta !== '' ? $hostPosta : null, + 'ruoli' => Role::with('permissions')->orderBy('name')->get()->map(fn (Role $r) => [ + 'nome' => $r->name, + 'permessi' => $r->permissions->pluck('name') + ->map(fn ($p) => self::PERMESSI[$p] ?? $p) + ->all(), + ]), + // Quanti dati sono conservati adesso. I campi cifrati restano NULL + // quando sono vuoti, quindi si contano in SQL senza decifrare. + 'conteggi' => [ + 'proclamatori' => Proclamatore::count(), + 'proclamatoriAttivi' => Proclamatore::where('attivo', true)->count(), + 'conEmail' => Proclamatore::whereNotNull('email')->count(), + 'conTelefono' => Proclamatore::whereNotNull('telefono')->count(), + 'anonimizzati' => Proclamatore::onlyTrashed()->count(), + 'utenti' => User::count(), + 'assegnazioni' => Assegnazione::count(), + 'assegnazioniAperte' => Assegnazione::aperte()->count(), + 'attivita' => Activity::count(), + 'attivitaDal' => Activity::min('created_at'), + ], ]); } } diff --git a/resources/views/livewire/privacy.blade.php b/resources/views/livewire/privacy.blade.php index f5a4ceb..a35e388 100644 --- a/resources/views/livewire/privacy.blade.php +++ b/resources/views/livewire/privacy.blade.php @@ -1,6 +1,9 @@
+ {{-- Ogni valore che può cambiare arriva da Privacy::render() (impostazioni, + configurazione, ruoli, conteggi): qui non si scrivono numeri a mano. --}}
-

Informativa Privacy

+

Informativa Privacy

+

Testo rivisto il {{ $revisione }} · valori aggiornati in tempo reale dalle impostazioni

@@ -13,10 +16,21 @@

Dati trattati

    -
  • Proclamatori: nome e cognome — conservati in forma crittografata (AES-256-CBC) nel database.
  • -
  • Territori: numero, località, zona, tipologia, mappe PDF — nessun dato personale.
  • -
  • Assegnazioni: collegamento tra proclamatore e territorio con date di assegnazione e rientro.
  • -
  • Utenti: nome, email e ruolo per l'accesso all'applicazione.
  • +
  • Proclamatori: nome e cognome; facoltativamente email e numero di telefono, usati solo per mandare il link della piantina. Tutti cifrati nel database ({{ $cifratura }}).
  • +
  • Territori e piantine: numero, zona, tipologia, confini, mappe e PDF — nessun dato personale.
  • +
  • Assegnazioni: collegamento tra proclamatore e territorio, con date di assegnazione e rientro, conteggio nelle campagne, codice del link alla piantina e stato del suo invio (inviato o no, e con quale canale).
  • +
  • Utenti dell'applicazione: nome, email (che va confermata), password conservata solo come impronta non reversibile, ruolo.
  • +
  • Registro delle attività: chi ha fatto cosa e quando. I proclamatori vi compaiono solo con un identificativo numerico, mai con nome, numero o email.
  • +
+
+ +
+

Dati conservati oggi

+
    +
  • {{ $conteggi['proclamatori'] }} proclamatori, di cui {{ $conteggi['proclamatoriAttivi'] }} attivi; {{ $conteggi['conEmail'] }} con email e {{ $conteggi['conTelefono'] }} con numero di telefono; {{ $conteggi['anonimizzati'] }} nel cestino o anonimizzati.
  • +
  • {{ $conteggi['assegnazioni'] }} assegnazioni nello storico, di cui {{ $conteggi['assegnazioniAperte'] }} in corso.
  • +
  • {{ $conteggi['utenti'] }} utenti dell'applicazione.
  • +
  • {{ $conteggi['attivita'] }} voci nel registro delle attività@if($conteggi['attivitaDal']), dal {{ \Illuminate\Support\Carbon::parse($conteggi['attivitaDal'])->format('d/m/Y') }}@endif.
@@ -25,6 +39,7 @@

I dati personali sono trattati esclusivamente per:

  • Organizzare l'assegnazione e il rientro dei territori di predicazione.
  • +
  • Mandare al proclamatore il link della piantina del territorio assegnato.
  • Monitorare la copertura territoriale per anno teocratico.
  • Gestire le campagne speciali di predicazione.
@@ -35,24 +50,60 @@

Il trattamento è basato sul legittimo interesse dell'organizzazione religiosa nella gestione delle proprie attività interne (Art. 6(1)(f) GDPR) e sull'appartenenza volontaria dei proclamatori alla congregazione (Art. 9(2)(d) GDPR).

+
+

Chi accede ai dati

+

L'accesso è riservato agli utenti dell'applicazione, ciascuno secondo il proprio ruolo:

+
    + @foreach($ruoli as $ruolo) +
  • {{ ucfirst($ruolo['nome']) }}: {{ $ruolo['permessi'] ? implode('; ', $ruolo['permessi']) : 'nessun permesso' }}.
  • + @endforeach +
+

Il proclamatore non ha un account: riceve solo il link alla piantina del territorio che gli è assegnato.

+
+

Misure di sicurezza

    -
  • Crittografia: i dati personali (nome/cognome) sono crittografati con AES-256-CBC prima della memorizzazione.
  • -
  • Controllo accessi: sistema RBAC con tre ruoli (Admin, Servitore di Territorio, Proclamatore Semplificato).
  • -
  • Audit trail: tutte le operazioni sono registrate e conservate per {{ $auditRetention }} giorni.
  • -
  • Comunicazioni sicure: l'accesso avviene tramite HTTPS.
  • -
  • Hosting locale: i dati risiedono sull'infrastruttura della congregazione, non su servizi cloud di terze parti.
  • +
  • Cifratura: nome, cognome, email e telefono dei proclamatori sono cifrati con {{ $cifratura }} prima di essere salvati; la chiave non sta nel database.
  • +
  • Accesso: password personale, conferma dell'email, blocco temporaneo dopo troppi tentativi; la sessione scade dopo {{ $durataSessione }} minuti di inattività.
  • +
  • Permessi: ogni ruolo vede e modifica solo ciò che gli serve (vedi sopra).
  • +
  • Registro delle attività: le operazioni sono registrate e conservate per {{ $auditRetention }} giorni.
  • +
  • Comunicazioni: l'applicazione risponde all'indirizzo {{ $indirizzo }}@if($https), su connessione cifrata (HTTPS)@else. Attenzione: l'indirizzo non usa HTTPS, le comunicazioni non sono cifrate.@endif.
  • +
  • Hosting: i dati risiedono sul server gestito dagli amministratori dell'applicazione, non su servizi cloud di terze parti.
  • +
+
+ +
+

Link alla piantina

+
    +
  • Il link è personale e funziona solo finché il territorio è assegnato, al massimo per {{ $scadenzaLink }} giorni dall'assegnazione.
  • +
  • Mostra la piantina del territorio, senza dati personali; il PDF {{ $downloadPdf ? 'si può anche scaricare' : 'si può solo consultare, non scaricare' }}.
  • +
+
+ +
+

Servizi esterni

+
    +
  • WhatsApp: se si sceglie questo canale, il messaggio con il nome del proclamatore e il link parte dal WhatsApp di chi assegna il territorio, non dall'applicazione.
  • +
  • Email: + @if($postaAttiva) + il link viene inviato dal server di posta {{ $hostPosta ?? 'configurato' }}, che riceve l'indirizzo e il nome del proclamatore. + @else + nessun server di posta configurato: l'applicazione non invia email. + @endif +
  • +
  • Mappe: le piantine usano le mappe di OpenStreetMap; per disegnarle si scaricano tessere dai server di OpenStreetMap, senza inviare dati personali.

Conservazione dei dati

    -
  • I dati dei proclamatori inattivi possono essere anonimizzati su richiesta.
  • -
  • I territori eliminati sono conservati nel cestino (soft-delete) prima dell'eliminazione definitiva.
  • -
  • I log di audit sono conservati per {{ $auditRetention }} giorni, configurabili nelle impostazioni.
  • -
  • Lo storico assegnazioni è conservato per la durata necessaria alla gestione territoriale.
  • +
  • I dati di un proclamatore possono essere anonimizzati su richiesta: nome, cognome, email e telefono vengono cancellati in modo irreversibile.
  • +
  • Territori e proclamatori eliminati restano nel cestino finché non vengono eliminati definitivamente.
  • +
  • Il registro delle attività è conservato per {{ $auditRetention }} giorni (modificabile nelle impostazioni), poi cancellato automaticamente.
  • +
  • Lo storico delle assegnazioni è conservato per la durata necessaria alla gestione territoriale.
  • +
  • I backup e gli export XML scaricati dagli amministratori contengono i dati dei proclamatori in chiaro (nome, cognome, email, telefono): vanno custoditi in un luogo protetto e cancellati quando non servono più.
@@ -65,7 +116,7 @@
  • Cancellazione: richiedere l'anonimizzazione o la rimozione dei propri dati.
  • Portabilità: richiedere l'esportazione dei propri dati.
  • -

    Le richieste possono essere inoltrate al Servitore di Territorio o all'Amministratore dell'applicazione.

    +

    Le richieste vanno rivolte agli amministratori dell'applicazione della congregazione {{ $congregazione }}.

    diff --git a/tests/Feature/PrivacyTest.php b/tests/Feature/PrivacyTest.php new file mode 100644 index 0000000..5cb193a --- /dev/null +++ b/tests/Feature/PrivacyTest.php @@ -0,0 +1,111 @@ +withoutVite(); + $this->seed(RolesAndPermissionsSeeder::class); + + $this->admin = User::create(['name' => 'Admin', 'email' => 'admin@example.test', 'password' => 'password-di-prova']); + $this->admin->assignRole('amministratore'); + } + + public function test_i_valori_arrivano_dalle_impostazioni(): void + { + Setting::firstOrCreate([], ['congregazione_nome' => 'x'])->update([ + 'congregazione_nome' => 'Congregazione Zisa', + 'audit_retention_days' => 400, + 'giorni_per_smarrito' => 95, + 'pdf_viewer_show_download' => false, + ]); + + $this->actingAs($this->admin)->get(route('privacy')) + ->assertOk() + ->assertSee('Congregazione Zisa') + ->assertSee('conservato per') + ->assertSee('400 giorni') + ->assertSee('al massimo per 95 giorni') + ->assertSee('si può solo consultare, non scaricare'); + } + + public function test_cambiare_un_impostazione_cambia_l_informativa(): void + { + $impostazioni = Setting::firstOrCreate([], ['congregazione_nome' => 'Prova']); + $impostazioni->update(['pdf_viewer_show_download' => true, 'giorni_per_smarrito' => 60]); + + $this->actingAs($this->admin)->get(route('privacy')) + ->assertSee('al massimo per 60 giorni') + ->assertSee('si può anche scaricare'); + } + + public function test_i_ruoli_sono_quelli_veri_con_i_loro_permessi(): void + { + Setting::firstOrCreate([], ['congregazione_nome' => 'Prova']); + + $this->actingAs($this->admin)->get(route('privacy')) + ->assertSee('Amministratore') + ->assertSee('Assistente') + ->assertSee('Operatore') + ->assertSee('assegnazione dei territori') + ->assertDontSee('Servitore di Territorio') + ->assertDontSee('Proclamatore Semplificato'); + } + + public function test_conta_i_dati_conservati_senza_mostrarli(): void + { + Setting::firstOrCreate([], ['congregazione_nome' => 'Prova']); + Proclamatore::create(['nome' => 'Mario', 'cognome' => 'Rossi', 'attivo' => true, 'email' => 'mario@example.test', 'telefono' => '3331234567']); + Proclamatore::create(['nome' => 'Anna', 'cognome' => 'Verdi', 'attivo' => false]); + + $this->actingAs($this->admin)->get(route('privacy')) + ->assertSee('2 proclamatori, di cui 1 attivi; 1 con email e 1 con numero di telefono') + ->assertDontSee('Rossi') + ->assertDontSee('mario@example.test'); + } + + public function test_senza_posta_dice_che_non_si_inviano_email(): void + { + Setting::firstOrCreate([], ['congregazione_nome' => 'Prova']); + config(['mail.default' => 'log']); + + $this->actingAs($this->admin)->get(route('privacy')) + ->assertSee('nessun server di posta configurato'); + } + + public function test_con_la_posta_dell_app_ne_mostra_il_server(): void + { + Setting::firstOrCreate([], ['congregazione_nome' => 'Prova']) + ->update(['mail_host' => 'smtp.example.test', 'mail_port' => 587]); + + $this->actingAs($this->admin)->get(route('privacy')) + ->assertSee('smtp.example.test'); + } + + public function test_e_riservata_all_amministratore(): void + { + Setting::firstOrCreate([], ['congregazione_nome' => 'Prova']); + $operatore = User::create(['name' => 'Op', 'email' => 'op@example.test', 'password' => 'password-di-prova']); + $operatore->assignRole('operatore'); + + $this->actingAs($operatore)->get(route('privacy'))->assertForbidden(); + } +}