[FEAT] Standardizzazione lead form — allineamento a TRPG Agile
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 <noreply@anthropic.com>
This commit is contained in:
parent
c4c34aeed1
commit
b1dcd4cbd7
195
application/controllers/MktgLeadController.php
Normal file
195
application/controllers/MktgLeadController.php
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* MktgLeadController — Raccolta lead marketing standardizzata
|
||||||
|
*
|
||||||
|
* Endpoint pubblico (no auth):
|
||||||
|
* POST /api/mktg-lead → riceve il lead e lo inoltra a mktg.agile.software
|
||||||
|
*
|
||||||
|
* Standard condiviso con TRPG Agile — la chiave webhook rimane server-side.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once APP_PATH . '/controllers/BaseController.php';
|
||||||
|
require_once APP_PATH . '/services/EmailService.php';
|
||||||
|
|
||||||
|
class MktgLeadController extends BaseController
|
||||||
|
{
|
||||||
|
private const WEBHOOK_URL = 'https://mktg.agile.software/api/webhook/leads';
|
||||||
|
private const WEBHOOK_KEY = 'wh_nis2_2026_c1d2e3f4a5b6c7d8';
|
||||||
|
private const PRODUCT = 'NIS2 Agile';
|
||||||
|
private const SOURCE = 'nis2-landing';
|
||||||
|
private const NOTIFY_EMAIL = 'info@agile.software';
|
||||||
|
|
||||||
|
private EmailService $email;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->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 .= "<tr style='border-top:1px solid rgba(239,68,68,0.1);'>
|
||||||
|
<td style='padding:10px 0;color:#94A3B8;font-size:13px;width:140px;vertical-align:top;'>{$label}</td>
|
||||||
|
<td style='padding:10px 0;color:#F8FAFC;font-size:14px;'>{$val}</td>
|
||||||
|
</tr>";
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = "
|
||||||
|
<div style='font-family:Inter,Arial,sans-serif;max-width:600px;margin:0 auto;'>
|
||||||
|
<div style='background:#0F172A;padding:24px 32px;border-radius:12px 12px 0 0;'>
|
||||||
|
<h2 style='color:#EF4444;margin:0;font-size:20px;'>
|
||||||
|
🛡️ Nuovo lead NIS2 Agile — " . self::PRODUCT . "
|
||||||
|
</h2>
|
||||||
|
<p style='color:#94A3B8;margin:6px 0 0;font-size:13px;'>Ricevuto il {$date} · IP: {$ip}</p>
|
||||||
|
</div>
|
||||||
|
<div style='background:#1E293B;padding:28px 32px;border-radius:0 0 12px 12px;'>
|
||||||
|
<table style='width:100%;border-collapse:collapse;'>{$rows}</table>
|
||||||
|
<div style='margin-top:24px;padding:16px;background:rgba(239,68,68,0.05);border-radius:8px;border:1px solid rgba(239,68,68,0.15);'>
|
||||||
|
<p style='margin:0;color:#94A3B8;font-size:13px;'>
|
||||||
|
⚠️ Webhook mktg.agile.software non raggiungibile — lead salvato via email.<br>
|
||||||
|
Genera il codice invito: <a href='https://nis2.agile.software/licenseExt.html' style='color:#EF4444;'>licenseExt.html</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>";
|
||||||
|
|
||||||
|
$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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -972,26 +972,40 @@
|
|||||||
<input type="text" name="azienda" placeholder="Nome azienda o studio" required>
|
<input type="text" name="azienda" placeholder="Nome azienda o studio" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Ruolo <span>*</span></label>
|
<label>Telefono</label>
|
||||||
<select name="ruolo" required>
|
<input type="tel" name="telefono" placeholder="+39 02 123456">
|
||||||
<option value="">Seleziona il tuo ruolo...</option>
|
</div>
|
||||||
<option value="Responsabile Compliance / CISO">Responsabile Compliance / CISO</option>
|
<div class="form-group">
|
||||||
<option value="IT Manager / Responsabile Sicurezza">IT Manager / Responsabile Sicurezza</option>
|
<label>Tipo di utilizzo <span>*</span></label>
|
||||||
<option value="Consulente Cybersecurity / MSSP">Consulente Cybersecurity / MSSP</option>
|
<select name="tipo" required>
|
||||||
|
<option value="">Seleziona...</option>
|
||||||
|
<option value="Azienda soggetta NIS2">Azienda soggetta NIS2</option>
|
||||||
|
<option value="Consulente / CISO esterno">Consulente / CISO esterno</option>
|
||||||
|
<option value="MSSP / Managed Security Provider">MSSP / Managed Security Provider</option>
|
||||||
|
<option value="IT Manager / Responsabile sicurezza">IT Manager / Responsabile sicurezza</option>
|
||||||
|
<option value="DPO / Legale / Compliance">DPO / Legale / Compliance</option>
|
||||||
<option value="Direzione / CEO / CTO">Direzione / CEO / CTO</option>
|
<option value="Direzione / CEO / CTO">Direzione / CEO / CTO</option>
|
||||||
<option value="DPO / Legale">DPO / Legale</option>
|
</select>
|
||||||
<option value="Altra figura">Altra figura</option>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>N° dipendenti</label>
|
||||||
|
<select name="n_dipendenti">
|
||||||
|
<option value="">Seleziona...</option>
|
||||||
|
<option value="<50"><50</option>
|
||||||
|
<option value="50-249">50–249</option>
|
||||||
|
<option value="250-999">250–999</option>
|
||||||
|
<option value="1000+">1000+</option>
|
||||||
|
<option value="Studio multi-cliente">Studio multi-cliente</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group full">
|
<div class="form-group full">
|
||||||
<label>Dimensioni azienda</label>
|
<label>Cosa ti interessa? <span>*</span></label>
|
||||||
<select name="dimensioni">
|
<select name="interesse" required>
|
||||||
<option value="">Seleziona...</option>
|
<option value="">Seleziona...</option>
|
||||||
<option value="Micro (<10 dipendenti)">Micro (<10 dipendenti)</option>
|
<option value="Informazioni generali">Informazioni generali sul prodotto</option>
|
||||||
<option value="Piccola (10-49 dipendenti)">Piccola (10–49 dipendenti)</option>
|
<option value="Demo guidata">Demo guidata con un consulente</option>
|
||||||
<option value="Media (50-249 dipendenti)">Media (50–249 dipendenti)</option>
|
<option value="Accesso immediato">Accesso immediato alla piattaforma</option>
|
||||||
<option value="Grande (250+ dipendenti)">Grande (250+ dipendenti)</option>
|
<option value="Integrazione B2B">Integrazione B2B / API per il mio sistema</option>
|
||||||
<option value="Studio / Consulente multi-cliente">Studio / Consulente multi-cliente</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group full">
|
<div class="form-group full">
|
||||||
@ -1073,16 +1087,19 @@ document.getElementById('inviteForm').addEventListener('submit', async function(
|
|||||||
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Invio in corso...';
|
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Invio in corso...';
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
nome: form.nome.value.trim(),
|
name: form.nome.value.trim(),
|
||||||
email: form.email.value.trim(),
|
email: form.email.value.trim(),
|
||||||
azienda: form.azienda.value.trim(),
|
phone: form.telefono.value.trim(),
|
||||||
ruolo: form.ruolo.value,
|
company: form.azienda.value.trim(),
|
||||||
dimensioni: form.dimensioni.value,
|
tipo: form.tipo.value,
|
||||||
messaggio: form.messaggio.value.trim()
|
size: form.n_dipendenti.value,
|
||||||
|
product_interest: form.interesse.value,
|
||||||
|
source: 'nis2-landing',
|
||||||
|
notes: form.messaggio.value.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/contact/request-invite', {
|
const res = await fetch('/api/mktg-lead/submit', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
|
|||||||
@ -104,7 +104,8 @@ $controllerMap = [
|
|||||||
'whistleblowing'=> 'WhistleblowingController',
|
'whistleblowing'=> 'WhistleblowingController',
|
||||||
'normative' => 'NormativeController',
|
'normative' => 'NormativeController',
|
||||||
'cross-analysis' => 'CrossAnalysisController',
|
'cross-analysis' => 'CrossAnalysisController',
|
||||||
'contact' => 'ContactController',
|
'contact' => 'ContactController', // legacy
|
||||||
|
'mktg-lead' => 'MktgLeadController', // standard condiviso TRPG/NIS2
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isset($controllerMap[$controllerName])) {
|
if (!isset($controllerMap[$controllerName])) {
|
||||||
@ -371,10 +372,15 @@ $actionMap = [
|
|||||||
'GET:portfolio' => 'portfolio',
|
'GET:portfolio' => 'portfolio',
|
||||||
],
|
],
|
||||||
|
|
||||||
// ── ContactController (lead / richiesta invito) ──
|
// ── ContactController (lead / richiesta invito) — legacy ──
|
||||||
'contact' => [
|
'contact' => [
|
||||||
'POST:requestInvite' => 'requestInvite',
|
'POST:requestInvite' => 'requestInvite',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// ── MktgLeadController — standard condiviso TRPG/NIS2 ──
|
||||||
|
'mktg-lead' => [
|
||||||
|
'POST:submit' => 'submit',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user