(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();
});
})();