ligbox-ops-platform/projects/ops-desk/frontend/assets/access-control-panel.js
Ligbox Spec Hub b03bb2c37c feat(desk): UI/API Spec 039-041 + deploy atómico e smoke GREEN
Commita governance, user-wizard, operational-feed e catálogo RBAC;
adiciona deploy-desk-full.sh, smoke-desk.sh e regra anti-deploy parcial;
documenta credencial VM112 @betinplace.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 13:32:15 +00:00

169 lines
6.7 KiB
JavaScript

/**
* Controle de acesso — delega ao Access Control Hub
* Spec 040 · DS-FE-005
*/
(function () {
'use strict';
const ROLE_CODES = {
super_admin: 'SU',
ops_lead: 'CO',
technician: 'TEC',
noc: 'NOC',
sales_admin: 'SAD',
sales_support: 'SSU',
finance: 'FIN',
marketing: 'MKT',
seo: 'SEO',
developer: 'DEV',
devops: 'DVO',
security_analyst: 'SOC',
content_editor: 'CMS',
agentic_operator: 'AIO',
partner: 'PTR',
root: 'RO',
};
const ACCESS_ACTIONS = [
{ id: 'create_user', actionId: 'desk.auth.user.create', label: 'Criar usuário', hint: 'Provisionar conta directa' },
{ id: 'edit_user', actionId: 'desk.auth.user.edit', label: 'Editar usuário', hint: 'Nome, perfil e metadados' },
{ id: 'freeze_user', actionId: 'desk.auth.user.freeze', label: 'Congelar conta', hint: 'Bloquear login — SSU nunca' },
{ id: 'approve_registration', actionId: 'desk.auth.user.approve_registration', label: 'Aprovar pedidos de cadastro', hint: 'SU + CO' },
{ id: 'manage_passwords', actionId: 'desk.auth.user.password.reset', label: 'Gerenciar senhas', hint: 'Reset admin' },
{ id: 'reset_2fa', actionId: 'desk.auth.user.2fa.reset', label: 'Resetar 2FA', hint: 'Recuperação autenticador' },
{ id: 'manage_modules', actionId: 'desk.auth.modules.toggle', label: 'Módulos Desk ON/OFF', hint: 'Feature flags' },
{ id: 'purge_domain', actionId: 'vm112.domain.purge', label: 'Purge domínio', hint: 'Irreversível' },
{ id: 'openpanel_delete', actionId: 'vm123_openpanel.site.delete', label: 'Deletar instância OpenPanel', hint: 'SU + CO' },
{ id: 'validate_billing', actionId: 'desk.billing.state.validate', label: 'Validar billing', hint: 'billing_state' },
{ id: 'agent_approve', actionId: 'desk.agent.runbook.approve', label: 'Aprovar remediação A7', hint: 'Agentes' },
];
let levelsCache = null;
let levelsRole = null;
let catalogEditable = false;
function esc(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
async function loadLevelsForRole(roleId) {
if (levelsCache && levelsRole === roleId) return levelsCache;
const r = await fetchWithTimeout('/api/v1/rbac/actions', { headers: authHeaders() });
if (!r.ok) throw new Error(String(r.status));
const data = await r.json();
catalogEditable = !!data.editable;
const map = {};
(data.actions || []).forEach((a) => {
map[a.id] = {
effective: a.effective?.[roleId] || 'none',
default: a.defaults?.[roleId] || 'none',
overridden: a.overridden_roles?.includes(roleId),
};
});
levelsCache = map;
levelsRole = roleId;
return map;
}
function actionOn(level) {
return level === 'full' || level === 'approve' || level === 'api' || level === 'system';
}
function renderPermToggles(selectedRole, roleMeta, levels, editable) {
const code = ROLE_CODES[selectedRole] || selectedRole?.slice(0, 3).toUpperCase() || '—';
const rows = ACCESS_ACTIONS.map((a) => {
const lv = levels[a.actionId]?.effective || 'none';
const on = actionOn(lv);
const partial = on && lv !== 'full';
const ro = !editable ? ' acs-toggle--readonly' : '';
const dis = editable ? '' : ' disabled';
return `
<div class="acs-perm-row" title="${esc(a.hint)} · ${esc(lv)}">
<span class="acs-perm-label">${esc(a.label)}${partial ? ` <em class="acs-partial">(${esc(lv)})</em>` : ''}</span>
<label class="acs-toggle${ro}" aria-label="${esc(a.label)}" data-acs-action="${esc(a.actionId)}">
<input type="checkbox" ${on ? 'checked' : ''}${dis} data-acs-toggle="${esc(a.actionId)}"/>
<span class="acs-toggle-slider"></span>
</label>
</div>`;
}).join('');
const editHint = editable
? 'Toggles ligados ao catálogo Spec 039 — alterações gravam na Matriz «Quem faz o quê».'
: 'Modo consulta — use a aba «Quem faz o quê» com edição activa para alterar.';
return `
<div class="acs-perms-card acs-card lb-card" style="padding:16px">
<header class="acs-perms-head">
<span class="acs-role-code" title="Código da função">${esc(code)}</span>
<div>
<h3>Capacidades da função</h3>
<p class="acs-perms-desc">Função <strong>${esc(roleMeta?.label || selectedRole)}</strong>. ${editHint}</p>
</div>
</header>
<div class="acs-perm-list">${rows}</div>
<p class="ticket-meta"><a href="#" class="acs-goto-exec" data-goto-exec>Abrir mapa completo «Quem faz o quê»</a></p>
</div>`;
}
async function patchToggle(actionId, roleId, checked) {
const level = checked ? 'full' : 'none';
const r = await fetchWithTimeout('/api/v1/rbac/actions/override', {
method: 'PATCH',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ action_id: actionId, role_id: roleId, level, reset: !checked }),
});
if (!r.ok) throw new Error(await r.text());
levelsCache = null;
return r.json();
}
async function paintCapabilitiesOnly(host, opts = {}) {
if (!host) return;
const selectedRole = opts.selectedRole || 'super_admin';
const roleMeta = opts.roleMeta || {};
const editable = !!opts.editable;
host.innerHTML = '<p class="loading">Carregando capacidades…</p>';
let levels = {};
try {
levels = await loadLevelsForRole(selectedRole);
} catch (e) {
host.innerHTML = `<p class="loading">Catálogo indisponível: ${esc(e.message)}</p>`;
return;
}
const canEdit = editable && catalogEditable;
host.innerHTML = renderPermToggles(selectedRole, roleMeta, levels, canEdit);
host.querySelector('[data-goto-exec]')?.addEventListener('click', (e) => {
e.preventDefault();
document.querySelector('[data-am-tab="quem-faz-o-que"]')?.click();
});
host.querySelectorAll('[data-acs-toggle]').forEach((inp) => {
inp.addEventListener('change', async () => {
if (!canEdit) return;
const actionId = inp.dataset.acsToggle;
try {
await patchToggle(actionId, selectedRole, inp.checked);
} catch (err) {
inp.checked = !inp.checked;
window.alert(`Erro ao gravar: ${err.message}`);
}
});
});
}
async function paint(host, opts = {}) {
if (!host) return;
if (window.DeskAccessControlHub?.paint) {
return window.DeskAccessControlHub.paint(host, opts);
}
return paintCapabilitiesOnly(host, opts);
}
window.DeskAccessControlPanel = {
ROLE_CODES,
ACCESS_ACTIONS,
paint,
paintCapabilitiesOnly,
invalidateCache: () => { levelsCache = null; levelsRole = null; },
resetUserMgmt: () => window.DeskUserManagement?.reset?.(),
};
})();