nis2-agile/public/settings.html

1371 lines
76 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impostazioni - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.settings-tabs {
display: flex;
border-bottom: 2px solid var(--gray-200);
margin-bottom: 24px;
gap: 0;
overflow-x: auto;
}
.settings-tab {
padding: 12px 24px;
font-size: 0.875rem;
font-weight: 600;
color: var(--gray-500);
cursor: pointer;
border: none;
background: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all var(--transition-fast);
white-space: nowrap;
font-family: inherit;
}
.settings-tab:hover {
color: var(--gray-700);
}
.settings-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
.settings-section {
margin-bottom: 32px;
}
.settings-section-title {
font-size: 1rem;
font-weight: 700;
color: var(--gray-900);
margin-bottom: 4px;
}
.settings-section-desc {
font-size: 0.8125rem;
color: var(--gray-500);
margin-bottom: 20px;
}
.entity-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
border-radius: var(--border-radius-lg);
font-weight: 700;
font-size: 0.9rem;
margin-bottom: 8px;
}
.entity-badge.essential {
background: var(--danger-bg);
color: var(--danger);
border: 1px solid var(--danger);
}
.entity-badge.important {
background: var(--warning-bg);
color: #a16207;
border: 1px solid var(--warning);
}
.entity-badge.not_applicable {
background: var(--gray-100);
color: var(--gray-600);
border: 1px solid var(--gray-300);
}
.entity-badge.voluntary {
background: var(--primary-bg);
color: var(--primary);
border: 1px solid var(--primary-light);
}
.plan-badge {
display: inline-flex;
align-items: center;
padding: 6px 16px;
border-radius: 100px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.plan-badge.free { background: var(--gray-100); color: var(--gray-600); }
.plan-badge.professional { background: var(--primary-bg); color: var(--primary); }
.plan-badge.enterprise { background: #f3e8ff; color: #7c3aed; }
.password-section {
border-top: 1px solid var(--gray-200);
padding-top: 24px;
margin-top: 24px;
}
.strength-bar {
display: flex;
gap: 4px;
margin-top: 8px;
margin-bottom: 4px;
}
.strength-segment {
flex: 1;
height: 4px;
border-radius: 2px;
background: var(--gray-200);
transition: background var(--transition-fast);
}
.strength-segment.active.weak { background: var(--danger); }
.strength-segment.active.fair { background: #f97316; }
.strength-segment.active.good { background: var(--warning); }
.strength-segment.active.strong { background: var(--secondary); }
.strength-text {
font-size: 0.7rem;
color: var(--gray-500);
}
.member-role-badge {
display: inline-flex;
padding: 3px 10px;
border-radius: 100px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.member-role-badge.org_admin { background: var(--primary-bg); color: var(--primary); }
.member-role-badge.compliance_manager { background: var(--secondary-bg); color: #15803d; }
.member-role-badge.board_member { background: #f3e8ff; color: #7c3aed; }
.member-role-badge.auditor { background: var(--info-bg); color: var(--primary); }
.member-role-badge.employee { background: var(--gray-100); color: var(--gray-600); }
.security-item {
padding: 20px;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
margin-bottom: 16px;
}
.security-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.security-item-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--gray-800);
}
.security-item-desc {
font-size: 0.8125rem;
color: var(--gray-500);
line-height: 1.5;
}
.coming-soon-badge {
display: inline-flex;
padding: 3px 10px;
border-radius: 100px;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
background: var(--gray-100);
color: var(--gray-500);
}
.btn-icon-action {
background: none;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius-sm);
color: var(--gray-500);
cursor: pointer;
padding: 6px;
transition: all var(--transition-fast);
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-icon-action:hover {
border-color: var(--danger);
color: var(--danger);
background: var(--danger-bg);
}
.btn-icon-action svg {
width: 16px;
height: 16px;
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="settings.title">Impostazioni</h2>
<div class="content-header-actions">
<span class="text-muted" id="header-org-name"></span>
</div>
</header>
<div class="content-body">
<!-- Tab Navigation -->
<div class="settings-tabs">
<button class="settings-tab active" onclick="switchTab('org')">Organizzazione</button>
<button class="settings-tab" onclick="switchTab('profile')">Profilo</button>
<button class="settings-tab" onclick="switchTab('members')">Membri</button>
<button class="settings-tab" onclick="switchTab('security')">Sicurezza</button>
<button class="settings-tab" onclick="switchTab('apikeys')">API Keys</button>
<button class="settings-tab" onclick="switchTab('webhooks')">Webhook</button>
</div>
<!-- ══════════════ TAB: Organizzazione ══════════════ -->
<div class="tab-panel active" id="tab-org">
<div class="card">
<div class="card-body">
<div class="settings-section">
<h3 class="settings-section-title">Classificazione NIS2</h3>
<p class="settings-section-desc">La classificazione viene calcolata automaticamente in base a settore, numero di dipendenti e fatturato.</p>
<div id="entity-classification">
<div class="spinner" style="margin:20px auto;"></div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">Piano di Abbonamento</h3>
<p class="settings-section-desc">Il piano attivo per questa organizzazione.</p>
<div id="subscription-plan">
<div class="spinner" style="margin:12px 0;"></div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">Dati Organizzazione</h3>
<p class="settings-section-desc">Informazioni generali dell'organizzazione soggetta a compliance NIS2.</p>
<form id="org-form" onsubmit="saveOrganization(event)">
<div class="form-row">
<div class="form-group">
<label class="form-label">Nome Organizzazione <span class="required">*</span></label>
<input type="text" class="form-input" id="org-name" required>
</div>
<div class="form-group">
<label class="form-label">Settore NIS2 <span class="required">*</span></label>
<select class="form-select" id="org-sector" required>
<option value="">Seleziona settore...</option>
<option value="energy">Energia</option>
<option value="transport">Trasporti</option>
<option value="banking">Banche</option>
<option value="health">Sanita'</option>
<option value="water">Acqua</option>
<option value="digital_infra">Infrastrutture Digitali</option>
<option value="public_admin">Pubblica Amministrazione</option>
<option value="manufacturing">Manifatturiero</option>
<option value="postal">Servizi Postali</option>
<option value="chemical">Chimico</option>
<option value="food">Alimentare</option>
<option value="waste">Rifiuti</option>
<option value="ict_services">Servizi ICT</option>
<option value="digital_providers">Provider Digitali</option>
<option value="space">Spazio</option>
<option value="research">Ricerca</option>
<option value="other">Altro</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Partita IVA</label>
<input type="text" class="form-input" id="org-vat" placeholder="IT12345678901">
</div>
<div class="form-group">
<label class="form-label">Codice Fiscale</label>
<input type="text" class="form-input" id="org-fiscal">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Numero Dipendenti</label>
<input type="number" class="form-input" id="org-employees" min="0" placeholder="0">
</div>
<div class="form-group">
<label class="form-label">Fatturato Annuo (EUR)</label>
<input type="number" class="form-input" id="org-turnover" min="0" step="1000" placeholder="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Paese</label>
<input type="text" class="form-input" id="org-country" placeholder="IT">
</div>
<div class="form-group">
<label class="form-label">Citta'</label>
<input type="text" class="form-input" id="org-city">
</div>
</div>
<div class="form-group">
<label class="form-label">Indirizzo</label>
<input type="text" class="form-input" id="org-address">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Sito Web</label>
<input type="url" class="form-input" id="org-website" placeholder="https://...">
</div>
<div class="form-group">
<label class="form-label">Email di Contatto</label>
<input type="email" class="form-input" id="org-email">
</div>
</div>
<div class="form-group">
<label class="form-label">Telefono di Contatto</label>
<input type="tel" class="form-input" id="org-phone" style="max-width:320px;">
</div>
<button type="submit" class="btn btn-primary" id="btn-save-org">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg>
Salva Modifiche
</button>
</form>
</div>
</div>
</div>
</div>
<!-- ══════════════ TAB: Profilo ══════════════ -->
<div class="tab-panel" id="tab-profile">
<div class="card mb-24">
<div class="card-header">
<h3>Informazioni Personali</h3>
</div>
<div class="card-body">
<div class="settings-section">
<p class="settings-section-desc">Aggiorna le tue informazioni personali e le preferenze dell'account.</p>
<form id="profile-form" onsubmit="saveProfile(event)">
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" id="profile-email" disabled style="background:var(--gray-50); max-width:400px;">
<span class="form-help">L'email non puo' essere modificata.</span>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Nome Completo <span class="required">*</span></label>
<input type="text" class="form-input" id="profile-name" required>
</div>
<div class="form-group">
<label class="form-label">Telefono</label>
<input type="tel" class="form-input" id="profile-phone">
</div>
</div>
<div class="form-group" style="max-width:320px;">
<label class="form-label">Lingua Preferita</label>
<select class="form-select" id="profile-language">
<option value="it">Italiano</option>
<option value="en">English</option>
</select>
</div>
<button type="submit" class="btn btn-primary" id="btn-save-profile">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg>
Aggiorna Profilo
</button>
</form>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>Cambia Password</h3>
</div>
<div class="card-body">
<p class="settings-section-desc">Per motivi di sicurezza, dopo il cambio password verrai disconnesso da tutte le sessioni.</p>
<form id="password-form" onsubmit="changePassword(event)">
<div class="form-group" style="max-width:400px;">
<label class="form-label">Password Attuale <span class="required">*</span></label>
<input type="password" class="form-input" id="pw-current" required autocomplete="current-password">
</div>
<div class="form-group" style="max-width:400px;">
<label class="form-label">Nuova Password <span class="required">*</span></label>
<input type="password" class="form-input" id="pw-new" required autocomplete="new-password" oninput="checkPasswordStrength(this.value)">
<div class="strength-bar" id="pw-strength-bar">
<div class="strength-segment" id="pw-seg-1"></div>
<div class="strength-segment" id="pw-seg-2"></div>
<div class="strength-segment" id="pw-seg-3"></div>
<div class="strength-segment" id="pw-seg-4"></div>
</div>
<div class="strength-text" id="pw-strength-text"></div>
</div>
<div class="form-group" style="max-width:400px;">
<label class="form-label">Conferma Nuova Password <span class="required">*</span></label>
<input type="password" class="form-input" id="pw-confirm" required autocomplete="new-password">
</div>
<button type="submit" class="btn btn-warning" id="btn-change-pw">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>
Cambia Password
</button>
</form>
</div>
</div>
</div>
<!-- ══════════════ TAB: Membri ══════════════ -->
<div class="tab-panel" id="tab-members">
<div class="card">
<div class="card-header">
<h3>Membri dell'Organizzazione</h3>
<button class="btn btn-primary btn-sm" onclick="showInviteModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Invita Membro
</button>
</div>
<div class="card-body" style="padding:0;">
<div id="members-container">
<div class="spinner" style="margin:40px auto;"></div>
</div>
</div>
</div>
</div>
<!-- ══════════════ TAB: Sicurezza ══════════════ -->
<div class="tab-panel" id="tab-security">
<div class="card mb-24">
<div class="card-header">
<h3>Sicurezza Account</h3>
</div>
<div class="card-body">
<div class="security-item">
<div class="security-item-header">
<span class="security-item-title">Sessione Corrente</span>
<span class="badge badge-success">Attiva</span>
</div>
<p class="security-item-desc" id="session-info">Sessione autenticata tramite token JWT. L'accesso e' protetto da crittografia.</p>
</div>
<div class="security-item">
<div class="security-item-header">
<span class="security-item-title">Autenticazione a Due Fattori (2FA)</span>
<span class="coming-soon-badge">Prossimamente</span>
</div>
<p class="security-item-desc">L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account. Questa funzionalita' sara' disponibile in un futuro aggiornamento.</p>
</div>
<div class="security-item">
<div class="security-item-header">
<span class="security-item-title">Accesso API</span>
<span class="badge badge-info">JWT</span>
</div>
<p class="security-item-desc">L'accesso alle API avviene tramite token JWT (JSON Web Token) con scadenza automatica e meccanismo di refresh. Tutti i token vengono invalidati al cambio password.</p>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3>Registro Attivita' Recenti</h3>
</div>
<div class="card-body" style="padding:0;">
<div id="audit-log-container">
<div class="spinner" style="margin:40px auto;"></div>
</div>
</div>
</div>
</div>
<!-- ══════════════ TAB: API Keys ══════════════ -->
<div class="tab-panel" id="tab-apikeys">
<div class="card mb-24">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<div>
<h3>API Keys</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">
Chiavi per accesso esterno alle API NIS2 Agile (SIEM, GRC, dashboard esterne).
</p>
</div>
<button class="btn btn-primary" onclick="showCreateApiKeyModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuova API Key
</button>
</div>
<div class="card-body" style="padding:0;">
<div id="apikeys-container"><div class="spinner" style="margin:40px auto;"></div></div>
</div>
</div>
<div class="card">
<div class="card-header"><h3>Scope Disponibili</h3></div>
<div class="card-body">
<div id="available-scopes-container"></div>
</div>
</div>
</div>
<!-- ══════════════ TAB: Webhook ══════════════ -->
<div class="tab-panel" id="tab-webhooks">
<div class="card mb-24">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<div>
<h3>Webhook Subscriptions</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">
Notifiche push verso sistemi esterni (SIEM, 231 Agile, SustainAI) su eventi NIS2.
</p>
</div>
<button class="btn btn-primary" onclick="showCreateWebhookModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuovo Webhook
</button>
</div>
<div class="card-body" style="padding:0;">
<div id="webhooks-container"><div class="spinner" style="margin:40px auto;"></div></div>
</div>
</div>
<div class="card">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
<h3>Delivery Log</h3>
<button class="btn btn-sm btn-secondary" onclick="loadDeliveries()">Aggiorna</button>
</div>
<div class="card-body" style="padding:0;">
<div id="deliveries-container"><div class="empty-state" style="padding:32px;"><p>Seleziona un webhook per vedere i delivery.</p></div></div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/common.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/help.js"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
// ── Init ─────────────────────────────────────────────────
loadSidebar();
I18n.init();
HelpSystem.init();
let currentOrg = null;
let currentUser = null;
let currentOrgId = localStorage.getItem('nis2_org_id');
// Carica dati iniziali
loadOrgData();
loadProfileData();
// ── Tab Navigation ───────────────────────────────────────
function switchTab(tab) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
const tabMap = { org: 0, profile: 1, members: 2, security: 3, apikeys: 4, webhooks: 5 };
const panelMap = { org: 'tab-org', profile: 'tab-profile', members: 'tab-members', security: 'tab-security', apikeys: 'tab-apikeys', webhooks: 'tab-webhooks' };
document.querySelectorAll('.settings-tab')[tabMap[tab]].classList.add('active');
document.getElementById(panelMap[tab]).classList.add('active');
if (tab === 'members') loadMembers();
if (tab === 'security') loadAuditLog();
if (tab === 'apikeys') loadApiKeys();
if (tab === 'webhooks') { loadWebhooks(); loadDeliveries(); }
}
// ── Settori NIS2 ─────────────────────────────────────────
const sectorLabels = {
energy: 'Energia', transport: 'Trasporti', banking: 'Banche',
health: 'Sanita\'', water: 'Acqua', digital_infra: 'Infrastrutture Digitali',
public_admin: 'Pubblica Amministrazione', manufacturing: 'Manifatturiero',
postal: 'Servizi Postali', chemical: 'Chimico', food: 'Alimentare',
waste: 'Rifiuti', ict_services: 'Servizi ICT',
digital_providers: 'Provider Digitali', space: 'Spazio',
research: 'Ricerca', other: 'Altro'
};
const entityTypeLabels = {
essential: 'Essenziale',
important: 'Importante',
not_applicable: 'Non Applicabile'
};
const roleLabels = {
org_admin: 'Amministratore',
compliance_manager: 'Compliance Manager',
board_member: 'Membro CDA',
auditor: 'Auditor',
employee: 'Dipendente'
};
// ── Organizzazione ───────────────────────────────────────
async function loadOrgData() {
try {
const result = await api.getCurrentOrg();
if (result.success && result.data) {
currentOrg = result.data;
fillOrgForm(currentOrg);
renderEntityClassification(currentOrg);
renderSubscriptionPlan(currentOrg);
document.getElementById('header-org-name').textContent = currentOrg.name || '';
} else {
showNotification('Impossibile caricare i dati dell\'organizzazione.', 'error');
}
} catch (e) {
console.error('Errore caricamento org:', e);
}
}
function fillOrgForm(org) {
document.getElementById('org-name').value = org.name || '';
document.getElementById('org-sector').value = org.sector || '';
document.getElementById('org-vat').value = org.vat_number || '';
document.getElementById('org-fiscal').value = org.fiscal_code || '';
document.getElementById('org-employees').value = org.employee_count || '';
document.getElementById('org-turnover').value = org.annual_turnover_eur || '';
document.getElementById('org-country').value = org.country || '';
document.getElementById('org-city').value = org.city || '';
document.getElementById('org-address').value = org.address || '';
document.getElementById('org-website').value = org.website || '';
document.getElementById('org-email').value = org.contact_email || '';
document.getElementById('org-phone').value = org.contact_phone || '';
}
function renderEntityClassification(org) {
const type = org.entity_type || 'not_applicable';
const isVoluntary = org.voluntary_compliance == 1 && type === 'not_applicable';
const displayLabel = isVoluntary ? 'Adesione Volontaria' : (entityTypeLabels[type] || 'Non Applicabile');
const badgeClass = isVoluntary ? 'voluntary' : type;
let description = '';
if (isVoluntary) {
description = 'L\'organizzazione ha scelto di aderire <strong>volontariamente</strong> ai requisiti della Direttiva NIS2, pur non rientrando nell\'ambito di applicazione obbligatorio. Le misure di sicurezza sono adottate su base volontaria.';
} else if (type === 'essential') {
description = 'L\'organizzazione rientra tra le entita\' <strong>Essenziali</strong> ai sensi della Direttiva NIS2. Soggetta a supervisione proattiva con sanzioni fino a EUR 10M o 2% del fatturato globale.';
} else if (type === 'important') {
description = 'L\'organizzazione rientra tra le entita\' <strong>Importanti</strong> ai sensi della Direttiva NIS2. Soggetta a supervisione reattiva con sanzioni fino a EUR 7M o 1,4% del fatturato globale.';
} else {
description = 'In base ai parametri attuali, l\'organizzazione <strong>non rientra</strong> nell\'ambito di applicazione della Direttiva NIS2. Si consiglia comunque di adottare le best practice di cybersecurity.';
}
// Voluntary toggle (only visible for not_applicable entities)
const voluntaryToggle = type === 'not_applicable' ? `
<div style="margin-top:16px; padding:16px; background:var(--gray-50); border-radius:var(--border-radius); border:1px solid var(--gray-200);">
<label style="display:flex; align-items:center; gap:10px; cursor:pointer;">
<input type="checkbox" id="org-voluntary" ${isVoluntary ? 'checked' : ''}
onchange="toggleVoluntaryCompliance(this.checked)"
style="accent-color:var(--primary); width:18px; height:18px; flex-shrink:0;">
<span style="font-size:0.875rem; font-weight:600; color:var(--gray-700);">Adesione Volontaria alla NIS2</span>
</label>
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:6px; margin-left:28px;">
Aderisci volontariamente ai requisiti NIS2 anche se non obbligato dalla normativa.
</p>
</div>` : '';
document.getElementById('entity-classification').innerHTML = `
<div class="entity-badge ${badgeClass}">
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
Entita': ${displayLabel}
</div>
<p style="font-size:0.8125rem; color:var(--gray-600); line-height:1.6; max-width:640px;">${description}</p>
${voluntaryToggle}
`;
}
async function toggleVoluntaryCompliance(checked) {
if (!currentOrg) return;
try {
const result = await api.updateOrganization(currentOrg.id, { voluntary_compliance: checked ? 1 : 0 });
if (result.success) {
currentOrg.voluntary_compliance = checked ? 1 : 0;
renderEntityClassification(currentOrg);
showNotification(checked ? 'Adesione volontaria attivata.' : 'Adesione volontaria disattivata.', 'success');
} else {
showNotification(result.message || 'Errore.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
function renderSubscriptionPlan(org) {
const plan = org.subscription_plan || 'free';
const planLabels = { free: 'Free', professional: 'Professional', enterprise: 'Enterprise' };
document.getElementById('subscription-plan').innerHTML = `
<span class="plan-badge ${plan}">${planLabels[plan] || plan}</span>
`;
}
async function saveOrganization(e) {
e.preventDefault();
if (!currentOrg) return;
const btn = document.getElementById('btn-save-org');
btn.disabled = true;
btn.textContent = 'Salvataggio...';
const data = {
name: document.getElementById('org-name').value.trim(),
sector: document.getElementById('org-sector').value,
vat_number: document.getElementById('org-vat').value.trim(),
fiscal_code: document.getElementById('org-fiscal').value.trim(),
employee_count: parseInt(document.getElementById('org-employees').value) || 0,
annual_turnover_eur: parseFloat(document.getElementById('org-turnover').value) || 0,
country: document.getElementById('org-country').value.trim(),
city: document.getElementById('org-city').value.trim(),
address: document.getElementById('org-address').value.trim(),
website: document.getElementById('org-website').value.trim(),
contact_email: document.getElementById('org-email').value.trim(),
contact_phone: document.getElementById('org-phone').value.trim()
};
try {
const result = await api.updateOrganization(currentOrg.id, data);
if (result.success) {
showNotification('Organizzazione aggiornata con successo!', 'success');
// Ricarica dati per aggiornare classificazione
loadOrgData();
} else {
showNotification(result.message || 'Errore nel salvataggio.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = `
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg>
Salva Modifiche`;
}
}
// ── Profilo ──────────────────────────────────────────────
async function loadProfileData() {
try {
const result = await api.getMe();
if (result.success && result.data) {
currentUser = result.data;
document.getElementById('profile-email').value = currentUser.email || '';
document.getElementById('profile-name').value = currentUser.full_name || '';
document.getElementById('profile-phone').value = currentUser.phone || '';
document.getElementById('profile-language').value = currentUser.preferred_language || 'it';
}
} catch (e) {
console.error('Errore caricamento profilo:', e);
}
}
async function saveProfile(e) {
e.preventDefault();
const btn = document.getElementById('btn-save-profile');
btn.disabled = true;
btn.textContent = 'Salvataggio...';
const data = {
full_name: document.getElementById('profile-name').value.trim(),
phone: document.getElementById('profile-phone').value.trim(),
preferred_language: document.getElementById('profile-language').value
};
try {
const response = await fetch('/api/auth/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('nis2_access_token')
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showNotification('Profilo aggiornato con successo!', 'success');
} else {
showNotification(result.message || 'Errore nell\'aggiornamento.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = `
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg>
Aggiorna Profilo`;
}
}
// ── Password ─────────────────────────────────────────────
function checkPasswordStrength(password) {
let score = 0;
let label = '';
let cls = '';
if (password.length >= 8) score++;
if (password.length >= 12) score++;
if (/[A-Z]/.test(password) && /[a-z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
if (score <= 1) { label = 'Debole'; cls = 'weak'; }
else if (score <= 2) { label = 'Sufficiente'; cls = 'fair'; }
else if (score <= 3) { label = 'Buona'; cls = 'good'; }
else { label = 'Forte'; cls = 'strong'; }
const segments = Math.min(score, 4);
for (let i = 1; i <= 4; i++) {
const seg = document.getElementById('pw-seg-' + i);
seg.className = 'strength-segment';
if (i <= segments) {
seg.classList.add('active', cls);
}
}
document.getElementById('pw-strength-text').textContent = password.length > 0 ? 'Sicurezza: ' + label : '';
}
async function changePassword(e) {
e.preventDefault();
const currentPw = document.getElementById('pw-current').value;
const newPw = document.getElementById('pw-new').value;
const confirmPw = document.getElementById('pw-confirm').value;
if (newPw !== confirmPw) {
showNotification('Le password non coincidono.', 'error');
return;
}
if (newPw.length < 8) {
showNotification('La password deve essere di almeno 8 caratteri.', 'error');
return;
}
const btn = document.getElementById('btn-change-pw');
btn.disabled = true;
btn.textContent = 'Aggiornamento...';
try {
const response = await fetch('/api/auth/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('nis2_access_token')
},
body: JSON.stringify({
current_password: currentPw,
new_password: newPw
})
});
const result = await response.json();
if (result.success) {
showNotification('Password modificata. Verrai disconnesso...', 'success');
setTimeout(() => {
api.clearTokens();
window.location.href = '/login.html';
}, 2000);
} else {
showNotification(result.message || 'Errore nel cambio password.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = `
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>
Cambia Password`;
}
}
// ── Membri ───────────────────────────────────────────────
async function loadMembers() {
const container = document.getElementById('members-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const result = await api.request('GET', '/organizations/' + currentOrgId + '/members');
if (result.success && result.data) {
renderMembersTable(result.data);
} else {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>
<h4>Nessun membro trovato</h4>
<p>Invita i membri del team per collaborare alla compliance NIS2.</p>
</div>`;
}
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore nel caricamento dei membri</h4></div>';
}
}
function renderMembersTable(members) {
const container = document.getElementById('members-container');
if (!members || members.length === 0) {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/></svg>
<h4>Nessun membro trovato</h4>
<p>Invita i membri del team per collaborare alla compliance NIS2.</p>
</div>`;
return;
}
let html = `
<div class="table-container">
<table>
<thead>
<tr>
<th>Nome</th>
<th>Email</th>
<th>Ruolo</th>
<th>Data Ingresso</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>`;
members.forEach(m => {
const role = m.org_role || 'employee';
const roleLabel = roleLabels[role] || role;
html += `
<tr>
<td><strong>${escapeHtml(m.full_name || '-')}</strong></td>
<td>${escapeHtml(m.email || '-')}</td>
<td><span class="member-role-badge ${role}">${escapeHtml(roleLabel)}</span></td>
<td>${formatDate(m.joined_at)}</td>
<td>
<button class="btn-icon-action" onclick="removeMember(${m.id}, '${escapeHtml(m.full_name || m.email)}')" title="Rimuovi membro">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
</button>
</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
function showInviteModal() {
showModal('Invita Membro', `
<p style="margin-bottom:16px; font-size:0.875rem; color:var(--gray-600);">Aggiungi un membro esistente alla tua organizzazione. L'utente deve gia' essere registrato sulla piattaforma.</p>
<div class="form-group">
<label class="form-label">Email del Membro <span class="required">*</span></label>
<input type="email" class="form-input" id="invite-email" placeholder="utente@esempio.it" required>
</div>
<div class="form-group">
<label class="form-label">Ruolo <span class="required">*</span></label>
<select class="form-select" id="invite-role">
<option value="employee">Dipendente</option>
<option value="compliance_manager">Compliance Manager</option>
<option value="board_member">Membro CDA</option>
<option value="auditor">Auditor</option>
<option value="org_admin">Amministratore</option>
</select>
</div>
`, {
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="doInviteMember()">Invita</button>
`
});
}
async function doInviteMember() {
const email = document.getElementById('invite-email').value.trim();
const role = document.getElementById('invite-role').value;
if (!email) {
showNotification('Inserisci l\'email del membro.', 'warning');
return;
}
closeModal();
try {
const result = await api.request('POST', '/organizations/' + currentOrgId + '/invite', {
email: email,
role: role
});
if (result.success) {
showNotification('Membro aggiunto con successo!', 'success');
loadMembers();
} else {
showNotification(result.message || 'Errore nell\'invito.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
async function removeMember(userId, name) {
showModal('Conferma Rimozione', `
<p style="font-size:0.875rem; color:var(--gray-700);">
Sei sicuro di voler rimuovere <strong>${escapeHtml(name)}</strong> dall'organizzazione?
</p>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:8px;">
L'utente perdera' l'accesso a tutti i dati dell'organizzazione. Questa azione non puo' essere annullata.
</p>
`, {
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-danger" onclick="doRemoveMember(${userId})">Rimuovi</button>
`
});
}
async function doRemoveMember(userId) {
closeModal();
try {
const result = await api.request('DELETE', '/organizations/' + currentOrgId + '/members/' + userId);
if (result.success) {
showNotification('Membro rimosso con successo.', 'success');
loadMembers();
} else {
showNotification(result.message || 'Errore nella rimozione.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ── API Keys ─────────────────────────────────────────────
async function loadApiKeys() {
const container = document.getElementById('apikeys-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const result = await api.request('GET', '/webhooks/api-keys');
if (result.success) {
renderApiKeys(result.data.api_keys || []);
renderAvailableScopes(result.data.available_scopes || {});
}
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore caricamento API Keys</h4></div>'; }
}
function renderApiKeys(keys) {
const container = document.getElementById('apikeys-container');
if (!keys.length) {
container.innerHTML = `<div class="empty-state" style="padding:40px;"><svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/></svg><h4>Nessuna API Key</h4><p>Crea una chiave per integrare sistemi esterni.</p></div>`;
return;
}
let html = `<div class="table-container"><table><thead><tr><th>Nome</th><th>Prefisso</th><th>Scopes</th><th>Ultimo Uso</th><th>Scadenza</th><th>Stato</th><th>Azioni</th></tr></thead><tbody>`;
keys.forEach(k => {
const active = k.is_active ? '<span class="badge badge-success">Attiva</span>' : '<span class="badge badge-danger">Revocata</span>';
const scopes = (k.scopes || []).map(s => `<span class="badge badge-neutral" style="font-size:0.65rem;">${escapeHtml(s)}</span>`).join(' ');
html += `<tr>
<td><strong>${escapeHtml(k.name)}</strong><br><small style="color:var(--gray-400);">Creata da ${escapeHtml(k.created_by_name || '-')}</small></td>
<td><code style="font-size:0.8rem;">${escapeHtml(k.key_prefix)}...</code></td>
<td>${scopes}</td>
<td>${k.last_used_at ? formatDateTime(k.last_used_at) : '<span style="color:var(--gray-400);">Mai</span>'}</td>
<td>${k.expires_at ? formatDate(k.expires_at) : '<span style="color:var(--gray-400);">Nessuna</span>'}</td>
<td>${active}</td>
<td>${k.is_active ? `<button class="btn-icon-action btn-danger-hover" onclick="revokeApiKey(${k.id}, '${escapeHtml(k.name)}')" title="Revoca"><svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9z" clip-rule="evenodd"/></svg></button>` : ''}</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
function renderAvailableScopes(scopes) {
const container = document.getElementById('available-scopes-container');
let html = '<div style="display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:12px;">';
Object.entries(scopes).forEach(([key, desc]) => {
html += `<div style="padding:12px; background:var(--gray-50); border-radius:var(--border-radius); border:1px solid var(--gray-200);">
<code style="font-size:0.8rem; color:var(--primary);">${escapeHtml(key)}</code>
<p style="font-size:0.8rem; color:var(--gray-600); margin-top:4px;">${escapeHtml(desc)}</p>
</div>`;
});
html += '</div>';
container.innerHTML = html;
}
function showCreateApiKeyModal() {
showModal('Crea API Key', `
<div class="form-group">
<label class="form-label">Nome *</label>
<input type="text" id="apikey-name" class="form-control" placeholder="Es. SIEM Integration, Dashboard ESG..." required>
</div>
<div class="form-group">
<label class="form-label">Scopes *</label>
<div id="apikey-scopes-checkboxes" style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:8px;">
${['read:all','read:compliance','read:risks','read:incidents','read:assets','read:supply_chain','read:policies','admin:licenses','admin:org','sso:login'].map(s =>
`<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" name="scope" value="${s}" style="accent-color:var(--primary);">
<code style="font-size:0.75rem;">${s}</code>
</label>`
).join('')}
</div>
</div>
<div class="form-group">
<label class="form-label">Scadenza (opzionale)</label>
<input type="date" id="apikey-expires" class="form-control">
</div>
`, '<button class="btn btn-primary" onclick="createApiKey()">Crea API Key</button>');
}
async function createApiKey() {
const name = document.getElementById('apikey-name').value.trim();
const scopes = [...document.querySelectorAll('input[name="scope"]:checked')].map(cb => cb.value);
const expires = document.getElementById('apikey-expires').value;
if (!name) { showNotification('Inserisci un nome per la chiave.', 'error'); return; }
if (!scopes.length) { showNotification('Seleziona almeno uno scope.', 'error'); return; }
try {
const result = await api.request('POST', '/webhooks/api-keys', { name, scopes, expires_at: expires || null });
if (result.success) {
closeModal();
showModal('API Key Creata', `
<div style="padding:16px; background:var(--warning-bg); border:1px solid var(--warning); border-radius:var(--border-radius); margin-bottom:16px;">
<strong style="color:#a16207;">⚠ Salva questa chiave ora. Non sarà più visibile.</strong>
</div>
<div style="padding:12px; background:var(--gray-900); border-radius:var(--border-radius); font-family:monospace; font-size:0.875rem; color:#34d399; word-break:break-all; user-select:all;">
${escapeHtml(result.data.key)}
</div>
<p style="font-size:0.8rem; color:var(--gray-500); margin-top:8px;">Usa come header: <code>X-API-Key: ${escapeHtml(result.data.key)}</code></p>
`, '<button class="btn btn-primary" onclick="closeModal(); loadApiKeys();">Ho salvato la chiave</button>');
} else { showNotification(result.message || 'Errore.', 'error'); }
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function revokeApiKey(id, name) {
if (!confirm(`Revocare la chiave "${name}"? L'operazione è irreversibile.`)) return;
try {
const result = await api.request('DELETE', `/webhooks/api-keys/${id}`);
if (result.success) { showNotification('API Key revocata.', 'success'); loadApiKeys(); }
else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
// ── Webhooks ─────────────────────────────────────────────
const availableEvents = {
'incident.created': 'Nuovo incidente', 'incident.updated': 'Incidente aggiornato',
'incident.significant': 'Incidente significativo (Art.23)', 'incident.deadline_warning': 'Scadenza Art.23 imminente',
'risk.high_created': 'Rischio HIGH/CRITICAL', 'risk.updated': 'Rischio aggiornato',
'compliance.score_changed': 'Variazione compliance >5%', 'policy.approved': 'Policy approvata',
'policy.created': 'Nuova policy', 'supplier.risk_flagged': 'Fornitore a rischio',
'assessment.completed': 'Assessment completato', 'whistleblowing.received': 'Nuova segnalazione',
'normative.update': 'Aggiornamento normativo', 'webhook.test': 'Test',
'*': 'Tutti gli eventi (wildcard)'
};
async function loadWebhooks() {
const container = document.getElementById('webhooks-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const result = await api.request('GET', '/webhooks/subscriptions');
if (result.success) renderWebhooks(result.data.subscriptions || []);
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore caricamento webhook</h4></div>'; }
}
function renderWebhooks(subs) {
const container = document.getElementById('webhooks-container');
if (!subs.length) {
container.innerHTML = `<div class="empty-state" style="padding:40px;"><svg viewBox="0 0 20 20" fill="currentColor" width="40" height="40" style="color:var(--gray-300)"><path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z"/><path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z"/></svg><h4>Nessun Webhook</h4><p>Configura webhook per notifiche push verso SIEM e sistemi esterni.</p></div>`;
return;
}
let html = `<div class="table-container"><table><thead><tr><th>Nome</th><th>URL</th><th>Eventi</th><th>Delivery</th><th>Stato</th><th>Azioni</th></tr></thead><tbody>`;
subs.forEach(s => {
const evts = (s.events || []).map(e => `<span class="badge badge-neutral" style="font-size:0.65rem;">${escapeHtml(e)}</span>`).join(' ');
const active = s.is_active ? '<span class="badge badge-success">Attivo</span>' : '<span class="badge badge-warning">Pausa</span>';
const deliveryInfo = `<small style="color:var(--gray-500);">${s.success_deliveries||0}${s.failed_deliveries||0}✗</small>`;
html += `<tr>
<td><strong>${escapeHtml(s.name)}</strong>${s.failure_count >= 5 ? '<br><span style="color:var(--danger);font-size:0.75rem;">⚠ '+s.failure_count+' errori</span>' : ''}</td>
<td><code style="font-size:0.75rem; word-break:break-all;">${escapeHtml(s.url)}</code></td>
<td>${evts}</td>
<td>${deliveryInfo}</td>
<td>${active}</td>
<td style="white-space:nowrap;">
<button class="btn-icon-action" onclick="testWebhook(${s.id})" title="Test Ping">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>
</button>
<button class="btn-icon-action" onclick="toggleWebhook(${s.id}, ${s.is_active})" title="${s.is_active ? 'Disabilita' : 'Abilita'}">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
</button>
<button class="btn-icon-action btn-danger-hover" onclick="deleteWebhook(${s.id}, '${escapeHtml(s.name)}')" title="Elimina">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9z" clip-rule="evenodd"/></svg>
</button>
</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
function showCreateWebhookModal() {
const evtCheckboxes = Object.entries(availableEvents).map(([key, label]) =>
`<label style="display:flex; align-items:center; gap:8px; font-size:0.8rem; cursor:pointer; padding:4px 0;">
<input type="checkbox" name="wh-event" value="${key}" style="accent-color:var(--primary);">
<span><code style="font-size:0.7rem;">${key}</code> — ${escapeHtml(label)}</span>
</label>`
).join('');
showModal('Crea Webhook', `
<div class="form-group">
<label class="form-label">Nome *</label>
<input type="text" id="wh-name" class="form-control" placeholder="Es. SIEM Integration, 231 Agile Notify...">
</div>
<div class="form-group">
<label class="form-label">URL Endpoint * (https://...)</label>
<input type="url" id="wh-url" class="form-control" placeholder="https://your-siem.example.com/webhooks/nis2">
</div>
<div class="form-group">
<label class="form-label">Eventi da ascoltare *</label>
<div style="max-height:200px; overflow-y:auto; padding:8px; border:1px solid var(--gray-200); border-radius:var(--border-radius); margin-top:8px;">${evtCheckboxes}</div>
</div>
`, '<button class="btn btn-primary" onclick="createWebhook()">Crea Webhook</button>');
}
async function createWebhook() {
const name = document.getElementById('wh-name').value.trim();
const url = document.getElementById('wh-url').value.trim();
const events = [...document.querySelectorAll('input[name="wh-event"]:checked')].map(cb => cb.value);
if (!name || !url) { showNotification('Nome e URL obbligatori.', 'error'); return; }
if (!events.length) { showNotification('Seleziona almeno un evento.', 'error'); return; }
try {
const result = await api.request('POST', '/webhooks/subscriptions', { name, url, events });
if (result.success) {
closeModal();
showModal('Webhook Creato', `
<div style="padding:16px; background:var(--warning-bg); border:1px solid var(--warning); border-radius:var(--border-radius); margin-bottom:16px;">
<strong style="color:#a16207;">⚠ Salva il secret per verificare la firma HMAC. Non sarà più visibile.</strong>
</div>
<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:8px;">Usa questo secret per verificare l'header <code>X-NIS2-Signature</code>:</p>
<div style="padding:12px; background:var(--gray-900); border-radius:var(--border-radius); font-family:monospace; font-size:0.875rem; color:#34d399; word-break:break-all; user-select:all;">
${escapeHtml(result.data.secret)}
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:8px;">Firma: <code>sha256=HMAC_SHA256(body, secret)</code></p>
`, '<button class="btn btn-primary" onclick="closeModal(); loadWebhooks();">Ho salvato il secret</button>');
} else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function testWebhook(id) {
try {
const result = await api.request('POST', `/webhooks/subscriptions/${id}/test`);
if (result.success) { showNotification('Ping di test inviato. Controlla i delivery log.', 'success'); loadDeliveries(); }
else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function toggleWebhook(id, currentActive) {
try {
const result = await api.request('PUT', `/webhooks/subscriptions/${id}`, { is_active: currentActive ? 0 : 1 });
if (result.success) { loadWebhooks(); showNotification(currentActive ? 'Webhook disabilitato.' : 'Webhook abilitato.', 'success'); }
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function deleteWebhook(id, name) {
if (!confirm(`Eliminare il webhook "${name}"?`)) return;
try {
const result = await api.request('DELETE', `/webhooks/subscriptions/${id}`);
if (result.success) { showNotification('Webhook eliminato.', 'success'); loadWebhooks(); }
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
async function loadDeliveries(subscriptionId) {
const container = document.getElementById('deliveries-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const url = subscriptionId ? `/webhooks/deliveries?subscription_id=${subscriptionId}` : '/webhooks/deliveries';
const result = await api.request('GET', url);
if (result.success) {
const deliveries = result.data.deliveries || [];
if (!deliveries.length) {
container.innerHTML = '<div class="empty-state" style="padding:32px;"><h4>Nessun delivery registrato</h4></div>';
return;
}
let html = `<div class="table-container"><table><thead><tr><th>Evento</th><th>Webhook</th><th>Stato</th><th>HTTP</th><th>Tentativo</th><th>Data</th></tr></thead><tbody>`;
deliveries.forEach(d => {
const statusClass = d.status === 'delivered' ? 'success' : d.status === 'retrying' ? 'warning' : 'danger';
html += `<tr>
<td><code style="font-size:0.75rem;">${escapeHtml(d.event_type)}</code></td>
<td style="font-size:0.8rem;">${escapeHtml(d.subscription_name || '-')}</td>
<td><span class="badge badge-${statusClass}">${escapeHtml(d.status)}</span></td>
<td>${d.http_status ? `<code>${d.http_status}</code>` : '-'}</td>
<td>${d.attempt}/3</td>
<td style="font-size:0.8rem;">${formatDateTime(d.created_at)}</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
}
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore caricamento delivery</h4></div>'; }
}
// ── Audit Log ────────────────────────────────────────────
async function loadAuditLog() {
const container = document.getElementById('audit-log-container');
container.innerHTML = '<div class="spinner" style="margin:40px auto;"></div>';
try {
const result = await api.getAuditLogs({ limit: 20 });
if (result.success && result.data && result.data.length > 0) {
let html = `
<div class="table-container">
<table>
<thead>
<tr>
<th>Azione</th>
<th>Tipo</th>
<th>Data</th>
</tr>
</thead>
<tbody>`;
const actionLabels = {
user_login: 'Accesso effettuato',
user_logout: 'Disconnessione',
user_registered: 'Registrazione utente',
profile_updated: 'Profilo aggiornato',
password_changed: 'Password modificata',
organization_created: 'Organizzazione creata',
organization_updated: 'Organizzazione aggiornata',
member_invited: 'Membro invitato',
member_removed: 'Membro rimosso',
assessment_created: 'Assessment creato',
assessment_completed: 'Assessment completato',
risk_created: 'Rischio creato',
risk_updated: 'Rischio aggiornato',
incident_created: 'Incidente creato',
policy_created: 'Policy creata',
policy_approved: 'Policy approvata'
};
result.data.forEach(log => {
html += `
<tr>
<td>${escapeHtml(actionLabels[log.action] || log.action || '-')}</td>
<td><span class="badge badge-neutral">${escapeHtml(log.entity_type || '-')}</span></td>
<td>${formatDateTime(log.created_at)}</td>
</tr>`;
});
html += '</tbody></table></div>';
container.innerHTML = html;
} else {
container.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
<h4>Nessuna attivita' registrata</h4>
<p>Le attivita' del tuo account appariranno qui.</p>
</div>`;
}
} catch (e) {
container.innerHTML = `
<div class="empty-state">
<h4>Impossibile caricare il registro</h4>
<p>Riprova piu' tardi.</p>
</div>`;
}
}
</script>
</body>
</html>