158 lines
4.3 KiB
JavaScript
158 lines
4.3 KiB
JavaScript
// public/assets/admin.js
|
|
/* English comments: admin user management + audit */
|
|
|
|
function fmtDate(s) {
|
|
if (!s) return '';
|
|
const d = new Date(s.replace(' ', 'T') + 'Z');
|
|
if (Number.isNaN(d.getTime())) return s;
|
|
return d.toLocaleString();
|
|
}
|
|
|
|
function adminApi(path, opts = {}) {
|
|
return window.Api.request(path, opts);
|
|
}
|
|
|
|
function handleAdminError(err) {
|
|
const message = err?.message || 'Request failed';
|
|
alert(message);
|
|
}
|
|
|
|
async function loadRoles() {
|
|
const res = await adminApi('/api/admin/roles');
|
|
return res?.data?.items || [];
|
|
}
|
|
|
|
async function loadUsers() {
|
|
const res = await adminApi('/api/admin/users');
|
|
return res?.data?.items || [];
|
|
}
|
|
|
|
async function loadAudit() {
|
|
const res = await adminApi('/api/admin/audit', {
|
|
method: 'POST',
|
|
body: { page: 1, per_page: 50 },
|
|
});
|
|
return res?.data?.items || [];
|
|
}
|
|
|
|
function renderUsers(users, roles) {
|
|
const body = UI.qs('adminUsers');
|
|
if (!body) return;
|
|
body.innerHTML = '';
|
|
users.forEach((u) => {
|
|
const tr = document.createElement('tr');
|
|
const rolesList = (u.roles || '').split(',').filter(Boolean);
|
|
const status = u.status || '';
|
|
|
|
const roleOptions = roles.map((r) => {
|
|
const selected = rolesList.includes(r.name) ? 'selected' : '';
|
|
return `<option value="${r.name}" ${selected}>${r.name}</option>`;
|
|
}).join('');
|
|
|
|
tr.innerHTML = `
|
|
<td>${u.email || ''}</td>
|
|
<td>
|
|
<select data-action="role" data-id="${u.id}">
|
|
${roleOptions}
|
|
</select>
|
|
</td>
|
|
<td>${status}</td>
|
|
<td>${fmtDate(u.last_login_at)}</td>
|
|
<td>
|
|
<div class="admin-actions">
|
|
<button class="btn" data-action="toggle" data-id="${u.id}" data-status="${status}">
|
|
${status === 'disabled' ? 'Enable' : 'Disable'}
|
|
</button>
|
|
<button class="btn" data-action="reset2fa" data-id="${u.id}">Reset 2FA</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function renderAudit(items) {
|
|
const body = UI.qs('adminAudit');
|
|
if (!body) return;
|
|
body.innerHTML = '';
|
|
items.forEach((a) => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${fmtDate(a.created_at)}</td>
|
|
<td>${a.actor_email || a.actor_user_id || ''}</td>
|
|
<td>${a.action || ''}</td>
|
|
<td>${a.target_type || ''} ${a.target_id || ''}</td>
|
|
<td class="muted">${a.meta_json || ''}</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
let actionsBound = false;
|
|
|
|
async function bindActions(roles) {
|
|
if (actionsBound) return;
|
|
const body = UI.qs('adminUsers');
|
|
if (!body) return;
|
|
body.addEventListener('change', async (e) => {
|
|
const sel = e.target;
|
|
if (!sel || sel.dataset.action !== 'role') return;
|
|
const userId = sel.dataset.id;
|
|
const role = sel.value;
|
|
try {
|
|
await adminApi(`/api/admin/users/${userId}/roles`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ roles: [role] }),
|
|
});
|
|
} catch (err) {
|
|
handleAdminError(err);
|
|
}
|
|
});
|
|
body.addEventListener('click', async (e) => {
|
|
const btn = e.target.closest('button');
|
|
if (!btn) return;
|
|
const action = btn.dataset.action;
|
|
const userId = btn.dataset.id;
|
|
if (action === 'toggle') {
|
|
const disabled = btn.dataset.status !== 'disabled';
|
|
try {
|
|
await adminApi(`/api/admin/users/${userId}/disable`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ disabled }),
|
|
});
|
|
init();
|
|
} catch (err) {
|
|
handleAdminError(err);
|
|
}
|
|
}
|
|
if (action === 'reset2fa') {
|
|
try {
|
|
await adminApi(`/api/admin/users/${userId}/reset-2fa`, { method: 'POST' });
|
|
} catch (err) {
|
|
handleAdminError(err);
|
|
}
|
|
}
|
|
});
|
|
actionsBound = true;
|
|
}
|
|
|
|
async function init() {
|
|
window.Auth.requireAuth();
|
|
window.UI?.initHeader?.();
|
|
if (!window.Auth.isAdmin()) {
|
|
UI.qs('adminUsers').innerHTML = '<tr><td colspan="5">Admin only</td></tr>';
|
|
return;
|
|
}
|
|
const [roles, users, audit] = await Promise.all([loadRoles(), loadUsers(), loadAudit()]);
|
|
renderUsers(users, roles);
|
|
renderAudit(audit);
|
|
bindActions(roles);
|
|
}
|
|
|
|
const btnReloadUsers = UI.qs('btnReloadUsers');
|
|
if (btnReloadUsers) btnReloadUsers.addEventListener('click', init);
|
|
const btnReloadAudit = UI.qs('btnReloadAudit');
|
|
if (btnReloadAudit) btnReloadAudit.addEventListener('click', init);
|
|
|
|
init();
|