(function () { function basePath() { return document.body.getAttribute('data-base-path') || ''; } function canEditGlobal() { return document.body.getAttribute('data-can-rbac-root') === '1'; } function setStatus(msg, isError) { const el = document.getElementById('rbac-status'); if (!el) return; el.textContent = msg; el.classList.toggle('error', !!isError); } async function fetchJson(url, opts) { const res = await fetch(url, Object.assign({ credentials: 'same-origin' }, opts || {})); const data = await res.json().catch(() => ({})); if (!res.ok || data.ok === false) { throw new Error(data.error || 'HTTP ' + res.status); } return data; } function esc(s) { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; } function renderPanel(data) { const tbody = document.getElementById('rbac-users-tbody'); if (!tbody) return; tbody.innerHTML = ''; const companies = data.companies || []; const companyRoles = data.company_roles || []; const globalRoles = data.global_roles || []; const sets = [{ key: '', label: '(role defaults only)' }].concat(data.privilege_sets || []); const seenAuth = new Set(); (data.users || []).forEach((u) => { (u.memberships || []).forEach((m) => { const tr = document.createElement('tr'); const globalSelect = canEditGlobal() ? '' : '' + esc(u.global_role) + ''; const roleSelect = ''; const setSelect = ''; let authCell = ''; if (!seenAuth.has(u.id)) { seenAuth.add(u.id); const fails = Number(u.auth_failures || 0); if (canEditGlobal() && fails > 0) { authCell = '' + fails + ' fail ' + ''; } else if (fails > 0) { authCell = '' + fails + ' recent fail'; } else { authCell = ''; } } tr.innerHTML = '' + esc(u.username) + '' + '' + globalSelect + '' + '' + esc(m.slug) + ' ' + esc(m.name) + '' + '' + roleSelect + '' + '' + setSelect + '' + '' + authCell + ''; tbody.appendChild(tr); }); if (!(u.memberships || []).length && canEditGlobal()) { const tr = document.createElement('tr'); const fails = Number(u.auth_failures || 0); let authCell = fails > 0 ? '' : ''; tr.innerHTML = '' + esc(u.username) + '' + '' + esc(u.global_role) + '' + 'No company membership' + '' + authCell + ''; tbody.appendChild(tr); } }); if (!tbody.children.length) { tbody.innerHTML = 'No users in scope.'; } const hint = document.getElementById('rbac-scope-hint'); if (hint) { hint.textContent = 'Slug admin = company owner/admin (graphs company scope). Root may change global roles. Privilege sets add remote-access grants to members.'; if (companies.length) { hint.textContent += ' Companies: ' + companies.map((c) => c.slug).join(', '); } } } async function postAction(action, body) { await fetchJson(basePath() + '/api/rbac.php?action=' + encodeURIComponent(action), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); } function bindChanges() { document.addEventListener('click', async (ev) => { const btn = ev.target && ev.target.closest ? ev.target.closest('.rbac-clear-auth') : null; if (!btn || !canEditGlobal()) return; const userId = Number(btn.getAttribute('data-user-id')); if (!userId) return; try { await postAction('clear_auth_lockouts', { user_id: userId }); setStatus('Auth lockouts cleared.'); load(); } catch (e) { setStatus(String(e.message || e), true); } }); document.addEventListener('change', async (ev) => { const t = ev.target; if (!t || !t.classList) return; try { if (t.classList.contains('rbac-global-role') && canEditGlobal()) { await postAction('set_global_role', { user_id: Number(t.getAttribute('data-user-id')), role: t.value, }); setStatus('Global role updated.'); return; } if (t.classList.contains('rbac-company-role')) { await postAction('set_company_role', { user_id: Number(t.getAttribute('data-user-id')), company_id: Number(t.getAttribute('data-company-id')), role: t.value, }); setStatus('Company role updated.'); return; } if (t.classList.contains('rbac-privilege-set')) { await postAction('apply_privilege_set', { user_id: Number(t.getAttribute('data-user-id')), company_id: Number(t.getAttribute('data-company-id')), privilege_set: t.value, }); setStatus('Privilege set applied.'); } } catch (e) { setStatus(String(e.message || e), true); } }); } async function load() { setStatus('Loading…'); try { const data = await fetchJson(basePath() + '/api/rbac.php?action=panel'); renderPanel(data); setStatus('Ready — changes save on selection.'); } catch (e) { setStatus(String(e.message || e), true); } } document.addEventListener('DOMContentLoaded', function () { bindChanges(); load(); }); })();