mirror of
git://f0xx.org/ac/ac-be-access
synced 2026-07-29 02:19:14 +03:00
197 lines
7.2 KiB
JavaScript
197 lines
7.2 KiB
JavaScript
(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()
|
|
? '<select class="rbac-global-role" data-user-id="' + u.id + '">' +
|
|
globalRoles.map((r) => '<option value="' + esc(r) + '"' + (u.global_role === r ? ' selected' : '') + '>' + esc(r) + '</option>').join('') +
|
|
'</select>'
|
|
: '<code>' + esc(u.global_role) + '</code>';
|
|
|
|
const roleSelect =
|
|
'<select class="rbac-company-role" data-user-id="' + u.id + '" data-company-id="' + m.company_id + '">' +
|
|
companyRoles
|
|
.map((r) => '<option value="' + esc(r) + '"' + (m.role === r ? ' selected' : '') + '>' + esc(r) + '</option>')
|
|
.join('') +
|
|
'</select>';
|
|
|
|
const setSelect =
|
|
'<select class="rbac-privilege-set" data-user-id="' + u.id + '" data-company-id="' + m.company_id + '">' +
|
|
sets
|
|
.map((s) => {
|
|
const key = s.key != null ? s.key : '';
|
|
const label = s.label || key || '(none)';
|
|
const sel = (m.privilege_set || '') === key ? ' selected' : '';
|
|
return '<option value="' + esc(key) + '"' + sel + '>' + esc(label) + '</option>';
|
|
})
|
|
.join('') +
|
|
(m.privilege_set === 'custom' ? '<option value="" disabled>custom grants (edit via API)</option>' : '') +
|
|
'</select>';
|
|
|
|
let authCell = '';
|
|
if (!seenAuth.has(u.id)) {
|
|
seenAuth.add(u.id);
|
|
const fails = Number(u.auth_failures || 0);
|
|
if (canEditGlobal() && fails > 0) {
|
|
authCell =
|
|
'<span class="muted">' + fails + ' fail</span> ' +
|
|
'<button type="button" class="btn btn--secondary rbac-clear-auth" data-user-id="' + u.id + '">Clear lockouts</button>';
|
|
} else if (fails > 0) {
|
|
authCell = '<span class="muted">' + fails + ' recent fail</span>';
|
|
} else {
|
|
authCell = '<span class="muted">—</span>';
|
|
}
|
|
}
|
|
|
|
tr.innerHTML =
|
|
'<td>' + esc(u.username) + '</td>' +
|
|
'<td>' + globalSelect + '</td>' +
|
|
'<td><code>' + esc(m.slug) + '</code> ' + esc(m.name) + '</td>' +
|
|
'<td>' + roleSelect + '</td>' +
|
|
'<td>' + setSelect + '</td>' +
|
|
'<td>' + authCell + '</td>';
|
|
tbody.appendChild(tr);
|
|
});
|
|
if (!(u.memberships || []).length && canEditGlobal()) {
|
|
const tr = document.createElement('tr');
|
|
const fails = Number(u.auth_failures || 0);
|
|
let authCell = fails > 0
|
|
? '<button type="button" class="btn btn--secondary rbac-clear-auth" data-user-id="' + u.id + '">Clear lockouts (' + fails + ')</button>'
|
|
: '<span class="muted">—</span>';
|
|
tr.innerHTML =
|
|
'<td>' + esc(u.username) + '</td>' +
|
|
'<td><code>' + esc(u.global_role) + '</code></td>' +
|
|
'<td colspan="3" class="muted">No company membership</td>' +
|
|
'<td>' + authCell + '</td>';
|
|
tbody.appendChild(tr);
|
|
}
|
|
});
|
|
|
|
if (!tbody.children.length) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="muted">No users in scope.</td></tr>';
|
|
}
|
|
|
|
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();
|
|
});
|
|
})();
|