This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/surfaces/admin/users.js
Jeffrey Smith b10f5bee05
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m22s
CI/CD / test-sqlite (pull_request) Successful in 2m35s
CI/CD / build-and-deploy (pull_request) Successful in 1m18s
drop users.role column: full RBAC through group membership
The role column was a pre-RBAC artifact. All authorization now flows
through explicit group membership and permission grants:

- Everyone group: all users added on creation (no implicit membership)
- Admins group: grants surface.admin.access + all platform permissions
- JWT claims, login response, profile: role field removed
- OIDC: isIdPAdmin() maps IdP claims → Admins group (no role writes)
- Admin UI: role dropdown removed, admin managed through groups
- Middleware cache simplified to isActive only

28 files changed, -79 lines net. Zero magic roles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:23:12 +00:00

152 lines
6.8 KiB
JavaScript

/**
* Admin > People > Users
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
export default function UsersSection() {
const [users, setUsers] = useState([]);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const load = useCallback(async () => {
try {
const resp = await sw.api.admin.users.list();
setUsers(resp.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, []);
const filtered = users.filter(u => {
if (!search) return true;
const q = search.toLowerCase();
return u.username?.toLowerCase().includes(q) || u.email?.toLowerCase().includes(q);
});
async function createUser(e) {
e.preventDefault();
const form = e.target;
const data = {
username: form.username.value.trim(),
email: form.email.value.trim(),
password: form.password.value,
is_admin: form.is_admin.checked,
};
if (!data.username || !data.password) return sw.toast('Username and password required', 'error');
try {
await sw.api.admin.users.create(data);
sw.toast('User created', 'success');
setShowCreate(false);
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function toggleActive(id, active) {
try {
await sw.api.admin.users.toggleActive(id, active);
sw.toast(`User ${active ? 'activated' : 'deactivated'}`, 'success');
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function toggleAdmin(id, isAdmin) {
try {
await sw.api.admin.users.updateRole(id, isAdmin);
sw.toast(isAdmin ? 'Admin granted' : 'Admin revoked', 'success');
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function resetPassword(id, username) {
const pw = await sw.prompt(`New password for "${username}":`, '');
if (!pw) return;
try {
await sw.api.admin.users.resetPassword(id, pw);
sw.toast('Password reset', 'success');
} catch (e) { sw.toast(e.message, 'error'); }
}
async function approveUser(id) {
try {
await sw.api.admin.users.toggleActive(id, true);
sw.toast('User approved', 'success');
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function deleteUser(id, username) {
const ok = await sw.confirm(`Delete user "${username}"? This is permanent and destroys their vault.`, true);
if (!ok) return;
try {
await sw.api.admin.users.del(id);
sw.toast('User deleted', 'success');
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
function statusBadge(u) {
if (u.status === 'pending') return html`<span class="badge badge-pending">pending</span>`;
if (!u.is_active) return html`<span class="badge badge-inactive">inactive</span>`;
return html`<span class="badge badge-active">active</span>`;
}
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
return html`
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<div class="admin-search">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text-3);flex-shrink:0;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input placeholder="Search users\u2026" value=${search} onInput=${e => setSearch(e.target.value)} autocomplete="off" />
</div>
<div style="flex:1"></div>
<button class="btn-md btn-primary" onClick=${() => setShowCreate(true)}>+ Add</button>
</div>
${showCreate && html`
<form class="admin-inline-form" style="margin-bottom:16px;" onSubmit=${createUser}>
<div class="form-row">
<div class="form-group"><label>Username</label><input name="username" placeholder="username" autocomplete="off" /></div>
<div class="form-group"><label>Email</label><input name="email" placeholder="email@example.com" autocomplete="off" /></div>
<div class="form-group"><label>Password</label><input name="password" type="password" placeholder="min 8 chars" autocomplete="new-password" /></div>
<div class="form-group" style="align-self:flex-end;">
<label class="toggle-label"><input type="checkbox" name="is_admin" /><span class="toggle-track"></span><span>Admin</span></label>
</div>
</div>
<div class="form-row" style="margin-top:8px;gap:8px;">
<button type="submit" class="btn-small btn-primary">Create</button>
<button type="button" class="btn-small" onClick=${() => setShowCreate(false)}>Cancel</button>
</div>
</form>
`}
<div class="admin-list">
${filtered.length === 0 && html`<div class="empty-hint">No users found</div>`}
${filtered.map(u => html`
<div class="admin-surface-row" key=${u.id}>
<div style="flex:1;min-width:0;">
<strong>${u.username}</strong>
<span class="text-muted" style="margin-left:8px;font-size:12px;">${u.email || ''}</span>
${' '}${statusBadge(u)}
</div>
<div class="admin-actions-cell">
${u.status === 'pending' && html`
<button class="btn-small btn-primary" onClick=${() => approveUser(u.id)}>Approve</button>
`}
${u.is_active
? html`<button class="btn-small" onClick=${() => toggleActive(u.id, false)}>Disable</button>`
: html`<button class="btn-small" onClick=${() => toggleActive(u.id, true)}>Enable</button>`
}
<button class="btn-small" onClick=${() => resetPassword(u.id, u.username)}>Reset PW</button>
<button class="btn-small btn-danger" onClick=${() => deleteUser(u.id, u.username)}>Delete</button>
</div>
</div>
`)}
</div>
</div>
`;
}