diff --git a/public/admin/users.html b/public/admin/users.html index 7d02f53..ee9a153 100644 --- a/public/admin/users.html +++ b/public/admin/users.html @@ -231,6 +231,7 @@ Ultimo Login Attivo Data Creazione + Azioni `; @@ -270,6 +271,14 @@ ${formatDate(user.created_at)} + + ${role !== 'super_admin' && isActive + ? `` + : '-'} + `; }); @@ -333,6 +342,27 @@ loadUsers(currentPage); } } + + // ── Impersonate (Fase 4 / G11 — UI) ── + async function impersonateUser(userId, email) { + if (!confirm('Entrare come ' + email + ' per 1 ora?\n\nSarai loggato come quell\'utente. Tornerai alla tua sessione facendo logout.')) return; + try { + const res = await api.post('/auth/impersonate', { user_id: userId }); + if (res.success && res.data && res.data.access_token) { + // Salva sessione originale per "Exit impersonate" + sessionStorage.setItem('nis2_impersonate_origin_token', api.token); + sessionStorage.setItem('nis2_impersonate_origin_refresh', api.refreshToken || ''); + sessionStorage.setItem('nis2_impersonate_target_email', email); + // Sostituisce token con quello impersonate (no refresh per default) + api.setTokens(res.data.access_token, ''); + window.location.href = '/dashboard.html'; + } else { + showNotification(res.message || 'Impersonate fallito.', 'error'); + } + } catch (e) { + showNotification('Errore di connessione.', 'error'); + } + } diff --git a/public/js/common.js b/public/js/common.js index ef489c7..5b8cb83 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -291,6 +291,9 @@ function loadSidebar() { // Firm branding white-label (Fase 5 / G16) — non bloccante _loadFirmBranding(); + + // Impersonate banner (Fase 4 / G11 — UI) + _renderImpersonateBanner(); } const _roleLabels = { @@ -431,6 +434,36 @@ async function _switchOrg(orgId) { window.location.reload(); } +// ── Impersonate banner persistente (Fase 4 / G11 — UI) ────────────────── +function _renderImpersonateBanner() { + const originToken = sessionStorage.getItem('nis2_impersonate_origin_token'); + const targetEmail = sessionStorage.getItem('nis2_impersonate_target_email'); + if (!originToken) return; + + if (document.getElementById('impersonate-banner')) return; + const bar = document.createElement('div'); + bar.id = 'impersonate-banner'; + bar.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;background:#F59E0B;color:#1F2937;padding:8px 16px;text-align:center;font-size:.85rem;font-weight:600;box-shadow:0 2px 8px rgba(0,0,0,.2);'; + bar.innerHTML = + '' + + 'Modalità Impersonate — stai usando l\'account di ' + (targetEmail || '?') + '  ' + + ''; + document.body.insertBefore(bar, document.body.firstChild); + // Sposta il contenuto sotto + document.body.style.paddingTop = (bar.offsetHeight + (parseInt(document.body.style.paddingTop) || 0)) + 'px'; +} + +function _exitImpersonate() { + const originToken = sessionStorage.getItem('nis2_impersonate_origin_token'); + const originRefresh = sessionStorage.getItem('nis2_impersonate_origin_refresh'); + if (!originToken) return; + api.setTokens(originToken, originRefresh || ''); + sessionStorage.removeItem('nis2_impersonate_origin_token'); + sessionStorage.removeItem('nis2_impersonate_origin_refresh'); + sessionStorage.removeItem('nis2_impersonate_target_email'); + window.location.href = '/admin/users.html'; +} + // ── Firm branding white-label (Fase 5 / G16) ──────────────────────────── async function _loadFirmBranding() { try { diff --git a/public/settings.html b/public/settings.html index b2a8371..dab82a6 100644 --- a/public/settings.html +++ b/public/settings.html @@ -210,10 +210,12 @@
+ +
@@ -420,6 +422,71 @@ + +
+
+
+

Preferenze utente

+

Personalizzazione interfaccia e notifiche.

+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ + +
+
+
+
+
+
@@ -546,6 +613,44 @@
+ +
+
+
+

Branding white-label

+

Personalizza colori, logo e nome prodotto per il tuo studio. Visibile a tutti i tuoi clienti loggati.

