[FEAT] Landing NIS2: accesso su invito + form lead request

- index.html: CTA "Registrati" → "Richiedi accesso" (anchor form)
  Badge hero "Accesso su invito — Richiedi il tuo codice per iniziare"
  Sezione #richiedi-accesso con form lead (nome, email, azienda, ruolo,
  dimensioni, messaggio) + JS submit asincrono + stato successo/errore
  CTA finale aggiornato con messaggio codice invito
- ContactController.php: POST /api/contact/request-invite
  Validazione campi, rate limit 3/10min per IP, email a info@agile.software
  tramite EmailService con template HTML branded
- index.php: route contact → ContactController + action requestInvite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-03-09 11:23:40 +01:00
parent 6cf1cd7384
commit 0a194f6f12
3 changed files with 382 additions and 11 deletions

View File

@ -0,0 +1,147 @@
<?php
/**
* ContactController Gestione lead e richieste codice invito
*
* Endpoint pubblici (no auth):
* POST /api/contact/request-invite richiesta accesso con codice invito
*/
require_once APP_PATH . '/controllers/BaseController.php';
require_once APP_PATH . '/services/EmailService.php';
class ContactController extends BaseController
{
private EmailService $email;
public function __construct()
{
parent::__construct();
$this->email = new EmailService();
}
/**
* POST /api/contact/request-invite
* Raccoglie i dati del lead e invia notifica a info@agile.software
*/
public function requestInvite(): void
{
// Rate limit: max 3 richieste / 10 min per IP
$ip = $this->getClientIP();
$key = 'contact_' . md5($ip);
$cacheFile = sys_get_temp_dir() . '/nis2_contact_' . $key;
$now = time();
if (file_exists($cacheFile)) {
$data = json_decode(file_get_contents($cacheFile), true);
$data['requests'] = array_filter($data['requests'] ?? [], fn($t) => $t > ($now - 600));
if (count($data['requests']) >= 3) {
$this->jsonError('Troppe richieste. Riprova tra qualche minuto.', 429);
return;
}
} else {
$data = ['requests' => []];
}
$data['requests'][] = $now;
file_put_contents($cacheFile, json_encode($data));
// Validazione input
$body = $this->getRequestBody();
$nome = trim($body['nome'] ?? '');
$email = trim($body['email'] ?? '');
$azienda = trim($body['azienda'] ?? '');
$ruolo = trim($body['ruolo'] ?? '');
$dimensioni = trim($body['dimensioni'] ?? '');
$messaggio = trim($body['messaggio'] ?? '');
if (!$nome || !$email || !$azienda || !$ruolo) {
$this->jsonError('Compila tutti i campi obbligatori (nome, email, azienda, ruolo).', 422);
return;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->jsonError('Indirizzo email non valido.', 422);
return;
}
// Sanitize
$nome = htmlspecialchars($nome, ENT_QUOTES, 'UTF-8');
$email = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
$azienda = htmlspecialchars($azienda, ENT_QUOTES, 'UTF-8');
$ruolo = htmlspecialchars($ruolo, ENT_QUOTES, 'UTF-8');
$dimensioni = htmlspecialchars($dimensioni, ENT_QUOTES, 'UTF-8');
$messaggio = htmlspecialchars($messaggio, ENT_QUOTES, 'UTF-8');
$date = date('d/m/Y H:i');
$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:#06B6D4;margin:0;font-size:20px;'>
🛡️ Nuova richiesta codice invito NIS2 Agile
</h2>
<p style='color:#94A3B8;margin:6px 0 0;font-size:13px;'>Ricevuta il {$date}</p>
</div>
<div style='background:#1E293B;padding:28px 32px;border-radius:0 0 12px 12px;'>
<table style='width:100%;border-collapse:collapse;'>
<tr>
<td style='padding:10px 0;color:#94A3B8;font-size:13px;width:140px;vertical-align:top;'>Nome</td>
<td style='padding:10px 0;color:#F8FAFC;font-size:14px;font-weight:600;'>{$nome}</td>
</tr>
<tr style='border-top:1px solid rgba(6,182,212,0.1);'>
<td style='padding:10px 0;color:#94A3B8;font-size:13px;vertical-align:top;'>Email</td>
<td style='padding:10px 0;color:#06B6D4;font-size:14px;'><a href='mailto:{$email}' style='color:#06B6D4;'>{$email}</a></td>
</tr>
<tr style='border-top:1px solid rgba(6,182,212,0.1);'>
<td style='padding:10px 0;color:#94A3B8;font-size:13px;vertical-align:top;'>Azienda/Studio</td>
<td style='padding:10px 0;color:#F8FAFC;font-size:14px;font-weight:600;'>{$azienda}</td>
</tr>
<tr style='border-top:1px solid rgba(6,182,212,0.1);'>
<td style='padding:10px 0;color:#94A3B8;font-size:13px;vertical-align:top;'>Ruolo</td>
<td style='padding:10px 0;color:#F8FAFC;font-size:14px;'>{$ruolo}</td>
</tr>
" . ($dimensioni ? "
<tr style='border-top:1px solid rgba(6,182,212,0.1);'>
<td style='padding:10px 0;color:#94A3B8;font-size:13px;vertical-align:top;'>Dimensioni</td>
<td style='padding:10px 0;color:#F8FAFC;font-size:14px;'>{$dimensioni}</td>
</tr>" : "") . "
" . ($messaggio ? "
<tr style='border-top:1px solid rgba(6,182,212,0.1);'>
<td style='padding:10px 0;color:#94A3B8;font-size:13px;vertical-align:top;'>Messaggio</td>
<td style='padding:10px 0;color:#F8FAFC;font-size:14px;line-height:1.6;'>{$messaggio}</td>
</tr>" : "") . "
<tr style='border-top:1px solid rgba(6,182,212,0.1);'>
<td style='padding:10px 0;color:#94A3B8;font-size:13px;vertical-align:top;'>IP</td>
<td style='padding:10px 0;color:#64748B;font-size:12px;'>{$ip}</td>
</tr>
</table>
<div style='margin-top:24px;padding:16px;background:rgba(6,182,212,0.06);border-radius:8px;border:1px solid rgba(6,182,212,0.15);'>
<p style='margin:0;color:#94A3B8;font-size:13px;'>
Rispondi a questo lead generando un codice invito da:
<a href='https://nis2.agile.software/licenseExt.html' style='color:#06B6D4;'>licenseExt.html</a>
</p>
</div>
</div>
</div>";
$sent = $this->email->send(
'info@agile.software',
"🛡️ Richiesta accesso NIS2 Agile — {$nome} ({$azienda})",
$html,
'noreply@agile.software'
);
if (!$sent) {
$this->jsonError('Errore nell\'invio della richiesta. Prova a contattarci direttamente a info@agile.software.', 500);
return;
}
$this->jsonSuccess(null, 'Richiesta inviata! Ti contatteremo entro 24 ore con il tuo codice di accesso.');
}
private function getClientIP(): string
{
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($ips[0]);
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
}

View File

@ -74,6 +74,15 @@
}
.nav-name span { color: var(--cyan); }
.nav-actions { display: flex; gap: 12px; align-items: center; }
.btn-invite {
background: var(--brand-gradient);
color: white;
box-shadow: 0 4px 16px rgba(6,182,212,0.25);
}
.btn-invite:hover {
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(6,182,212,0.35);
}
.btn {
display: inline-flex;
align-items: center;
@ -531,6 +540,89 @@
}
.norma-strip i { color: var(--cyan); }
/* ── BADGE INVITO ── */
.invite-badge {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(245,158,11,0.1);
border: 1px solid rgba(245,158,11,0.25);
border-radius: 8px;
padding: 8px 16px;
font-size: 13px;
color: var(--orange);
margin-bottom: 20px;
}
.invite-badge i { font-size: 12px; }
/* ── FORM LEAD ── */
#richiedi-accesso { background: var(--surface-dark); }
.form-box {
background: var(--brand-primary);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 48px;
max-width: 680px;
margin: 0 auto;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group { display: flex; flex-direction: column; gap: 8px; }
.form-group.full { grid-column: 1 / -1; }
.form-group label {
font-size: 13px;
font-weight: 600;
color: var(--text-light);
}
.form-group label span { color: var(--red); margin-left: 2px; }
.form-group input,
.form-group select,
.form-group textarea {
background: var(--surface-dark);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px 16px;
font-size: 14px;
color: var(--text-white);
font-family: inherit;
transition: border-color 0.2s;
width: 100%;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--cyan);
}
.form-group input::placeholder,
.form-group textarea::placeholder { color: var(--text-muted); }
.form-group select option { background: #1E293B; }
.form-group textarea { resize: vertical; min-height: 90px; }
.form-submit { width: 100%; padding: 14px; font-size: 16px; margin-top: 8px; }
.form-note { font-size: 12px; color: var(--text-muted); text-align: center; margin-top: 12px; }
.form-success {
display: none;
text-align: center;
padding: 32px;
}
.form-success i { font-size: 48px; color: var(--green); margin-bottom: 16px; display: block; }
.form-success h3 { font-size: 22px; font-weight: 700; color: var(--text-white); margin-bottom: 8px; }
.form-success p { color: var(--text-muted); font-size: 15px; }
.form-error-msg {
display: none;
background: rgba(239,68,68,0.08);
border: 1px solid rgba(239,68,68,0.2);
border-radius: 8px;
padding: 12px 16px;
font-size: 13px;
color: var(--red);
margin-bottom: 16px;
}
/* ── RESPONSIVE ── */
@media (max-width: 900px) {
nav { padding: 0 20px; }
@ -546,6 +638,8 @@
.lg231-card { flex-direction: column; padding: 28px; }
.footer-inner { flex-direction: column; align-items: flex-start; }
.nav-actions .btn-ghost { display: none; }
.form-grid { grid-template-columns: 1fr; }
.form-box { padding: 28px 20px; }
}
</style>
</head>
@ -561,8 +655,8 @@
<a href="/login.html" class="btn btn-ghost btn-sm">
<i class="fa-solid fa-right-to-bracket"></i> Accedi
</a>
<a href="/register.html" class="btn btn-primary btn-sm">
<i class="fa-solid fa-rocket"></i> Registrati
<a href="#richiedi-accesso" class="btn btn-primary btn-sm">
<i class="fa-solid fa-envelope"></i> Richiedi accesso
</a>
</div>
</nav>
@ -583,12 +677,16 @@
<p class="hero-subtitle">
Piattaforma SaaS multi-tenant per guidare la tua azienda alla conformità con la Direttiva NIS2 (EU 2022/2555). Gap analysis AI, risk management, incident response Art.23, policy e formazione — tutto integrato.
</p>
<div class="invite-badge">
<i class="fa-solid fa-lock"></i>
Accesso su invito — Richiedi il tuo codice per iniziare
</div>
<div class="hero-ctas">
<a href="/register.html" class="btn btn-primary btn-lg">
<i class="fa-solid fa-rocket"></i> Inizia gratuitamente
<a href="#richiedi-accesso" class="btn btn-primary btn-lg">
<i class="fa-solid fa-envelope"></i> Richiedi accesso
</a>
<a href="/login.html" class="btn btn-ghost btn-lg">
<i class="fa-solid fa-right-to-bracket"></i> Accedi
<i class="fa-solid fa-right-to-bracket"></i> Ho un codice — Accedi
</a>
</div>
<div class="hero-stats">
@ -849,21 +947,95 @@
</div>
</section>
<!-- FORM RICHIESTA ACCESSO -->
<section id="richiedi-accesso">
<div class="container">
<div class="section-header">
<div class="section-eyebrow"><i class="fa-solid fa-envelope"></i> Richiedi accesso</div>
<h2 class="section-title">Ottieni il tuo <span class="gradient">codice invito</span></h2>
<p class="section-desc">La piattaforma è ad accesso controllato. Compila il form e ti invieremo il codice invito entro 24 ore.</p>
</div>
<div class="form-box">
<div class="form-error-msg" id="formError"></div>
<form id="inviteForm">
<div class="form-grid">
<div class="form-group">
<label>Nome e Cognome <span>*</span></label>
<input type="text" name="nome" placeholder="Mario Rossi" required>
</div>
<div class="form-group">
<label>Email aziendale <span>*</span></label>
<input type="email" name="email" placeholder="mario@azienda.it" required>
</div>
<div class="form-group">
<label>Azienda / Studio <span>*</span></label>
<input type="text" name="azienda" placeholder="Nome azienda o studio" required>
</div>
<div class="form-group">
<label>Ruolo <span>*</span></label>
<select name="ruolo" required>
<option value="">Seleziona il tuo ruolo...</option>
<option value="Responsabile Compliance / CISO">Responsabile Compliance / CISO</option>
<option value="IT Manager / Responsabile Sicurezza">IT Manager / Responsabile Sicurezza</option>
<option value="Consulente Cybersecurity / MSSP">Consulente Cybersecurity / MSSP</option>
<option value="Direzione / CEO / CTO">Direzione / CEO / CTO</option>
<option value="DPO / Legale">DPO / Legale</option>
<option value="Altra figura">Altra figura</option>
</select>
</div>
<div class="form-group full">
<label>Dimensioni azienda</label>
<select name="dimensioni">
<option value="">Seleziona...</option>
<option value="Micro (&lt;10 dipendenti)">Micro (&lt;10 dipendenti)</option>
<option value="Piccola (10-49 dipendenti)">Piccola (1049 dipendenti)</option>
<option value="Media (50-249 dipendenti)">Media (50249 dipendenti)</option>
<option value="Grande (250+ dipendenti)">Grande (250+ dipendenti)</option>
<option value="Studio / Consulente multi-cliente">Studio / Consulente multi-cliente</option>
</select>
</div>
<div class="form-group full">
<label>Messaggio (opzionale)</label>
<textarea name="messaggio" placeholder="Descrivi brevemente la tua esigenza o il settore di appartenenza NIS2..."></textarea>
</div>
</div>
<button type="submit" class="btn btn-primary form-submit" id="submitBtn">
<i class="fa-solid fa-paper-plane"></i> Invia richiesta
</button>
<p class="form-note">
<i class="fa-solid fa-lock" style="color:var(--cyan);margin-right:4px;font-size:11px;"></i>
I tuoi dati sono trattati nel rispetto del GDPR. Nessuna cessione a terzi.
</p>
</form>
<div class="form-success" id="formSuccess">
<i class="fa-solid fa-circle-check"></i>
<h3>Richiesta inviata!</h3>
<p>Ti contatteremo entro 24 ore all'indirizzo fornito con il tuo codice di accesso personalizzato.<br><br>
<strong style="color:var(--text-white);">Hai già ricevuto un codice?</strong><br>
<a href="/register.html" style="color:var(--cyan);text-decoration:none;">Registrati ora con il tuo codice invito →</a></p>
</div>
</div>
<p style="text-align:center;font-size:13px;color:var(--text-muted);margin-top:24px;">
Hai già un account? <a href="/login.html" style="color:var(--cyan);text-decoration:none;">Accedi alla dashboard →</a>
</p>
</div>
</section>
<!-- CTA FINALE -->
<section id="cta-finale">
<div class="container">
<div class="cta-box">
<h2>Inizia oggi la tua<br><span style="background: var(--brand-gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;">compliance NIS2</span></h2>
<p>Registrazione gratuita. Nessuna carta di credito richiesta. Operativo in 5 minuti.</p>
<h2>La tua compliance NIS2<br><span style="background: var(--brand-gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;">inizia con un codice</span></h2>
<p>Richiedi l'accesso oggi. Nessuna carta di credito richiesta. Operativo in 5 minuti dal ricevimento del codice.</p>
<div class="cta-actions">
<a href="/register.html" class="btn btn-primary btn-lg">
<i class="fa-solid fa-rocket"></i> Registrati gratis
<a href="#richiedi-accesso" class="btn btn-primary btn-lg">
<i class="fa-solid fa-envelope"></i> Richiedi il codice invito
</a>
<a href="/login.html" class="btn btn-ghost btn-lg">
<i class="fa-solid fa-right-to-bracket"></i> Accedi
</a>
</div>
<p class="cta-note">Hai già una licenza ricevuta da un consulente? <a href="/register.html" style="color:var(--cyan);text-decoration:none;">Registrati con il tuo codice invito</a></p>
<p class="cta-note">Sei un consulente o MSSP? Il form di richiesta ti permette di attivare accesso per il tuo intero portfolio clienti.</p>
</div>
</div>
</section>
@ -877,8 +1049,8 @@
<span style="color:var(--text-muted);font-size:12px;margin-left:8px;">by Agile Technology SRL</span>
</div>
<div class="footer-links">
<a href="#richiedi-accesso">Richiedi accesso</a>
<a href="/login.html">Accedi</a>
<a href="/register.html">Registrati</a>
<a href="https://lg231.agile.software/" target="_blank">231 Agile</a>
<a href="mailto:info@agile.software">info@agile.software</a>
</div>
@ -888,5 +1060,51 @@
</div>
</footer>
<script>
document.getElementById('inviteForm').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('submitBtn');
const errEl = document.getElementById('formError');
const successEl = document.getElementById('formSuccess');
const form = e.target;
errEl.style.display = 'none';
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> 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()
};
try {
const res = await fetch('/api/contact/request-invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const json = await res.json();
if (res.ok && json.success) {
form.style.display = 'none';
successEl.style.display = 'block';
} else {
errEl.textContent = json.message || 'Errore nell\'invio. Riprova o scrivici a info@agile.software.';
errEl.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-paper-plane"></i> Invia richiesta';
}
} catch {
errEl.textContent = 'Errore di rete. Riprova o scrivici a info@agile.software.';
errEl.style.display = 'block';
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-paper-plane"></i> Invia richiesta';
}
});
</script>
</body>
</html>

View File

@ -104,6 +104,7 @@ $controllerMap = [
'whistleblowing'=> 'WhistleblowingController',
'normative' => 'NormativeController',
'cross-analysis' => 'CrossAnalysisController',
'contact' => 'ContactController',
];
if (!isset($controllerMap[$controllerName])) {
@ -369,6 +370,11 @@ $actionMap = [
'GET:history' => 'history',
'GET:portfolio' => 'portfolio',
],
// ── ContactController (lead / richiesta invito) ──
'contact' => [
'POST:requestInvite' => 'requestInvite',
],
];
// ═══════════════════════════════════════════════════════════════════════════