From b1dcd4cbd78c6c115d94f6e2b92e555dff132522 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Mon, 9 Mar 2026 12:14:42 +0100 Subject: [PATCH] =?UTF-8?q?[FEAT]=20Standardizzazione=20lead=20form=20?= =?UTF-8?q?=E2=80=94=20allineamento=20a=20TRPG=20Agile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MktgLeadController.php: - Endpoint POST /api/mktg-lead/submit (standard condiviso TRPG/NIS2) - Proxy a mktg.agile.software/api/webhook/leads con X-Webhook-Key server-side - Payload standard: name, email, phone, company, product_interest, source, notes - source: "nis2-landing" per tracciamento CRM - Fallback email a info@agile.software se webhook non raggiungibile - Rate limit 3/10min per IP, supporto campi IT e EN index.html — form allineato a TRPG: - Aggiunto: telefono (opzionale) - Aggiunto: tipo utilizzo (select 6 opzioni) - N° dipendenti: fasce standardizzate (<50/50-249/250-999/1000+) - Aggiunto: interesse (info/demo/accesso/integrazione B2B) - Endpoint aggiornato a /api/mktg-lead/submit - Payload mappato su campi standard EN + source: "nis2-landing" Co-Authored-By: Claude Sonnet 4.6 --- .../controllers/MktgLeadController.php | 195 ++++++++++++++++++ public/index.html | 61 ++++-- public/index.php | 10 +- 3 files changed, 242 insertions(+), 24 deletions(-) create mode 100644 application/controllers/MktgLeadController.php diff --git a/application/controllers/MktgLeadController.php b/application/controllers/MktgLeadController.php new file mode 100644 index 0000000..e142e72 --- /dev/null +++ b/application/controllers/MktgLeadController.php @@ -0,0 +1,195 @@ +email = new EmailService(); + } + + /** + * POST /api/mktg-lead + */ + public function submit(): void + { + // Rate limit: 3 richieste / 10 min per IP + $ip = $this->getClientIP(); + $cacheFile = sys_get_temp_dir() . '/nis2_mktglead_' . md5($ip); + $now = time(); + + $cache = file_exists($cacheFile) ? json_decode(file_get_contents($cacheFile), true) : ['requests' => []]; + $cache['requests'] = array_filter($cache['requests'] ?? [], fn($t) => $t > ($now - 600)); + if (count($cache['requests']) >= 3) { + $this->jsonError('Troppe richieste. Riprova tra qualche minuto.', 429); + return; + } + $cache['requests'][] = $now; + file_put_contents($cacheFile, json_encode($cache)); + + // Input + $body = $this->getRequestBody(); + + // Supporta sia campi IT (form NIS2) che campi EN (standard mktg) + $name = trim($body['name'] ?? $body['nome'] ?? ''); + $email = trim($body['email'] ?? ''); + $phone = trim($body['phone'] ?? $body['telefono'] ?? ''); + $company = trim($body['company'] ?? $body['azienda'] ?? ''); + $role = trim($body['role'] ?? $body['tipo'] ?? ''); + $size = trim($body['size'] ?? $body['n_dipendenti'] ?? ''); + $interest = trim($body['product_interest'] ?? $body['interesse'] ?? ''); + $notes = trim($body['notes'] ?? $body['messaggio'] ?? ''); + $source = trim($body['source'] ?? self::SOURCE); + + // Validazione + if (!$name || !$email || !$company) { + $this->jsonError('Compila i campi obbligatori: nome, email, azienda.', 422); + return; + } + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $this->jsonError('Indirizzo email non valido.', 422); + return; + } + + // Sanitize + $name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); + $email = htmlspecialchars($email, ENT_QUOTES, 'UTF-8'); + $phone = htmlspecialchars($phone, ENT_QUOTES, 'UTF-8'); + $company = htmlspecialchars($company, ENT_QUOTES, 'UTF-8'); + $role = htmlspecialchars($role, ENT_QUOTES, 'UTF-8'); + $size = htmlspecialchars($size, ENT_QUOTES, 'UTF-8'); + $interest = htmlspecialchars($interest, ENT_QUOTES, 'UTF-8'); + $source = htmlspecialchars($source, ENT_QUOTES, 'UTF-8'); + + // Componi notes con campi aggiuntivi + $noteParts = []; + if ($role) $noteParts[] = "Ruolo: {$role}"; + if ($size) $noteParts[] = "Dimensioni: {$size}"; + if ($interest) $noteParts[] = "Interesse: {$interest}"; + if ($notes) $noteParts[] = $notes; + $fullNotes = implode(' | ', $noteParts); + + // Payload standard mktg + $payload = [ + 'name' => $name, + 'email' => $email, + 'phone' => $phone, + 'company' => $company, + 'product_interest' => $interest ?: self::PRODUCT, + 'source' => $source, + 'notes' => $fullNotes, + ]; + + // 1. Prova webhook mktg.agile.software + $webhookOk = $this->sendWebhook($payload); + + // 2. Fallback email se webhook fallisce + if (!$webhookOk) { + $this->sendFallbackEmail($payload, $ip); + } + + $this->jsonSuccess(null, 'Richiesta inviata! Ti contatteremo entro 24 ore con il tuo codice di accesso.'); + } + + private function sendWebhook(array $payload): bool + { + $ch = curl_init(self::WEBHOOK_URL); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'X-Webhook-Key: ' . self::WEBHOOK_KEY, + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 8, + CURLOPT_SSL_VERIFYPEER => true, + ]); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($error || $httpCode < 200 || $httpCode >= 300) { + error_log("MktgLead webhook failed [{$httpCode}]: {$error}"); + return false; + } + return true; + } + + private function sendFallbackEmail(array $payload, string $ip): void + { + $date = date('d/m/Y H:i'); + $rows = ''; + $labels = [ + 'name' => 'Nome', + 'email' => 'Email', + 'phone' => 'Telefono', + 'company' => 'Azienda', + 'product_interest' => 'Interesse', + 'source' => 'Source', + 'notes' => 'Note', + ]; + foreach ($labels as $key => $label) { + $val = $payload[$key] ?? ''; + if (!$val) continue; + $rows .= " + {$label} + {$val} + "; + } + + $html = " +
+
+

+ 🛡️ Nuovo lead NIS2 Agile — " . self::PRODUCT . " +

+

Ricevuto il {$date} · IP: {$ip}

+
+
+ {$rows}
+
+

+ ⚠️ Webhook mktg.agile.software non raggiungibile — lead salvato via email.
+ Genera il codice invito: licenseExt.html +

+
+
+
"; + + $this->email->send( + self::NOTIFY_EMAIL, + "🛡️ Lead NIS2 Agile — {$payload['name']} ({$payload['company']})", + $html, + 'noreply@agile.software' + ); + } + + private function getClientIP(): string + { + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + return trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]); + } + return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + } +} diff --git a/public/index.html b/public/index.html index 6f10ce3..2a0214f 100644 --- a/public/index.html +++ b/public/index.html @@ -972,26 +972,40 @@
- - +
+
+ + +
+
+ +
- - - - - - - + + + +
@@ -1073,16 +1087,19 @@ document.getElementById('inviteForm').addEventListener('submit', async function( btn.innerHTML = ' Invio in corso...'; const data = { - nome: form.nome.value.trim(), - email: form.email.value.trim(), - azienda: form.azienda.value.trim(), - ruolo: form.ruolo.value, - dimensioni: form.dimensioni.value, - messaggio: form.messaggio.value.trim() + name: form.nome.value.trim(), + email: form.email.value.trim(), + phone: form.telefono.value.trim(), + company: form.azienda.value.trim(), + tipo: form.tipo.value, + size: form.n_dipendenti.value, + product_interest: form.interesse.value, + source: 'nis2-landing', + notes: form.messaggio.value.trim() }; try { - const res = await fetch('/api/contact/request-invite', { + const res = await fetch('/api/mktg-lead/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) diff --git a/public/index.php b/public/index.php index c2338a4..196e80f 100644 --- a/public/index.php +++ b/public/index.php @@ -104,7 +104,8 @@ $controllerMap = [ 'whistleblowing'=> 'WhistleblowingController', 'normative' => 'NormativeController', 'cross-analysis' => 'CrossAnalysisController', - 'contact' => 'ContactController', + 'contact' => 'ContactController', // legacy + 'mktg-lead' => 'MktgLeadController', // standard condiviso TRPG/NIS2 ]; if (!isset($controllerMap[$controllerName])) { @@ -371,10 +372,15 @@ $actionMap = [ 'GET:portfolio' => 'portfolio', ], - // ── ContactController (lead / richiesta invito) ── + // ── ContactController (lead / richiesta invito) — legacy ── 'contact' => [ 'POST:requestInvite' => 'requestInvite', ], + + // ── MktgLeadController — standard condiviso TRPG/NIS2 ── + 'mktg-lead' => [ + 'POST:submit' => 'submit', + ], ]; // ═══════════════════════════════════════════════════════════════════════════