commit fcc111d0644bfb791873bbe86e53c3afe9fed997 Author: Anton Afanasyeu Date: Tue Jun 23 12:29:36 2026 +0200 initial diff --git a/README.md b/README.md new file mode 100644 index 0000000..f48fc46 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ac-be-access + +Thin BE UI. Remote: `git://f0xx.org/ac/ac-be-access` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3a8fdac --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "androidcast/be-access", + "description": "RBAC admin UI", + "require": { + "php": ">=8.1", + "androidcast/platform-php": "dev-next", + "androidcast/platform-web": "dev-next" + }, + "repositories": [ + { + "type": "vcs", + "url": "git://f0xx.org/ac/ac-platform-php" + }, + { + "type": "vcs", + "url": "git://f0xx.org/ac/ac-platform-web" + } + ] +} diff --git a/public/assets/js/rbac_admin.js b/public/assets/js/rbac_admin.js new file mode 100644 index 0000000..d85653a --- /dev/null +++ b/public/assets/js/rbac_admin.js @@ -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() + ? '' + : '' + 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(); + }); +})(); diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..e8271bc --- /dev/null +++ b/public/index.php @@ -0,0 +1,2 @@ + + * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8 + * Contributors: + * - Anton Afanasyeu (2 commits, 86 lines) + * - Cursor Agent (project assistant) + * Digest: SHA256 21e56cd12df6a94df41a1c0d67be530bd01454865ee1f032cef5f8d2e497509a + */ +?> + + + + + + <?= h($pageTitle ?? 'Console') ?> — <?= h(cfg('app_name')) ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + +
+ +
+ + + + + + + +
+
+

Tickets

+
+ + + + +
+
+

Loading…

+
+ + + + + + + + + + + + +
IssueOpenedApp / OSRatingTags
+
+ +
+ +
+
+

Issues

+
+ + + + + + +
+
+
+ Filter by tag +
+
+ +
+ +
+ + +
+ +
+

Loading…

+
+ + + + +
+
+ +
+ +
+
+

AndroidCast analytics

+
+ + + +
+
+ + +

Loading…

+ +
+

Your activity

+
+

Sessions / day

+

Unique devices / day

+

Avg duration (s) / day

+

Send sessions / day

+

Receive sessions / day

+

Success ratio

+

Outbound kbps / day

+

Inbound kbps / day

+

Issues / day

+
+
+ + + + + +
+

Live cast

+
+

Live casts / day

+

Avg cast length (s) / day

+

Cast platforms

+

Join platforms

+

Video codecs

+

Audio codecs

+

Bandwidth modes

+

Top casters

+
+
+ + +
+ +
+
+

Live cast sessions

+
+ + Education demo +
+
+ +

Loading…

+

Read-only session tree for all signed-in users. Sorted by last heartbeat (most recent first).

+
+ + + + + + + + + + + + + + +
StatusUserPlatformVideoDurationLast heartbeatJoin opensLink
+
+
+ +
+
+

Remote access

+
+ +
+
+ +

Loading…

+ +
+

Devices

+

+ Devices appear after they poll remote access (dev settings → WireGuard or RSSH). Sort is by last seen — top rows are actively polling. + Whitelist rows marked Needs whitelist (opted in, not yet allowed). + Stale rows have not polled in 7+ days. + Poll source IP is the address BE sees on the HTTP request (often a router or proxy on 10.7.x.x) — not the device WAN or LAN. +

+
+ + + + +
+
+
+ + + +
+
+ +
+

Active / pending sessions

+
+ + + + + + + + + + + + +
SessionDeviceStatusTunnelEndpointActions
+
+
+ +
+

Inactive / closed sessions

+
+ + + + + + + + + + + +
SessionDeviceStatusClosedReason
+
+
+ +
+

Audit log

+
+ + + + + + + + + + +
TimeDeviceActionReason
+
+
+
+ + + +
+
+

Access control

+
+

Loading…

+

+
+

Users & company roles

+
+ + + + + + + + + + + + +
UserGlobal roleCompanyCompany rolePrivilege setAuth lockouts
+
+
+

+ API: /api/rbac.php · + Root edits global roles; company owner/admin edits memberships and remote-access privilege sets. +

+
+ +
+
+ + + + +
+
+

New ticket

+ +
+
+ + + +
+
+

+
+ + +
+
+
+
+ + + + diff --git a/views/partials/console_home_landing.php b/views/partials/console_home_landing.php new file mode 100644 index 0000000..b8d660a --- /dev/null +++ b/views/partials/console_home_landing.php @@ -0,0 +1,58 @@ + +
+
+

Console

+
+ +
+
+

Issue triage, tickets, and session metrics — pick a workspace below.

+ + + +

All services

+ +
diff --git a/views/partials/console_quick_links.php b/views/partials/console_quick_links.php new file mode 100644 index 0000000..9106fd4 --- /dev/null +++ b/views/partials/console_quick_links.php @@ -0,0 +1,37 @@ + + * @var bool $skip_home_link omit "Console home" (use on home view) + */ +$bp = Auth::basePath(); +$proj = '/app/androidcast_project'; +$navClass = 'console-quick-links graphs-quick-links' . (isset($extra_class) ? ' ' . $extra_class : ''); +$items = []; +if (empty($skip_home_link)) { + $items[] = ['Console home', $bp . '/?view=home', 'nav-icon--home']; +} +$items[] = ['Issues', $bp . '/?view=reports', 'nav-icon--reports']; +$items[] = ['Tickets', $bp . '/?view=tickets', 'nav-icon--tickets']; +if (Rbac::can('remote_access_view')) { + $items[] = ['Remote access', $bp . '/?view=remote_access', 'nav-icon--remote']; +} +if (Rbac::can('short_links_view')) { + $items[] = ['Short links', $bp . '/?view=short_links', 'nav-icon--link']; +} +if (Rbac::canManageRbac()) { + $items[] = ['Access control', $bp . '/?view=rbac', 'nav-icon--tickets']; +} +$items[] = ['Graphs', $proj . '/graphs/', 'nav-icon--graphs']; +$items[] = ['Builder', $proj . '/build/', 'nav-icon--builder']; +$items[] = ['Hub', $proj . '/', 'nav-icon--home']; +?> + diff --git a/views/partials/cookie_consent.php b/views/partials/cookie_consent.php new file mode 100644 index 0000000..0a0e5ed --- /dev/null +++ b/views/partials/cookie_consent.php @@ -0,0 +1,15 @@ + +