Implementazione completa del progetto allineamento alla suite Evix (TRPG/lg231),
basato sul doc canonico docs/GAP_TRPG_NIS2_ALIGNMENT.md (5 fasi, 18 gap).
Version 1.0.0 → 1.5.0
Fase 1 — SSO Federation (v1.1.0)
- Migration 015_sso_columns: users.sso_identity_id + password_version
- application/services/SsoHelper.php (client SSO dual-mode, cURL nativo, zero deps)
- AuthController::login() + changePassword() conditional SSO (SSO_MODE=local default)
Fase 2 — Multi-device Sessions (v1.2.0)
- Migration 016_active_sessions: tabella + refresh_tokens.session_jti
- BaseController::requireAuth() verifica jti + last_activity throttle + parseDeviceLabel
- login() genera jti, logout/changePassword revoca selettiva
- GET/DELETE /auth/sessions[/{id}]
- UI settings.html tab Sicurezza con lista device + revoca
Fase 3 — Password Reset + Tenant Switcher (v1.3.0)
- Migration 017_password_reset_tokens (TTL 30min, single-use)
- POST /auth/forgot-password (risposta opaca) + reset-password
- Pagine forgot-password.html + reset-password.html (con strength bar)
- EmailService::sendPasswordReset
- POST /auth/switchContext con rotazione JWT + organization_id claim
- Dropdown tenant in sidebar esposto a tutti gli utenti con ≥2 org
Fase 4 — Impersonate + Preferences + Versioning UI (v1.4.0)
- POST /auth/impersonate (super_admin o consulente stesso firm, TTL 1h, audit)
- Migration 018_user_preferences: users.theme/timezone/notif_email/notif_inapp
- GET/PUT /auth/preferences
- Sidebar footer mostra versione + changelog modal su click
Fase 5 — Branding white-label + Auth-gate (v1.5.0)
- Migration 019_firm_branding (logo/colori/brand_name per consulting firm)
- BrandingController GET /branding/current (auth opzionale) + PUT
- common.js auto-applica CSS variables al boot
- public/js/auth-gate.js (gate password client-side per docs riservati, da TRPG)
Skip motivati:
- G15 demo login: simulator esistenti coprono
- G18 refactor controllers: rinviato (~5gg, valore tecnico solo)
Cron sync SSO: AgileHub Ticket #220 aperto a team AGILEHUB per estendere
sso-password-sync.sh al DB nis2_agile_db. Prerequisito per switch SSO_MODE=dual.
Backup files: tutti i file modificati hanno .bak.pre-{fase}-{ts} sia in DEV
sia in /var/www/nis2-agile/.backups/ su Hetzner (rollback ready).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
8.0 KiB
HTML
172 lines
8.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Imposta nuova password - NIS2 Agile</title>
|
|
<link rel="stylesheet" href="css/style.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
<style>
|
|
.pw-wrap { position: relative; }
|
|
.pw-wrap .form-input { padding-right: 42px; }
|
|
.pw-toggle {
|
|
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
|
|
background: none; border: none; cursor: pointer;
|
|
color: #9CA3AF; font-size: 15px; padding: 0;
|
|
}
|
|
.strength-bar { display:flex; gap:3px; margin-top:6px; }
|
|
.strength-bar span { flex:1; height:4px; border-radius:2px; background:#E0E4E8; transition:background .2s; }
|
|
.strength-text { font-size:.78rem; color:#6B7280; margin-top:6px; min-height:16px; }
|
|
.auth-success { background:#ECFDF5; color:#065F46; border:1px solid #A7F3D0; padding:12px 16px; border-radius:6px; font-size:.9rem; margin-bottom:16px; display:none; }
|
|
.auth-success.visible { display:block; }
|
|
.pw-rules { font-size:.78rem; color:#6B7280; margin-top:8px; line-height:1.6; }
|
|
.back-link { display:block; text-align:center; margin-top:14px; font-size:.85rem; color:#6B7280; text-decoration:none; }
|
|
.back-link:hover { color: var(--color-primary, #2563eb); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="auth-page">
|
|
<div class="auth-card">
|
|
<div class="auth-header">
|
|
<div class="auth-logo">
|
|
<div class="auth-logo-icon">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
|
|
</svg>
|
|
</div>
|
|
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
|
|
</div>
|
|
<p class="auth-subtitle">Imposta una nuova password</p>
|
|
</div>
|
|
|
|
<div class="auth-body">
|
|
<div class="auth-error" id="err"></div>
|
|
<div class="auth-success" id="ok"></div>
|
|
|
|
<form id="reset-form" novalidate>
|
|
<div class="form-group">
|
|
<label class="form-label" for="pw">Nuova password</label>
|
|
<div class="pw-wrap">
|
|
<input type="password" id="pw" class="form-input"
|
|
placeholder="Almeno 8 caratteri" autocomplete="new-password" required>
|
|
<button type="button" class="pw-toggle" onclick="togglePw('pw',this)" aria-label="Mostra/nascondi password">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div class="strength-bar"><span></span><span></span><span></span><span></span></div>
|
|
<div class="strength-text" id="strength-text"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="pw2">Conferma password</label>
|
|
<div class="pw-wrap">
|
|
<input type="password" id="pw2" class="form-input"
|
|
placeholder="Ripeti la password" autocomplete="new-password" required>
|
|
<button type="button" class="pw-toggle" onclick="togglePw('pw2',this)" aria-label="Mostra/nascondi password">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="pw-rules">Minimo 8 caratteri, con almeno una maiuscola, una minuscola e un numero.</p>
|
|
|
|
<button type="submit" class="btn btn-primary btn-lg w-full" id="submit-btn" style="margin-top:12px;">
|
|
Imposta password
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="auth-footer">
|
|
<a href="login.html" class="back-link"><i class="fas fa-arrow-left"></i> Torna al login</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const token = urlParams.get('token') || '';
|
|
const err = document.getElementById('err');
|
|
const ok = document.getElementById('ok');
|
|
const form = document.getElementById('reset-form');
|
|
const btn = document.getElementById('submit-btn');
|
|
const pwInput = document.getElementById('pw');
|
|
const pwBars = document.querySelectorAll('.strength-bar span');
|
|
const pwText = document.getElementById('strength-text');
|
|
|
|
if (!token) {
|
|
err.textContent = 'Token mancante. Richiedi un nuovo link di reset.';
|
|
err.classList.add('visible');
|
|
form.style.display = 'none';
|
|
}
|
|
|
|
function togglePw(id, button) {
|
|
const i = document.getElementById(id);
|
|
if (i.type === 'password') { i.type = 'text'; button.innerHTML = '<i class="fas fa-eye-slash"></i>'; }
|
|
else { i.type = 'password'; button.innerHTML = '<i class="fas fa-eye"></i>'; }
|
|
}
|
|
|
|
function computeStrength(pw) {
|
|
let s = 0;
|
|
if (pw.length >= 8) s++;
|
|
if (pw.length >= 12) s++;
|
|
if (/[A-Z]/.test(pw) && /[a-z]/.test(pw)) s++;
|
|
if (/\d/.test(pw) && /[^A-Za-z0-9]/.test(pw)) s++;
|
|
return s;
|
|
}
|
|
|
|
pwInput.addEventListener('input', function() {
|
|
const s = computeStrength(pwInput.value);
|
|
const colors = ['#E0E4E8', '#EF4444', '#F59E0B', '#3B82F6', '#10B981'];
|
|
const labels = ['', 'Debole', 'Media', 'Buona', 'Ottima'];
|
|
pwBars.forEach(function(bar, i) { bar.style.background = (i < s) ? colors[s] : '#E0E4E8'; });
|
|
pwText.textContent = pwInput.value.length > 0 ? 'Sicurezza: ' + labels[s] : '';
|
|
});
|
|
|
|
form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
err.classList.remove('visible');
|
|
ok.classList.remove('visible');
|
|
|
|
const pw = document.getElementById('pw').value;
|
|
const pw2 = document.getElementById('pw2').value;
|
|
if (pw !== pw2) {
|
|
err.textContent = 'Le due password non coincidono.';
|
|
err.classList.add('visible');
|
|
return;
|
|
}
|
|
if (pw.length < 8) {
|
|
err.textContent = 'La password deve essere di almeno 8 caratteri.';
|
|
err.classList.add('visible');
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Salvataggio...';
|
|
try {
|
|
const res = await fetch('/api/auth/reset-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token: token, new_password: pw })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
ok.textContent = data.message + ' Sarai reindirizzato al login.';
|
|
ok.classList.add('visible');
|
|
form.style.display = 'none';
|
|
setTimeout(function() { window.location.href = 'login.html'; }, 2500);
|
|
} else {
|
|
err.textContent = data.message || 'Errore. Token non valido o scaduto.';
|
|
err.classList.add('visible');
|
|
}
|
|
} catch (e) {
|
|
err.textContent = 'Errore di connessione al server.';
|
|
err.classList.add('visible');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Imposta password';
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|