[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:
DevEnv nis2-agile
2026-05-29 13:28:57 +02:00
co-authored by Claude Opus 4.7
parent e4f9e9179e
commit a7bd37a797
4 changed files with 251 additions and 3 deletions
+33
View File
@@ -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> &nbsp;'
+ '<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 {