+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
@@ -570,24 +675,104 @@ // Carica dati iniziali loadOrgData(); loadProfileData(); + // Mostra tab Branding solo a super_admin / consulente + (async () => { + try { + const me = await api.getMe(); + if (me && me.success && me.data && (me.data.role === 'super_admin' || me.data.role === 'consultant')) { + const btn = document.getElementById('tab-btn-branding'); + if (btn) btn.style.display = ''; + } + } catch (e) { /* silenzioso */ } + })(); // ── 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' }; + const tabMap = { org: 0, profile: 1, preferences: 2, members: 3, security: 4, apikeys: 5, webhooks: 6, branding: 7 }; + const panelMap = { org: 'tab-org', profile: 'tab-profile', preferences: 'tab-preferences', members: 'tab-members', security: 'tab-security', apikeys: 'tab-apikeys', webhooks: 'tab-webhooks', branding: 'tab-branding' }; document.querySelectorAll('.settings-tab')[tabMap[tab]].classList.add('active'); document.getElementById(panelMap[tab]).classList.add('active'); + if (tab === 'preferences') loadPreferences(); if (tab === 'members') loadMembers(); if (tab === 'security') { loadSessions(); loadAuditLog(); } if (tab === 'apikeys') loadApiKeys(); if (tab === 'webhooks') { loadWebhooks(); loadDeliveries(); } + if (tab === 'branding') loadBranding(); } + // ── Preferenze utente (Fase 4 / G12 — UI) ───────────────── + async function loadPreferences() { + try { + const res = await api.get('/auth/preferences'); + if (!res.success) return; + const p = res.data; + document.getElementById('pref-lang').value = p.preferred_language || 'it'; + document.getElementById('pref-theme').value = p.theme || 'auto'; + document.getElementById('pref-tz').value = p.timezone || 'Europe/Rome'; + document.getElementById('pref-notif-email').checked = !!p.notif_email; + document.getElementById('pref-notif-inapp').checked = !!p.notif_inapp; + } catch (e) { /* silenzioso */ } + } + + // ── Branding white-label (Fase 5 / G16 — UI) ───────────── + async function loadBranding() { + try { + const res = await api.get('/branding/current'); + if (!res.success) return; + const b = res.data; + document.getElementById('brand-name').value = b.custom_brand_name || ''; + document.getElementById('brand-logo').value = b.logo_url || ''; + document.getElementById('brand-pri').value = b.primary_color || '#1e40af'; + document.getElementById('brand-sec').value = b.secondary_color || '#06b6d4'; + } catch (e) { /* silenzioso */ } + } + + document.getElementById('branding-form').addEventListener('submit', async function(e) { + e.preventDefault(); + const btn = document.getElementById('brand-save-btn'); + const msg = document.getElementById('brand-saved-msg'); + btn.disabled = true; btn.textContent = 'Salvataggio...'; + try { + await api.put('/branding', { + custom_brand_name: document.getElementById('brand-name').value.trim() || null, + logo_url: document.getElementById('brand-logo').value.trim() || null, + primary_color: document.getElementById('brand-pri').value, + secondary_color: document.getElementById('brand-sec').value, + }); + msg.style.display = 'inline'; + } catch (e) { + alert('Errore: ' + (e.message || 'impossibile salvare')); + } finally { + btn.disabled = false; btn.textContent = 'Salva branding'; + } + }); + + document.getElementById('preferences-form').addEventListener('submit', async function(e) { + e.preventDefault(); + const btn = document.getElementById('pref-save-btn'); + const msg = document.getElementById('pref-saved-msg'); + btn.disabled = true; btn.textContent = 'Salvataggio...'; + try { + await api.put('/auth/preferences', { + preferred_language: document.getElementById('pref-lang').value, + theme: document.getElementById('pref-theme').value, + timezone: document.getElementById('pref-tz').value, + notif_email: document.getElementById('pref-notif-email').checked, + notif_inapp: document.getElementById('pref-notif-inapp').checked, + }); + msg.style.display = 'inline'; setTimeout(function(){ msg.style.display = 'none'; }, 3000); + } catch (e) { + alert('Errore: ' + (e.message || 'impossibile salvare')); + } finally { + btn.disabled = false; btn.textContent = 'Salva preferenze'; + } + }); + // ── Sessioni Multi-Device (Fase 2 / G07) ───────────────── async function loadSessions() { const container = document.getElementById('sessions-container'); diff --git a/public/version.json b/public/version.json index 286a996..71a3094 100644 --- a/public/version.json +++ b/public/version.json @@ -1 +1 @@ -{"version":"1.5.0","build":"20260529e","date":"2026-05-29T14:00:00+02:00","changelog":"Fase 5 Polishing: BrandingController + migration 019_firm_branding (white-label per consulenti), auth-gate.js per documenti riservati. Skip G15 demo (coperto dai simulator esistenti) e G18 refactor (rinviato). Progetto allineamento NIS2↔TRPG COMPLETATO (Fasi 1-5). Vedi docs/GAP_TRPG_NIS2_ALIGNMENT.md"} +{"version":"1.6.0","build":"20260529f","date":"2026-05-29T14:30:00+02:00","changelog":"UI Fasi 4+5: pulsante Impersonate in admin/users.html con banner persistente arancione, tab Preferenze in settings (lingua/tema/timezone/notifiche), tab Branding white-label per super_admin/consulenti (colori + logo + brand name). Vedi docs/GAP_TRPG_NIS2_ALIGNMENT.md"}