1371 lines
76 KiB
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>
|