[FEAT] Epic C / C5 — chiusura open-items: email gated + fonti + provenienza 92/87
- #7 (go-live ready): send() aggancia l'invio email del questionario/firma-lettura via EmailService, GATED dal kill-switch EMAIL_SENDING_ENABLED (oggi=false → ZERO invii; pronto al go-live). Magic-link assoluto via APP_URL; invio solo ai target con email valida; i link restano sempre disponibili per la condivisione manuale. Smoke prod: email_sent=0, email_enabled=false (nessun invio). - #2 (prevale Simon): nis2_sources.php — aggiunta NOTA DI PROVENIENZA sul 92 vs 87 (prodotto usa 92 = file referente; ACN ufficiale 87; divergenza nota/accettata). Numero NON modificato per decisione confermata. Note operative (host, fuori repo): - #8: EMAIL_MS_URL spostato sul path interno http://172.21.0.1:8081/api/emails (verificato 400 con X-Internal-Key = funzionante; pre edge-strip). Rollback: URL pubblico. - #9: pre-equip PHP-CA del DB CONFERMATO (CA presente, app connessa in TLS 1.3); enforce ALTER USER nis2_user REQUIRE SSL resta azione DB coordinata con VIGILE (utente @%). v1.21.3. Additivo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6992bdcf14
commit
69873e21be
@@ -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 '<p>Gentile ' . $esc($name) . ',</p>'
|
||||
. '<p>nell\'ambito degli adempimenti NIS2 della nostra organizzazione, ti chiediamo di ' . $azione . ': <strong>' . $esc($title) . '</strong>.</p>'
|
||||
. '<p><a href="' . $esc($link) . '" style="display:inline-block;padding:10px 18px;background:#0066CC;color:#fff;border-radius:8px;text-decoration:none;">Apri</a></p>'
|
||||
. '<p style="font-size:.85rem;color:#666;">Oppure copia questo link: ' . $esc($link) . '</p>'
|
||||
. '<p style="font-size:.8rem;color:#999;">Il link è personale: non inoltrarlo.</p>';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user