Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
152 lines
6.8 KiB
JavaScript
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>
|
|
`;
|
|
}
|