/** * Access Control Hub — Gestão de utilizadores * Spec 040 · DS-FE-004 · Matriz → aba Controle de acesso */ (function () { 'use strict'; const ROLE_META = [ { value: 'super_admin', label: 'Super Admin', group: 'Ops', code: 'SU' }, { value: 'ops_lead', label: 'Chefe Ops', group: 'Ops', code: 'CO' }, { value: 'technician', label: 'Suporte', group: 'Ops', code: 'TEC' }, { value: 'noc', label: 'NOC', group: 'Ops', code: 'NOC' }, { value: 'sales_admin', label: 'Sales Admin', group: 'Comercial', code: 'SAD' }, { value: 'sales_support', label: 'Sales Support', group: 'Comercial', code: 'SSU' }, { value: 'finance', label: 'Financeiro', group: 'Negócio', code: 'FIN' }, { value: 'marketing', label: 'Marketing', group: 'Negócio', code: 'MKT' }, { value: 'seo', label: 'SEO', group: 'Negócio', code: 'SEO' }, { value: 'developer', label: 'Developer', group: 'Plataforma', code: 'DEV' }, { value: 'devops', label: 'DevOps', group: 'Plataforma', code: 'DVO' }, { value: 'security_analyst', label: 'Segurança / SOC', group: 'Plataforma', code: 'SOC' }, { value: 'content_editor', label: 'Conteúdo / CMS', group: 'Plataforma', code: 'CMS' }, { value: 'agentic_operator', label: 'Operador Agentes IA', group: 'Plataforma', code: 'AIO' }, { value: 'partner', label: 'Partner', group: 'Externo', code: 'PTR' }, ]; let host = null; let opts = {}; let subTab = 'users'; let users = []; let stats = { total: 0, active: 0, frozen: 0, super_admin: 0 }; let selectedUser = null; let detailTab = 'details'; let filterQ = ''; let filterRole = 'all'; let filterGroup = 'all'; let filterStatus = 'all'; let confirmModal = null; let createMenuOpen = false; const favorites = new Set(); const SVG = 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round"'; const ICONS = { users: ``, userCheck: ``, snowflake: ``, shield: ``, plus: ``, chevronDown: ``, filter: ``, search: ``, more: ``, star: ``, close: ``, edit: ``, freeze: ``, copy: ``, lock: ``, shieldCheck: ``, usersGroup: ``, trash: ``, checkCircle: ``, infoShield: ``, }; function fmtDateTime(iso) { if (!iso) return '—'; try { const d = new Date(iso); const date = d.toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit', year: 'numeric' }); const time = d.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit', hour12: false }); return `${date} ${time}`; } catch (_) { return iso; } } function kpiCard(iconKey, tone, value, label, sub) { return `
${ICONS[iconKey]}
${esc(value)} ${esc(label)} ${esc(sub)}
`; } function quickAction(iconKey, label, dataAttr) { return ` `; } function esc(s) { return String(s ?? '').replace(/&/g, '&').replace(//g, '>'); } function roleMeta(id) { return ROLE_META.find((r) => r.value === id) || { label: id, group: '—', code: '?' }; } function fmtDate(iso) { if (!iso) return '—'; try { return new Date(iso).toLocaleDateString('pt-PT'); } catch (_) { return iso; } } function initials(name) { const p = String(name || '?').trim().split(/\s+/); return ((p[0]?.[0] || '') + (p[1]?.[0] || '')).toUpperCase() || '?'; } async function loadData() { const [uRes, sRes] = await Promise.all([ fetchWithTimeout('/api/v1/auth/users', { headers: authHeaders() }), fetchWithTimeout('/api/v1/governance/users/stats', { headers: authHeaders() }), ]); if (!uRes.ok) throw new Error(String(uRes.status)); if (!sRes.ok) throw new Error(String(sRes.status)); const uData = await uRes.json(); stats = await sRes.json(); users = uData.users || []; } function filteredUsers() { const q = filterQ.trim().toLowerCase(); const roleLock = opts.selectedRole; return users.filter((u) => { if (selectedUser && u.username === selectedUser) return true; const rm = roleMeta(u.role); if (roleLock && u.role !== roleLock && !(roleLock === 'super_admin' && u.username === 'root')) return false; if (filterRole !== 'all' && u.role !== filterRole) return false; if (filterGroup !== 'all' && rm.group !== filterGroup) return false; if (filterStatus === 'active' && !u.active) return false; if (filterStatus === 'frozen' && u.active) return false; if (!q) return true; const hay = [u.username, u.email, u.display_name, rm.label, rm.group].join(' ').toLowerCase(); return hay.includes(q); }); } function createSplitButton() { return `
`; } function subnavHtml() { return ` `; } function kpiSectionHtml() { return `
${subnavHtml()}
${createSplitButton()}
${kpiCard('users', 'blue', stats.total, 'Total de usuários', `${stats.active} activos • ${stats.frozen} congelados`)} ${kpiCard('userCheck', 'green', stats.active, 'Activos', 'Usuários com acesso')} ${kpiCard('snowflake', 'orange', stats.frozen, 'Congelados', 'Acesso bloqueado')} ${kpiCard('shield', 'purple', stats.super_admin, 'Super Admin', 'Permissão máxima')}
`; } function filterField(label, inner) { return ` `; } function userTableRow(u) { const rm = roleMeta(u.role); const sel = selectedUser === u.username ? ' ach-table-row--selected' : ''; const tfaHtml = u.totp_enabled ? `${ICONS.checkCircle}` : ``; const statusHtml = u.active ? `Activo` : `${ICONS.snowflake} Congelado`; return `
${esc(initials(u.display_name || u.username))} ${esc(u.display_name || u.username)}
${esc(rm.code)} ${esc(rm.label)} ${esc(rm.group)} ${statusHtml} ${tfaHtml} ${fmtDateTime(u.last_login_at)} `; } function usersTable(list) { return `
${list.length ? list.map(userTableRow).join('') : ''}
Utilizador Perfil Grupo Estado 2FA Último acesso Acções
Nenhum utilizador encontrado.
Mostrando ${list.length} de ${users.length}
`; } function usersPanel() { const list = filteredUsers(); const groups = [...new Set(ROLE_META.map((r) => r.group))]; const roleLock = opts.selectedRole; const roleLockMeta = roleMeta(roleLock); const roleLockBanner = roleLock ? `
${ICONS.infoShield} A mostrar apenas utilizadores da função ${esc(roleLockMeta.label)} (seleccionada na Matriz). Para ver todos, escolha outra função na barra lateral ou use o filtro Perfil abaixo.
` : ''; return `
${kpiSectionHtml()} ${roleLockBanner}
${filterField('Perfil', ` `)} ${filterField('Grupo', ` `)} ${filterField('Estado', ` `)}
${usersTable(list)}
`; } function detailPanel() { const u = users.find((x) => x.username === selectedUser); if (!u) { return `

Seleccione um utilizador

`; } const rm = roleMeta(u.role); const isRoot = u.username === 'root'; const isFav = favorites.has(u.username); const tfaOn = u.totp_enabled; let tabBody = ''; if (detailTab === 'details') { tabBody = `
Perfil ${esc(rm.code)} ${esc(rm.label)}
Grupo ${esc(rm.group)}
Telefone ${esc(u.phone || '—')}
Criado em ${fmtDateTime(u.created_at)}
Último acesso ${fmtDateTime(u.last_login_at)}
2FA ${tfaOn ? `${ICONS.checkCircle} Activado` : 'Não activo'}
`; } else if (detailTab === 'permissions') { tabBody = `

Permissões herdadas do perfil ${esc(rm.label)}. Edição avançada na aba «Quem faz o quê».

Abrir mapa de permissões

`; } else { tabBody = `

A carregar…

`; loadAudit(u.username); } return `
${esc(initials(u.display_name || u.username))}
${esc(u.display_name || u.username)} ${u.active ? 'Activo' : 'Congelado'}

${esc(u.email || u.username)}

${tabBody}

Ações rápidas

${quickAction('edit', 'Editar utilizador', 'data-ach-edit')} ${quickAction('freeze', u.active ? 'Congelar conta' : 'Activar conta', 'data-ach-freeze')} ${quickAction('copy', 'Copiar utilizador', 'data-ach-clone')} ${quickAction('lock', 'Redefinir senha', 'data-ach-reset-pwd')} ${quickAction('shieldCheck', 'Resetar 2FA', 'data-ach-reset-2fa')} ${quickAction('usersGroup', 'Gerenciar grupos', 'data-ach-groups')}
${!isRoot ? ` ` : ''}
${ICONS.infoShield} As alterações realizadas aqui são registadas em audit log e não podem ser desfeitas.
`; } async function loadAudit(username) { const el = host?.querySelector('[data-ach-audit]'); if (!el) return; try { const r = await fetchWithTimeout( `/api/v1/governance/audit?target_type=user&target_id=${encodeURIComponent(username)}&limit=20`, { headers: authHeaders() }, ); const data = await r.json(); const events = data.events || []; el.innerHTML = events.length ? events.map((e) => `
${esc(e.summary)}
${fmtDate(e.created_at)}
`).join('') : '

Sem actividades registadas.

'; } catch (e) { el.innerHTML = `

Erro: ${esc(e.message)}

`; } } function capabilitiesPanel() { if (!window.DeskAccessControlPanel?.paint) { return '

Módulo de capacidades indisponível.

'; } return '
'; } function render() { if (!host) return; const body = subTab === 'users' ? usersPanel() : capabilitiesPanel(); host.innerHTML = `
${subTab === 'capabilities' ? `
${subnavHtml()}
` : ''} ${body}
${confirmModal || ''}`; bindEvents(); if (subTab === 'capabilities') { const capHost = host.querySelector('#ach-capabilities-host'); window.DeskAccessControlPanel?.paintCapabilitiesOnly?.(capHost, opts) || window.DeskAccessControlPanel?.paint?.(capHost, { ...opts, usersOnly: false }); } } function showConfirm(title, message, onConfirm, danger = false) { confirmModal = `

${esc(title)}

${esc(message)}

Acção registada no audit log.

`; render(); host.querySelector('[data-ach-confirm-cancel]')?.addEventListener('click', () => { confirmModal = null; render(); }); host.querySelector('[data-ach-confirm-ok]')?.addEventListener('click', async () => { confirmModal = null; await onConfirm(); render(); }); } async function apiJson(path, init = {}) { const r = await fetchWithTimeout(path, { ...init, headers: authHeaders({ 'Content-Type': 'application/json', ...(init.headers || {}) }), }); if (!r.ok) throw new Error((await r.text()).slice(0, 200)); return r.json(); } function bindEvents() { host.querySelectorAll('[data-ach-sub]').forEach((btn) => { btn.addEventListener('click', () => { subTab = btn.dataset.achSub; render(); }); }); function openCreateWizard(defaultRole) { createMenuOpen = false; window.DeskUserWizard?.open?.({ defaultRole: defaultRole || opts.selectedRole || 'technician', onDone: async (wizardResult) => { await loadData(); const email = wizardResult?.user?.username || wizardResult?.user?.email; if (email) { selectedUser = email; filterRole = 'all'; filterGroup = 'all'; filterStatus = 'all'; filterQ = email.split('@')[0] || ''; } render(); }, }); } host.querySelector('[data-ach-create]')?.addEventListener('click', () => openCreateWizard()); host.querySelector('[data-ach-create-user]')?.addEventListener('click', () => openCreateWizard()); host.querySelector('[data-ach-create-support]')?.addEventListener('click', () => openCreateWizard('technician')); host.querySelector('[data-ach-create-menu]')?.addEventListener('click', (e) => { e.stopPropagation(); createMenuOpen = !createMenuOpen; render(); }); host.querySelector('[data-ach-filters]')?.addEventListener('click', () => { host.querySelector('[data-ach-search]')?.focus(); }); if (createMenuOpen) { const closeMenu = (e) => { if (!e.target.closest('[data-ach-create-dropdown]') && !e.target.closest('[data-ach-create-menu]')) { createMenuOpen = false; document.removeEventListener('click', closeMenu); render(); } }; setTimeout(() => document.addEventListener('click', closeMenu), 0); } host.querySelector('[data-ach-search]')?.addEventListener('input', (e) => { filterQ = e.target.value; clearTimeout(host._achTimer); host._achTimer = setTimeout(render, 200); }); ['role', 'group', 'status'].forEach((k) => { host.querySelector(`[data-ach-filter-${k}]`)?.addEventListener('change', (e) => { if (k === 'role') filterRole = e.target.value; if (k === 'group') filterGroup = e.target.value; if (k === 'status') filterStatus = e.target.value; render(); }); }); host.querySelectorAll('[data-ach-user]').forEach((el) => { el.addEventListener('click', (e) => { if (e.target.closest('.ach-row-menu')) e.preventDefault(); selectedUser = el.dataset.achUser; detailTab = 'details'; render(); }); }); host.querySelectorAll('[data-ach-dtab]').forEach((btn) => { btn.addEventListener('click', () => { detailTab = btn.dataset.achDtab; render(); }); }); host.querySelector('[data-ach-close-panel]')?.addEventListener('click', () => { selectedUser = null; render(); }); const u = users.find((x) => x.username === selectedUser); if (!u) return; host.querySelector('[data-ach-favorite]')?.addEventListener('click', () => { if (favorites.has(u.username)) favorites.delete(u.username); else favorites.add(u.username); render(); }); host.querySelector('[data-ach-groups]')?.addEventListener('click', () => { window.alert('Gerenciar grupos — disponível na próxima fase.'); }); host.querySelector('[data-ach-goto-matrix]')?.addEventListener('click', (e) => { e.preventDefault(); document.querySelector('[data-am-tab="quem-faz-o-que"]')?.click(); }); host.querySelector('[data-ach-freeze]')?.addEventListener('click', () => { const label = u.active ? 'Congelar' : 'Activar'; showConfirm(`${label} conta`, `${label} ${u.username}?`, async () => { await apiJson(`/api/v1/governance/users/${encodeURIComponent(u.username)}/freeze`, { method: 'POST' }); await loadData(); }); }); host.querySelector('[data-ach-reset-pwd]')?.addEventListener('click', () => { showConfirm('Redefinir senha', `Gerar nova senha para ${u.username}?`, async () => { const res = await apiJson(`/api/v1/governance/users/${encodeURIComponent(u.username)}/reset-password`, { method: 'POST' }); window.alert(`Nova senha: ${res.generated_password}`); await loadData(); }); }); host.querySelector('[data-ach-reset-2fa]')?.addEventListener('click', () => { showConfirm('Resetar 2FA', `Resetar autenticador de ${u.username}?`, async () => { await apiJson(`/api/v1/auth/users/${encodeURIComponent(u.username)}/reset-2fa`, { method: 'POST' }); await loadData(); }); }); host.querySelector('[data-ach-deactivate]')?.addEventListener('click', () => { showConfirm('Eliminar utilizador', `Eliminar permanentemente ${u.username}? Esta acção não pode ser desfeita.`, async () => { await apiJson(`/api/v1/auth/users/${encodeURIComponent(u.username)}`, { method: 'DELETE' }); selectedUser = null; await loadData(); }, true); }); host.querySelector('[data-ach-clone]')?.addEventListener('click', () => { const email = window.prompt('E-mail do novo utilizador (cópia):'); if (!email) return; apiJson(`/api/v1/auth/users/${encodeURIComponent(u.username)}/clone`, { method: 'POST', body: JSON.stringify({ email: email.trim().toLowerCase() }), }).then(async (res) => { if (res.generated_password) window.alert(`Copiado. Senha: ${res.generated_password}`); await loadData(); render(); }).catch((e) => window.alert(e.message)); }); host.querySelector('[data-ach-edit]')?.addEventListener('click', () => { if (window.DeskUserManagement?.openEdit) { window.DeskUserManagement.openEdit(u.username, async () => { await loadData(); render(); }); } }); } async function paint(container, options = {}) { host = container; opts = options; if (typeof canManageUsers === 'function' && !canManageUsers()) { host.innerHTML = '

Sem permissão — apenas Super Admin gere acessos.

'; return; } host.innerHTML = '

Carregando Controle de acesso…

'; try { await loadData(); if (!selectedUser && users.length) selectedUser = users[0].username; render(); } catch (e) { host.innerHTML = `

Erro: ${esc(e.message)}

`; } } function selectUser(username) { selectedUser = username; subTab = 'users'; filterRole = 'all'; filterQ = String(username || '').split('@')[0] || ''; if (host) { loadData().then(render).catch(() => render()); } } window.DeskAccessControlHub = { paint, selectUser }; })();