Add internal staff IAM: named users, roles, and management permissions.
Some checks are pending
offline / test (push) Waiting to run

verae-staff-iam (:3028) is the people directory — owner, billing-admin,
cs, sales, accounting, operator, viewer — with scrypt passwords, sessions,
and an audit log. Admin console login uses it when STAFF_IAM_URL is set
and hides tabs the account cannot use. CS/sales/accounting/staff/fleet
check permissions such as cs.credit and fleet.operate. Shared staff key
remains only as a fallback when IAM is unset.
This commit is contained in:
George Lambert 2026-09-11 18:36:12 -04:00
parent 2740d51446
commit d299d245e8
41 changed files with 1337 additions and 52 deletions

View file

@ -92,11 +92,41 @@ function currentPeriod() {
return new Date().toISOString().slice(0, 7);
}
const TAB_PERM = {
endpoints: 'admin.pricing',
tiers: 'admin.tiers',
customers: 'admin.customers',
statement: 'admin.statement',
invoices: 'admin.invoices',
reports: 'admin.reports',
system: 'admin.system',
users: 'iam.users.read',
};
function can(perm) {
const p = state.me?.permissions || ['*'];
return p.includes('*') || p.includes(perm);
}
function applyNav() {
document.querySelectorAll('aside nav button').forEach((btn) => {
const need = TAB_PERM[btn.dataset.tab];
btn.style.display = !need || can(need) ? '' : 'none';
});
}
async function load() {
state.me = await api('/me').catch(() => ({ permissions: ['*'] }));
applyNav();
if (!can('admin.pricing') && !can('admin.tiers') && !can('admin.customers')) {
if (state.me?.iamUrl) {
document.getElementById('endpoints').innerHTML =
`<h2>Staff IAM</h2><p class="lede">This console is for billing admins. Manage people at <a href="${state.me.iamUrl}" style="color:var(--accent)">${state.me.iamUrl}</a>.</p>`;
}
}
if (!can('admin.pricing') && !can('admin.tiers')) return;
state.pricing = await api('/pricing');
state.customers = (await api('/customers')).customers;
state.invoices = (await api('/invoices')).invoices;
state.users = (await api('/users')).users;
state.customers = can('admin.customers') ? (await api('/customers')).customers : [];
state.invoices = can('admin.invoices') ? (await api('/invoices')).invoices : [];
state.users = can('iam.users.read') || !state.me?.iam ? (await api('/users')).users : [];
renderEndpoints();
renderTiers();
renderCustomers();
@ -703,6 +733,16 @@ async function loadSystem() {
/* ---------------- admin users ---------------- */
function renderUsers() {
if (state.me?.iam) {
document.getElementById('users').innerHTML = `
<h2>Staff users</h2>
<p class="lede">Named internal accounts and roles live in Staff IAM not the billing-console seed table.</p>
<div class="card">
<p>Open <a class="btn" style="display:inline-block;text-decoration:none" href="${state.me.iamUrl || 'http://127.0.0.1:3028/'}">${state.me.iamUrl || 'http://127.0.0.1:3028/'}</a></p>
<p class="hint">Roles: owner, iam-admin, billing-admin, cs, sales, accounting, operator, viewer.</p>
</div>`;
return;
}
const rows = state.users
.slice()
.sort((a, b) => a.username.localeCompare(b.username))