Files
nis2-agile/public/register.html
T
DevEnv nis2-agileandClaude Opus 4.8 a5ff29e0da [FEAT] Epic C / C5.2a — Attività stakeholder: questionari tipo + attività + calendario (mig.052)
Fondazione delle attività verso gli stakeholder (Simon C5 §4):
- Questionari tipo (template): tipo questionario-da-compilare / firma-di-avvenuta-lettura,
  domande (JSON) + collegamento m2m a procedure (policies) E a misure/requisiti del
  framework (cfg_nis2_misure/requisiti).
- Attività: basate (opz.) su un template, assegnabili a un CODICE stakeholder (tutti
  quelli di quel tipo) o a singoli stakeholder; pianificabili con data e scadenza.
- Calendario NIS2: review_schedule esteso (ENUM entity_type += 'stakeholder_activity');
  su create/update con scadenza la riga di calendario viene sincronizzata.
- Invio: genera un magic-link per destinatario (token SHA-256) per il portale esterno
  (C5.2b). Email disattivate (kill-switch) -> i link si condividono manualmente.

mig.052: stk_questionnaire_templates, stk_template_(procedures|misure|requisiti),
stk_activities, stk_activity_targets, stk_activity_procedures + ALTER review_schedule.
StakeholderActivityController + /api/stakeholder-activities/*; stakeholder-activities.html
(2 tab: attività / questionari tipo + dettaglio invio); voce sidebar V2; help/i18n.
Smoke prod OK (template m2m, attività by_code+calendario, assign, send magic-link,
anti-IDOR individual, negativi 422; cleanup org 151 pulita). Additivo. v1.20.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:12:11 +02:00

608 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Registrazione - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 — self-hostato da /vendor (MAI CDN). Variante "-bi" della register.html. -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<style>
body { min-height: 100vh; display: flex; align-items: center; background: #f5f6f7; padding: 24px 0; }
.auth-wrap { width: 100%; max-width: 560px; margin: 0 auto; padding: 24px; }
.auth-brand { text-align: center; margin-bottom: 20px; }
.auth-brand .brand-name { font-weight: 700; font-size: 1.5rem; color: #06c; }
.auth-brand .brand-name span { color: #17324d; }
.auth-brand .brand-sub { color: #5a6772; font-size: .9rem; margin-top: 4px; }
/* Step indicator (BI Blu Italia) */
.step-indicator { display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; }
.step-item { display: flex; flex-direction: column; align-items: center; }
.step-circle { width: 32px; height: 32px; border-radius: 50%; border: 2px solid #d4d8db;
display: flex; align-items: center; justify-content: center; font-size: .8rem; font-weight: 700;
color: #5a6772; background: #fff; transition: all .25s; }
.step-item.active .step-circle { border-color: #06c; color: #06c; background: #e6f0ff; }
.step-item.done .step-circle { border-color: #008758; background: #008758; color: #fff; }
.step-label { font-size: .68rem; color: #5a6772; margin-top: 4px; white-space: nowrap; }
.step-item.active .step-label { color: #06c; font-weight: 600; }
.step-connector { width: 48px; height: 2px; background: #d4d8db; margin-bottom: 18px; transition: background .25s; }
.step-connector.done { background: #008758; }
/* Role cards (BI palette) */
.role-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; margin-bottom: 1.25rem; }
.role-card { border: 2px solid #d4d8db; border-radius: 8px; padding: 1rem .875rem; cursor: pointer;
transition: all .2s; background: #fff; text-align: left; }
.role-card:hover { border-color: #06c; background: #e6f0ff; }
.role-card:focus-visible { outline: 2px solid #06c; outline-offset: 2px; }
.role-card.selected { border-color: #06c; background: #e6f0ff; box-shadow: 0 0 0 3px rgba(0,102,204,.15); }
.role-card-icon { width: 36px; height: 36px; border-radius: 8px; background: rgba(0,102,204,.12); color: #06c;
display: flex; align-items: center; justify-content: center; margin-bottom: .6rem; }
.role-card-title { font-weight: 700; font-size: .88rem; color: #17324d; margin-bottom: .2rem; }
.role-card-desc { font-size: .72rem; color: #5a6772; line-height: 1.45; }
.invite-section { border: 1px dashed #d4d8db; border-radius: 8px; padding: .875rem 1rem; margin-bottom: 1rem; }
.invite-section summary { cursor: pointer; font-size: .82rem; font-weight: 600; color: #5a6772; }
.invite-section[open] summary { color: #06c; }
.pw-wrap { position: relative; }
.pw-wrap .form-control { padding-right: 44px; }
.pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; color: #5a6772;
cursor: pointer; padding: 6px; }
.lookup-status { font-size: .75rem; margin-top: .35rem; min-height: 1.1rem; display: flex; align-items: center; gap: .35rem; }
.lookup-status.ok { color: #008758; }
.lookup-status.err { color: #d9364f; }
.lookup-status.loading { color: #5a6772; }
.success-body { text-align: center; padding: .5rem 0 1rem; }
.success-icon { width: 64px; height: 64px; border-radius: 50%; background: #008758;
display: flex; align-items: center; justify-content: center; margin: 0 auto 1.25rem; color: #fff; }
/* Steps display */
#step-0, #step-1, #step-2 { display: none; }
#step-0 { display: block; }
.ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; }
</style>
<!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#0066CC">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
<meta name="application-name" content="NIS2 Agile">
<script src="/js/pwa.js?v=20260614" defer></script>
<!-- PWA:end -->
</head>
<body>
<main class="auth-wrap">
<div class="auth-brand">
<h1 class="brand-name">NIS2 <span>Agile</span></h1>
<div class="brand-sub" id="auth-subtitle">Crea il tuo account</div>
</div>
<div class="card card-bg has-bkg-grey shadow">
<div class="card-body">
<!-- Step indicator -->
<div class="step-indicator" id="step-indicator">
<div class="step-item active" id="si-0">
<div class="step-circle">1</div>
<div class="step-label">Ruolo</div>
</div>
<div class="step-connector" id="conn-0"></div>
<div class="step-item" id="si-1">
<div class="step-circle">2</div>
<div class="step-label">Dati</div>
</div>
<div class="step-connector" id="conn-1"></div>
<div class="step-item" id="si-2">
<div class="step-circle">3</div>
<div class="step-label">Fatto</div>
</div>
</div>
<div class="alert alert-danger d-none" id="register-error" role="alert"></div>
<!-- STEP 0: Ruolo + Codice Invito -->
<div id="step-0">
<div class="role-grid" role="radiogroup" aria-label="Seleziona il tuo ruolo">
<div class="role-card" id="card-compliance_manager" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('compliance_manager')" onkeydown="cardKey(event,'compliance_manager')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-locked"></use></svg></div>
<div class="role-card-title">CISO / Compliance Manager</div>
<div class="role-card-desc">Responsabile sicurezza e conformità NIS2 dell'organizzazione</div>
</div>
<div class="role-card" id="card-org_admin" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('org_admin')" onkeydown="cardKey(event,'org_admin')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg></div>
<div class="role-card-title">Legale Rappresentante / Admin</div>
<div class="role-card-desc">Amministratore e legale rappresentante dell'organizzazione</div>
</div>
<div class="role-card" id="card-consultant" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('consultant')" onkeydown="cardKey(event,'consultant')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg></div>
<div class="role-card-title">Consulente Cybersecurity</div>
<div class="role-card-desc">Gestisco la compliance NIS2 per più aziende clienti</div>
</div>
<div class="role-card" id="card-auditor" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('auditor')" onkeydown="cardKey(event,'auditor')">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg></div>
<div class="role-card-title">Auditor / Revisore</div>
<div class="role-card-desc">Verifico e valuto l'implementazione dei controlli di sicurezza</div>
</div>
<div class="role-card" id="card-board_member" role="radio" aria-checked="false" tabindex="0"
onclick="selectRole('board_member')" onkeydown="cardKey(event,'board_member')" style="grid-column: span 2;">
<div class="role-card-icon"><svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-key"></use></svg></div>
<div class="role-card-title">Board Member / DPO</div>
<div class="role-card-desc">Membro del consiglio o Responsabile della Protezione dei Dati con visibilità strategica</div>
</div>
</div>
<details class="invite-section" id="invite-details">
<summary>Ho un codice di invito B2B</summary>
<div class="form-group mt-2">
<label for="invite-code">Codice Invito</label>
<input type="text" id="invite-code" class="form-control"
placeholder="es. inv_a1b2c3d4e5f6..." autocomplete="off"
oninput="validateInvite(this.value)">
<div class="lookup-status" id="invite-status" aria-live="polite"></div>
</div>
</details>
<button class="btn btn-primary w-100 mt-2" id="btn-next-0" onclick="goToStep1()" disabled>
Continua
<svg class="ico ms-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-right"></use></svg>
</button>
</div>
<!-- STEP 1: Dati account -->
<div id="step-1">
<div id="invite-welcome" class="alert alert-info" style="display:none;">
<div id="invite-welcome-name" style="font-weight:700;"></div>
<div id="invite-welcome-plan" style="font-size:.8rem;"></div>
<p class="mb-0 mt-1" style="font-size:.85rem;">I tuoi dati sono stati pre-compilati dall'invito. <strong>Scegli solo la password</strong> per accedere alla piattaforma.</p>
</div>
<form id="register-form" novalidate>
<div class="row">
<div class="form-group col">
<label for="firstname" class="active">Nome <span aria-hidden="true">*</span></label>
<input type="text" id="firstname" name="firstname" class="form-control"
autocomplete="given-name" required>
</div>
<div class="form-group col">
<label for="lastname" class="active">Cognome <span aria-hidden="true">*</span></label>
<input type="text" id="lastname" name="lastname" class="form-control"
autocomplete="family-name" required>
</div>
</div>
<div class="form-group">
<label for="email" class="active">Indirizzo Email <span aria-hidden="true">*</span></label>
<input type="email" id="email" name="email" class="form-control"
autocomplete="email" required>
</div>
<div class="form-group" id="piva-group">
<label for="piva" class="active">Partita IVA Azienda <span aria-hidden="true">*</span></label>
<input type="text" id="piva" name="piva" class="form-control"
placeholder="12345678901" maxlength="11" inputmode="numeric"
oninput="lookupPiva(this.value)">
<div class="lookup-status" id="piva-status" aria-live="polite"></div>
</div>
<div class="form-group">
<label for="password" class="active">Password <span aria-hidden="true">*</span></label>
<div class="pw-wrap">
<input type="password" id="password" name="password" class="form-control"
placeholder="Minimo 8 caratteri" autocomplete="new-password" required
aria-describedby="ps-text">
<button type="button" class="pw-toggle" tabindex="-1"
onclick="togglePw('password', this)" aria-label="Mostra o nascondi la password">
<svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg>
</button>
</div>
<div class="progress mt-2" style="height:6px;">
<div class="progress-bar" id="ps-bar" role="progressbar" style="width:0%;" aria-hidden="true"></div>
</div>
<div class="form-text" id="ps-text" aria-live="polite"></div>
</div>
<div class="form-group">
<label for="password-confirm" class="active">Conferma Password <span aria-hidden="true">*</span></label>
<div class="pw-wrap">
<input type="password" id="password-confirm" name="password-confirm" class="form-control"
placeholder="Ripeti la password" autocomplete="new-password" required>
<button type="button" class="pw-toggle" tabindex="-1"
onclick="togglePw('password-confirm', this)" aria-label="Mostra o nascondi la password">
<svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-password-visible"></use></svg>
</button>
</div>
</div>
<div class="d-flex" style="gap:.75rem;">
<button type="button" class="btn btn-outline-primary" onclick="goToStep0()" aria-label="Torna allo step precedente">
<svg class="ico" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>
</button>
<button type="submit" class="btn btn-primary flex-grow-1" id="register-btn">
Crea Account
</button>
</div>
</form>
</div>
<!-- STEP 2: Successo -->
<div id="step-2">
<div class="success-body">
<div class="success-icon">
<svg class="icon icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
</div>
<h2 class="h4" id="success-title">Account creato!</h2>
<p id="success-desc" class="text-muted">
Il tuo account NIS2 Agile è pronto. Stai per essere reindirizzato...
</p>
<button class="btn btn-primary w-100" id="success-btn" onclick="goToDashboard()">
Entra nella piattaforma
<svg class="ico ms-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-right"></use></svg>
</button>
</div>
</div>
</div>
</div>
<p class="text-center mt-3 mb-0">
Hai già un account? <a href="login.html">Accedi</a>
</p>
<p class="text-center small text-muted">
Registrandoti accetti i
<a href="https://agentai.agile.software/terms" target="_blank" rel="noopener">Termini di Servizio</a>
e la <a href="https://agentai.agile.software/privacy" target="_blank" rel="noopener">Privacy Policy</a>.
</p>
</main>
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }
</script>
<!-- Stessa logica della register.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260627"></script>
<script src="js/common.js?v=20260627"></script>
<script>
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
// --- State ---
let selectedRole = null;
let inviteValid = false;
let inviteToken = null;
let inviteRecipient = null;
let isReducedReg = false;
let pivaData = null;
let pivaLookupTimer = null;
let successRedirect = 'onboarding.html';
const params = new URLSearchParams(window.location.search);
// --- Role Selection ---
function selectRole(role) {
selectedRole = role;
document.querySelectorAll('.role-card').forEach(c => { c.classList.remove('selected'); c.setAttribute('aria-checked','false'); });
const card = document.getElementById('card-' + role);
if (card) { card.classList.add('selected'); card.setAttribute('aria-checked','true'); }
document.getElementById('btn-next-0').disabled = false;
const pivaGroup = document.getElementById('piva-group');
pivaGroup.style.display = (role === 'consultant') ? 'none' : 'block';
}
function cardKey(e, role) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectRole(role); }
}
// Pre-fill from URL after DOM ready
(function() {
const r = params.get('role');
if (r && document.getElementById('card-' + r)) selectRole(r);
const tok = params.get('invite_token') || params.get('invite') || params.get('token');
if (tok) {
inviteToken = tok;
document.getElementById('invite-details').open = true;
document.getElementById('invite-code').value = tok;
validateInvite(tok);
}
if (params.get('nome')) document.getElementById('firstname').value = params.get('nome');
if (params.get('cognome')) document.getElementById('lastname').value = params.get('cognome');
if (params.get('email')) document.getElementById('email').value = params.get('email');
})();
// --- Invite Code Validation ---
let inviteTimer = null;
function validateInvite(val) {
const statusEl = document.getElementById('invite-status');
clearTimeout(inviteTimer);
val = val.trim();
if (!val) {
inviteValid = false; inviteToken = null; statusEl.innerHTML = ''; statusEl.className = 'lookup-status'; return;
}
statusEl.className = 'lookup-status loading';
statusEl.textContent = 'Verifica codice...';
inviteTimer = setTimeout(async () => {
try {
const res = await fetch(api.baseUrl + '/auth/validate-invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ invite_token: val })
});
const data = await res.json();
if (data.success) {
inviteValid = true; inviteToken = val;
statusEl.className = 'lookup-status ok';
const label = data.data?.label ? ` — ${data.data.label}` : '';
statusEl.textContent = `Codice valido${label} — accesso B2B confermato`;
if (data.role && document.getElementById('card-' + data.role)) selectRole(data.role);
const r = data.data?.recipient;
if (r) {
if (r.first_name) document.getElementById('firstname').value = r.first_name;
if (r.last_name) document.getElementById('lastname').value = r.last_name;
if (r.email) document.getElementById('email').value = r.email;
if (r.vat) { document.getElementById('piva').value = r.vat; lookupPiva(r.vat); }
inviteRecipient = r;
isReducedReg = !!(r.first_name && r.last_name && r.email);
}
if (isReducedReg) applyReducedRegMode();
} else {
inviteValid = false; inviteToken = null;
statusEl.className = 'lookup-status err';
statusEl.textContent = 'Codice non valido';
}
} catch {
inviteValid = false;
statusEl.className = 'lookup-status err';
statusEl.textContent = 'Impossibile verificare';
}
}, 600);
}
// --- P.IVA Lookup ---
function validaFormatoPiva(v) {
if (!/^\d{11}$/.test(v)) return false;
let s = 0;
for (let i = 0; i < 10; i++) {
let n = parseInt(v[i]);
if (i % 2 === 1) { n *= 2; if (n > 9) n -= 9; }
s += n;
}
return (10 - (s % 10)) % 10 === parseInt(v[10]);
}
function lookupPiva(val) {
clearTimeout(pivaLookupTimer);
val = val.replace(/\s/g, '');
const statusEl = document.getElementById('piva-status');
if (val.length < 11) { pivaData = null; statusEl.innerHTML = ''; statusEl.className = 'lookup-status'; return; }
if (val.length === 11 && !validaFormatoPiva(val)) {
pivaData = null;
statusEl.className = 'lookup-status err';
statusEl.textContent = 'P.IVA non valida (cifra di controllo errata)';
return;
}
statusEl.className = 'lookup-status ok';
statusEl.textContent = 'P.IVA valida — ricerca dati azienda...';
pivaLookupTimer = setTimeout(async () => {
try {
const res = await fetch(api.baseUrl + '/onboarding/lookup-piva', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vat_number: val })
});
const data = await res.json();
if (data.success && data.data && data.data.company_name) {
pivaData = data.data;
statusEl.className = 'lookup-status ok';
statusEl.textContent = data.data.company_name;
} else {
pivaData = null;
statusEl.className = 'lookup-status ok';
statusEl.textContent = 'P.IVA valida — inserisci i dati aziendali nel passo successivo';
}
} catch {
pivaData = null;
statusEl.className = 'lookup-status ok';
statusEl.textContent = 'P.IVA valida — inserisci i dati aziendali nel passo successivo';
}
}, 800);
}
// --- Registrazione Ridotta ---
function applyReducedRegMode() {
const name = ((inviteRecipient.first_name || '') + ' ' + (inviteRecipient.last_name || '')).trim();
document.getElementById('invite-welcome-name').textContent = 'Ciao ' + name + '!';
document.getElementById('invite-welcome-plan').textContent = 'Accesso B2B — account pre-configurato dall\'invito';
document.getElementById('invite-welcome').style.display = 'block';
['firstname', 'lastname', 'email'].forEach(id => {
const el = document.getElementById(id);
el.readOnly = true;
});
if (inviteRecipient.vat) document.getElementById('piva-group').style.display = 'none';
if (!selectedRole) selectRole('org_admin');
goToStep1();
}
// --- Step Navigation ---
function setStep(n) {
[0, 1, 2].forEach(i => {
document.getElementById('step-' + i).style.display = (i === n) ? 'block' : 'none';
const si = document.getElementById('si-' + i);
si.className = 'step-item' + (i < n ? ' done' : i === n ? ' active' : '');
if (i < n) si.querySelector('.step-circle').innerHTML = '<svg class="icon icon-xs icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>';
else si.querySelector('.step-circle').textContent = i + 1;
});
document.getElementById('conn-0').className = 'step-connector' + (n > 0 ? ' done' : '');
document.getElementById('conn-1').className = 'step-connector' + (n > 1 ? ' done' : '');
document.getElementById('step-indicator').style.display = (n === 2) ? 'none' : 'flex';
}
function goToStep1() {
if (!selectedRole) return;
document.getElementById('register-error').classList.add('d-none');
const labels = {
compliance_manager: 'Dati account — CISO / Compliance Manager',
org_admin: 'Dati account — Legale Rappresentante',
consultant: 'Dati account — Consulente Cybersecurity',
auditor: 'Dati account — Auditor / Revisore',
board_member: 'Dati account — Board Member / DPO'
};
document.getElementById('auth-subtitle').textContent = labels[selectedRole] || 'Dati account';
setStep(1);
document.getElementById('firstname').focus();
}
function goToStep0() {
document.getElementById('register-error').classList.add('d-none');
document.getElementById('auth-subtitle').textContent = 'Crea il tuo account';
setStep(0);
}
function goToDashboard() { window.location.href = successRedirect; }
function togglePw(id, btn) {
const inp = document.getElementById(id);
inp.type = (inp.type === 'password') ? 'text' : 'password';
const use = btn.querySelector('use');
use.setAttribute('href', 'vendor/bootstrap-italia/dist/svg/sprites.svg#' + (inp.type === 'text' ? 'it-password-invisible' : 'it-password-visible'));
}
// --- Password strength ---
document.getElementById('password').addEventListener('input', function() {
updateStrengthUI(calcPasswordStrength(this.value), this.value);
});
function calcPasswordStrength(pw) {
let s = 0;
if (pw.length >= 8) s++;
if (pw.length >= 12) s++;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++;
if (/\d/.test(pw)) s++;
if (/[^a-zA-Z0-9]/.test(pw)) s++;
return Math.min(4, Math.max(1, s <= 1 ? 1 : s === 2 ? 2 : s === 3 ? 3 : 4));
}
function updateStrengthUI(level, pw) {
const labels = { 1: 'Debole', 2: 'Sufficiente', 3: 'Buona', 4: 'Forte' };
const colors = { 1: '#d9364f', 2: '#e58a00', 3: '#06c', 4: '#008758' };
const bar = document.getElementById('ps-bar');
if (pw.length === 0) { bar.style.width = '0%'; document.getElementById('ps-text').textContent = ''; return; }
bar.style.width = (level * 25) + '%';
bar.style.backgroundColor = colors[level];
document.getElementById('ps-text').textContent = 'Sicurezza: ' + labels[level];
}
// --- Form Submit ---
document.getElementById('register-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('register-error');
errorEl.classList.add('d-none');
const firstname = document.getElementById('firstname').value.trim();
const lastname = document.getElementById('lastname').value.trim();
const email = document.getElementById('email').value.trim();
const piva = document.getElementById('piva').value.trim();
const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('password-confirm').value;
const fullname = firstname + ' ' + lastname;
function showErr(msg) { errorEl.textContent = msg; errorEl.classList.remove('d-none'); }
if (!firstname || !lastname || !email || !password) { showErr('Nome, cognome, email e password sono obbligatori.'); return; }
if (selectedRole !== 'consultant' && !piva) { showErr('Inserisci la Partita IVA della tua azienda.'); return; }
if (password.length < 8) { showErr('La password deve avere almeno 8 caratteri.'); return; }
if (password !== passwordConfirm) { showErr('Le password non coincidono.'); return; }
const btn = document.getElementById('register-btn');
btn.disabled = true;
btn.textContent = 'Registrazione in corso...';
try {
let result;
if (inviteToken) {
result = await fetch(api.baseUrl + '/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email, password, full_name: fullname,
role: selectedRole,
invite_token: inviteToken,
vat_number: piva || undefined
})
}).then(r => r.json());
} else {
result = await api.register(email, password, fullname, selectedRole);
}
if (result.success) {
if (inviteToken) {
successRedirect = 'dashboard.html';
if (result.data?.access_token) {
api.setTokens(result.data.access_token, result.data.refresh_token);
localStorage.setItem('nis2_user_role', result.data.user?.role || selectedRole || 'org_admin');
}
try {
const companyName = pivaData?.company_name || inviteRecipient?.company || 'Azienda';
const vatNum = (piva || inviteRecipient?.vat || '').replace(/[^0-9]/g, '');
if (vatNum.length === 11) {
btn.textContent = 'Configurazione in corso...';
const provRes = await fetch(api.baseUrl + '/services/provision', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
invite_token: inviteToken,
company: {
ragione_sociale: companyName,
partita_iva: vatNum,
sector: pivaData?.sector || 'ict',
nis2_entity_type: 'important'
},
admin: { email, full_name: fullname },
caller: { system: 'self-register' }
})
});
const provData = await provRes.json();
if (provData.success && provData.data?.org_id) {
localStorage.setItem('nis2_org_id', String(provData.data.org_id));
}
}
} catch { /* provision fallita */ }
} else if (selectedRole === 'consultant') {
successRedirect = 'companies.html';
} else {
successRedirect = 'onboarding.html';
}
const titles = {
consultant: 'Account Consulente creato!',
board_member: 'Account Board Member creato!',
};
const descs = {
consultant: 'Puoi ora gestire i tuoi clienti dalla sezione Aziende.',
org_admin: pivaData ? 'Abbiamo trovato ' + pivaData.company_name + '. Completa l\'onboarding per configurare la tua organizzazione.' : 'Completa l\'onboarding per configurare la tua organizzazione NIS2.',
};
document.getElementById('success-title').textContent = titles[selectedRole] || 'Account creato!';
document.getElementById('success-desc').textContent = descs[selectedRole] || 'Il tuo account NIS2 Agile è pronto. Stai per essere reindirizzato...';
document.getElementById('auth-subtitle').textContent = 'Registrazione completata';
setStep(2);
setTimeout(goToDashboard, 2500);
} else {
showErr(result.message || 'Errore durante la registrazione.');
}
} catch (err) {
showErr('Errore di connessione al server.');
} finally {
btn.disabled = false;
btn.textContent = 'Crea Account';
}
});
</script>
</body>
</html>