diff --git a/application/config/nis2_sources.php b/application/config/nis2_sources.php index b781e95..f241273 100644 --- a/application/config/nis2_sources.php +++ b/application/config/nis2_sources.php @@ -85,7 +85,7 @@ return [ 'key' => 'acn_specifiche_base_2025', 'short' => 'Specifiche di base ACN (Framework Nazionale 2025)', 'citation' => 'ACN, Misure di sicurezza di base NIS - Allegati 1 e 2 alla Determinazione n. 164179/2025', - 'full' => 'Misure di sicurezza di base (Determinazione ACN 164179/2025) articolate secondo il Framework Nazionale per la Cybersecurity e la Data Protection (ed. 2025): funzioni Governance (GV), Identificazione (ID), Protezione (PR), Rilevazione (DE), Risposta (RS), Ripristino (RC), poi categorie, sottocategorie e requisiti. Soggetti importanti: 37 misure / 92 requisiti (Allegato 1). Soggetti essenziali: 43 misure / 116 requisiti (Allegato 2), codifica identica con requisiti aggiuntivi. Esempi: GV.RR-04 (cybersicurezza nelle risorse umane, adeguatezza al ruolo), GV.PO-01 (policy di gestione del rischio). Adozione: 18 mesi dalla notifica di inserimento nell elenco NIS.', + 'full' => 'Misure di sicurezza di base (Determinazione ACN 164179/2025) articolate secondo il Framework Nazionale per la Cybersecurity e la Data Protection (ed. 2025): funzioni Governance (GV), Identificazione (ID), Protezione (PR), Rilevazione (DE), Risposta (RS), Ripristino (RC), poi categorie, sottocategorie e requisiti. Soggetti importanti: 37 misure / 92 requisiti (Allegato 1). Soggetti essenziali: 43 misure / 116 requisiti (Allegato 2), codifica identica con requisiti aggiuntivi. Esempi: GV.RR-04 (cybersicurezza nelle risorse umane, adeguatezza al ruolo), GV.PO-01 (policy di gestione del rischio). Adozione: 18 mesi dalla notifica di inserimento nell elenco NIS. NOTA DI PROVENIENZA: il prodotto adotta 92 requisiti per i soggetti importanti come da file del referente compliance (Simon); la pagina ufficiale ACN riporta 87 - divergenza nota e accettata, prevale il dato del referente; da riconciliare sul testo ufficiale.', 'file' => 'docs/nis2/Determina164179_apr2025.pdf', 'authority' => 'Agenzia per la Cybersicurezza Nazionale (ACN)', 'url' => 'https://www.acn.gov.it/portale/nis/modalita-specifiche-base', diff --git a/application/controllers/StakeholderActivityController.php b/application/controllers/StakeholderActivityController.php index fb3460d..ed3dd29 100644 --- a/application/controllers/StakeholderActivityController.php +++ b/application/controllers/StakeholderActivityController.php @@ -22,6 +22,7 @@ */ require_once __DIR__ . '/BaseController.php'; +require_once APP_PATH . '/services/EmailService.php'; class StakeholderActivityController extends BaseController { @@ -424,7 +425,13 @@ class StakeholderActivityController extends BaseController ? date('Y-m-d H:i:s', strtotime($a['due_date'] . ' +30 days')) : date('Y-m-d H:i:s', strtotime('+90 days')); + $emailOn = defined('EMAIL_SENDING_ENABLED') && EMAIL_SENDING_ENABLED; + $base = defined('APP_URL') ? rtrim(APP_URL, '/') : 'https://nis2.agile.software'; + $emailer = $emailOn ? new EmailService() : null; // istanziato solo se l'invio è abilitato + $isReadAck = ($a['type'] === 'read_ack'); + $links = []; + $emailSent = 0; foreach ($targets as $g) { $token = bin2hex(random_bytes(24)); // 48 hex Database::update('stk_activity_targets', [ @@ -433,6 +440,14 @@ class StakeholderActivityController extends BaseController 'sent_at' => date('Y-m-d H:i:s'), 'token_expires_at' => $expires, ], 'id = ?', [(int) $g['id']]); + $absLink = $base . '/stk-portal.html?t=' . $token; + // Invio email gated dal kill-switch: EmailService::send rispetta EMAIL_SENDING_ENABLED + // (con kill-switch OFF non parte nulla; pronto per il go-live). Solo se c'è un'email. + if ($emailer && !empty($g['contact_email']) && filter_var($g['contact_email'], FILTER_VALIDATE_EMAIL)) { + if ($emailer->send($g['contact_email'], $this->mailSubject($isReadAck, $a['title']), $this->mailBody($isReadAck, $a['title'], $g['stakeholder_name'], $absLink))) { + $emailSent++; + } + } $links[] = [ 'target_id' => (int) $g['id'], 'stakeholder_name' => $g['stakeholder_name'], @@ -442,16 +457,34 @@ class StakeholderActivityController extends BaseController } Database::update('stk_activities', ['status' => 'sent'], 'id = ? AND organization_id = ?', [$id, $orgId]); - $emailOn = defined('EMAIL_SENDING_ENABLED') && EMAIL_SENDING_ENABLED; - $this->logAudit('stk_activity_sent', 'stk_activity', $id, ['targets' => count($links), 'email_enabled' => $emailOn]); + $this->logAudit('stk_activity_sent', 'stk_activity', $id, ['targets' => count($links), 'email_enabled' => $emailOn, 'email_sent' => $emailSent]); $this->jsonSuccess([ 'sent' => count($links), - 'email_sent' => false, + 'email_sent' => $emailSent, + 'email_enabled' => $emailOn, 'links' => $links, 'note' => $emailOn - ? 'Invio email non ancora collegato: condividi i magic-link con gli stakeholder.' + ? ($emailSent . ' email inviate ai contatti disponibili; condividi i magic-link con gli stakeholder senza email.') : 'Email disattivate (kill-switch): copia e condividi manualmente i magic-link qui sotto.', - ], 'Attività inviata (magic-link generati)'); + ], 'Attività inviata'); + } + + /** Oggetto email invito (gated): questionario o presa visione. */ + private function mailSubject(bool $isReadAck, string $title): string + { + return ($isReadAck ? 'Presa visione richiesta: ' : 'Questionario NIS2: ') . $title; + } + + /** Corpo HTML email con il magic-link (EmailService applica il wrap/branding). */ + private function mailBody(bool $isReadAck, string $title, string $name, string $link): string + { + $esc = static fn($s) => htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); + $azione = $isReadAck ? 'prendere visione del documento e confermarne la lettura' : 'compilare il questionario'; + return '

Gentile ' . $esc($name) . ',

' + . '

nell\'ambito degli adempimenti NIS2 della nostra organizzazione, ti chiediamo di ' . $azione . ': ' . $esc($title) . '.

' + . '

Apri

' + . '

Oppure copia questo link: ' . $esc($link) . '

' + . '

Il link è personale: non inoltrarlo.

'; } // ───────────────────────────────────────────────────────────────────────── diff --git a/public/sw.js b/public/sw.js index dea13ca..939efeb 100644 --- a/public/sw.js +++ b/public/sw.js @@ -13,7 +13,7 @@ * Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json), * cosi' activate elimina automaticamente la shell vecchia. */ -const CACHE = 'nis2-shell-v1.21.2'; +const CACHE = 'nis2-shell-v1.21.3'; const PRECACHE = [ '/offline.html', diff --git a/public/version.json b/public/version.json index 1ac0aba..b67d2c2 100644 --- a/public/version.json +++ b/public/version.json @@ -1 +1 @@ -{"version": "1.21.2", "build": "2026-06-16-v1.21.2", "date": "2026-06-16", "changelog": "Epic C / C5 — fix open-item post-verifica: upload allegati RIPRISTINATO in prod (ACL host www-data su public/uploads; risolve anche evidence/visure) — collaudato. UI: create+assign attività non lascia più attività orfane su errore (no duplicati, messaggio chiaro) e richiede destinatari per assegnazione individuale; blocco PWA di stakeholder-activities allineato (favicon-16, status-bar, application-name). Additivo."} +{"version": "1.21.3", "build": "2026-06-16-v1.21.3", "date": "2026-06-16", "changelog": "Epic C / C5 chiusura open-items: aggancio invio email questionari/firma-lettura in send() (GATED dal kill-switch EMAIL_SENDING_ENABLED=false -> nessun invio finora, pronto per go-live; magic-link assoluto via APP_URL). nis2_sources.php: nota di provenienza 92 vs 87 (prevale dato referente Simon). [host] EMAIL_MS_URL spostato sul path interno; pre-equip PHP-CA DB confermato (TLS1.3 attivo). Additivo."}