nis2-agile/public/reset-password.html
DevEnv nis2-agile e4f9e9179e [FEAT] Allineamento NIS2 ↔ TRPG (Fasi 1-5): SSO + Sessions + Reset + Impersonate + Branding
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>
2026-05-29 13:18:35 +02:00

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>