[FEAT] UI Fasi 4+5: Impersonate banner + Preferenze + Branding white-label (v1.6.0)
Completamento UI per gli endpoint backend già attivi (commit e4f9e91):
- admin/users.html: colonna Azioni con pulsante "Impersonate" per utenti non-super_admin
attivi → salva token originale in sessionStorage, sostituisce con quello impersonate,
redirige a dashboard
- js/common.js: banner persistente arancione "Modalità Impersonate" in tutte le
pagine quando sessionStorage ha impersonate origin → pulsante "Esci impersonate"
ripristina token originale e torna ad admin/users
- settings.html: nuovo tab "Preferenze" (lingua/tema/timezone/notifiche email+in-app)
con form salva via PUT /auth/preferences
- settings.html: nuovo tab "Branding" (solo super_admin / consulente) con
brand_name/logo_url/primary_color/secondary_color, PUT /branding
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4f9e9179e
commit
a7bd37a797
@ -231,6 +231,7 @@
|
||||
<th>Ultimo Login</th>
|
||||
<th>Attivo</th>
|
||||
<th>Data Creazione</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
@ -270,6 +271,14 @@
|
||||
</label>
|
||||
</td>
|
||||
<td>${formatDate(user.created_at)}</td>
|
||||
<td>
|
||||
${role !== 'super_admin' && isActive
|
||||
? `<button class="btn btn-secondary btn-sm" onclick="impersonateUser(${user.id}, '${escapeHtml(user.email)}')" title="Entra come questo utente per 1 ora">
|
||||
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align:-2px"><path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/></svg>
|
||||
Impersonate
|
||||
</button>`
|
||||
: '<span class="text-muted">-</span>'}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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 =
|
||||
'<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" style="vertical-align:-3px;margin-right:6px"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.94 6.94a.75.75 0 011.06 0L10 6.94l.5.5L10 7.94l-.5-.5L9.5 8h-1V7l.5-.5z"/></svg>'
|
||||
+ 'Modalità Impersonate — stai usando l\'account di <strong>' + (targetEmail || '?') + '</strong> '
|
||||
+ '<button onclick="_exitImpersonate()" style="background:#1F2937;color:#fff;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:.8rem;font-weight:600;margin-left:8px;">Esci impersonate</button>';
|
||||
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 {
|
||||
|
||||
@ -210,10 +210,12 @@
|
||||
<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('preferences')">Preferenze</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>
|
||||
<button class="settings-tab" id="tab-btn-branding" onclick="switchTab('branding')" style="display:none;">Branding</button>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════ TAB: Organizzazione ══════════════ -->
|
||||
@ -420,6 +422,71 @@
|
||||
</div>
|
||||
|
||||
<!-- ══════════════ TAB: Membri ══════════════ -->
|
||||
<!-- ══════════════ TAB: Preferenze (Fase 4 / G12) ══════════════ -->
|
||||
<div class="tab-panel" id="tab-preferences">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Preferenze utente</h3>
|
||||
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizzazione interfaccia e notifiche.</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="preferences-form" style="max-width:560px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pref-lang">Lingua</label>
|
||||
<select id="pref-lang" class="form-input">
|
||||
<option value="it">Italiano</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="de">Deutsch</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pref-theme">Tema</label>
|
||||
<select id="pref-theme" class="form-input">
|
||||
<option value="auto">Auto (segue sistema)</option>
|
||||
<option value="light">Chiaro</option>
|
||||
<option value="dark">Scuro</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pref-tz">Timezone</label>
|
||||
<select id="pref-tz" class="form-input">
|
||||
<option value="Europe/Rome">Europe/Rome (CET/CEST)</option>
|
||||
<option value="Europe/London">Europe/London (GMT/BST)</option>
|
||||
<option value="Europe/Paris">Europe/Paris (CET/CEST)</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin (CET/CEST)</option>
|
||||
<option value="Europe/Madrid">Europe/Madrid (CET/CEST)</option>
|
||||
<option value="America/New_York">America/New_York (EST/EDT)</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles (PST/PDT)</option>
|
||||
<option value="UTC">UTC</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
|
||||
<input type="checkbox" id="pref-notif-email">
|
||||
<span>Ricevi notifiche via email (incidenti, deadline, training)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display:flex; gap:10px; align-items:center; font-size:.9rem;">
|
||||
<input type="checkbox" id="pref-notif-inapp">
|
||||
<span>Mostra notifiche in-app (badge sidebar)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:24px;">
|
||||
<button type="submit" class="btn btn-primary" id="pref-save-btn">Salva preferenze</button>
|
||||
<span id="pref-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-members">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@ -546,6 +613,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════ TAB: Branding (Fase 5 / G16) ══════════════ -->
|
||||
<div class="tab-panel" id="tab-branding">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Branding white-label</h3>
|
||||
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizza colori, logo e nome prodotto per il tuo studio. Visibile a tutti i tuoi clienti loggati.</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="branding-form" style="max-width:560px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="brand-name">Nome prodotto custom (opzionale)</label>
|
||||
<input type="text" id="brand-name" class="form-input" maxlength="120" placeholder="es. Studio Rossi NIS2 Suite">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="brand-logo">URL Logo (opzionale)</label>
|
||||
<input type="url" id="brand-logo" class="form-input" maxlength="512" placeholder="https://...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="brand-pri">Colore primario</label>
|
||||
<input type="color" id="brand-pri" class="form-input" style="height:42px; padding:4px;" value="#1e40af">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="brand-sec">Colore secondario</label>
|
||||
<input type="color" id="brand-sec" class="form-input" style="height:42px; padding:4px;" value="#06b6d4">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:24px;">
|
||||
<button type="submit" class="btn btn-primary" id="brand-save-btn">Salva branding</button>
|
||||
<span id="brand-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato — ricarica per vedere</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@ -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');
|
||||
|
||||
@ -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"}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user