mirror of
git://f0xx.org/ac/ac-be-access
synced 2026-07-29 02:59:28 +03:00
initial
This commit is contained in:
196
public/assets/js/rbac_admin.js
Normal file
196
public/assets/js/rbac_admin.js
Normal file
@@ -0,0 +1,196 @@
|
||||
(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();
|
||||
});
|
||||
})();
|
||||
2
public/index.php
Normal file
2
public/index.php
Normal file
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
require __DIR__ . '/../views/layout.php';
|
||||
Reference in New Issue
Block a user