// User Management admin page: create users, change passwords, delete users. // CSRF token and the current user's id are handed off from PHP via //
. const csrfToken = document.body.dataset.csrfToken; const currentUserId = document.body.dataset.currentUserId; const userModal = document.getElementById('modal-user-backdrop'); const passwordModal = document.getElementById('modal-password-backdrop'); const editModal = document.getElementById('modal-edit-backdrop'); function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text ?? ''; return div.innerHTML; } async function postForm(formData) { const response = await fetch('users.php', { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); return response.json(); } const EYE_OPEN_SVG = ` `; const EYE_OFF_SVG = ` `; function resetPasswordVisibility(form) { form.querySelectorAll('.password-toggle-btn').forEach((btn) => { const input = document.getElementById(btn.dataset.target); if (input) input.type = 'password'; btn.setAttribute('aria-label', 'Show password'); btn.innerHTML = EYE_OPEN_SVG; }); } // Add User modal function openUserModal() { const form = document.getElementById('form-user'); form.reset(); resetPasswordVisibility(form); userModal.classList.add('is-open'); } // Safari ignores autocomplete="off" on fields labeled "Username" and offers // saved logins whenever one has focus. The modals used to auto-focus that // field, and a closed modal left it focused, so the prompt kept popping up. // Now nothing is auto-focused, and closing a modal drops focus. function closeModal(modal) { modal.classList.remove('is-open'); if (modal.contains(document.activeElement)) document.activeElement.blur(); } function closeUserModal() { closeModal(userModal); } // Change Password modal function openPasswordModal(userId, username) { const isSelf = String(userId) === currentUserId; const form = document.getElementById('form-password'); form.reset(); resetPasswordVisibility(form); document.getElementById('password-field-user-id').value = userId; document.getElementById('modal-password-title').textContent = isSelf ? '🔑 Change Your Password' : `🔑 Change Password for ${username}`; document.getElementById('password-current-group').style.display = isSelf ? '' : 'none'; passwordModal.classList.add('is-open'); (isSelf ? document.getElementById('password-field-current') : document.getElementById('password-field-new')).focus(); } function closePasswordModal() { closeModal(passwordModal); } // Edit User modal function openEditModal(userId, username, role) { const isSelf = String(userId) === currentUserId; const form = document.getElementById('form-edit'); const roleSelect = document.getElementById('edit-field-role'); form.reset(); document.getElementById('edit-field-user-id').value = userId; document.getElementById('edit-field-username').value = username; roleSelect.value = role; roleSelect.disabled = isSelf; document.getElementById('edit-role-self-hint').hidden = !isSelf; editModal.classList.add('is-open'); } function closeEditModal() { closeModal(editModal); } function capitalize(text) { return text.charAt(0).toUpperCase() + text.slice(1); } // Updates an existing row's text and data attributes in place after an edit. function updateUserRow(user) { const row = document.getElementById(`user-row-${user.id}`); if (!row) return; row.querySelector('[data-label="Username"] strong').textContent = user.username; row.querySelector('[data-label="Role"]').textContent = capitalize(user.role); row.querySelectorAll('.btn-edit-user, .btn-change-password').forEach((btn) => { btn.dataset.username = user.username; }); row.querySelector('.btn-edit-user').dataset.role = user.role; if (String(user.id) === currentUserId) { const navName = document.querySelector('.admin-account-link'); if (navName) navName.textContent = user.username; } } document.getElementById('btn-close-edit-modal').addEventListener('click', closeEditModal); document.getElementById('btn-cancel-edit').addEventListener('click', closeEditModal); document.getElementById('form-edit').addEventListener('submit', async (e) => { e.preventDefault(); const saveBtn = document.getElementById('btn-save-edit'); saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; try { const res = await postForm(new FormData(e.target)); if (res.success) { closeEditModal(); updateUserRow(res.user); } else { alert(res.error || 'Failed to update user.'); } } catch { alert('Network error. Please try again.'); } finally { saveBtn.disabled = false; saveBtn.textContent = 'Save Changes'; } }); document.getElementById('btn-new-user').addEventListener('click', () => openUserModal()); document.getElementById('btn-close-user-modal').addEventListener('click', closeUserModal); document.getElementById('btn-cancel-user').addEventListener('click', closeUserModal); document.getElementById('btn-close-password-modal').addEventListener('click', closePasswordModal); document.getElementById('btn-cancel-password').addEventListener('click', closePasswordModal); // Password visibility toggles (works inside both modals) document.addEventListener('click', (e) => { const toggleBtn = e.target.closest('.password-toggle-btn'); if (!toggleBtn) return; const targetId = toggleBtn.dataset.target; const input = document.getElementById(targetId); if (!input) return; const isPassword = input.type === 'password'; input.type = isPassword ? 'text' : 'password'; toggleBtn.setAttribute('aria-label', isPassword ? 'Hide password' : 'Show password'); toggleBtn.innerHTML = isPassword ? EYE_OFF_SVG : EYE_OPEN_SVG; input.focus(); }); // Create user document.getElementById('form-user').addEventListener('submit', async (e) => { e.preventDefault(); const saveBtn = document.getElementById('btn-save-user'); saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; try { const res = await postForm(new FormData(e.target)); if (res.success) { closeUserModal(); insertUserRow(res.user); } else { alert(res.error || 'Failed to create user.'); } } catch { alert('Network error. Please try again.'); } finally { saveBtn.disabled = false; saveBtn.textContent = 'Create User'; } }); // Change password document.getElementById('form-password').addEventListener('submit', async (e) => { e.preventDefault(); const saveBtn = document.getElementById('btn-save-password'); saveBtn.disabled = true; saveBtn.textContent = 'Saving...'; try { const res = await postForm(new FormData(e.target)); if (res.success) { closePasswordModal(); alert('Password updated.'); } else { alert(res.error || 'Failed to update password.'); } } catch { alert('Network error. Please try again.'); } finally { saveBtn.disabled = false; saveBtn.textContent = 'Update Password'; } }); // Delegate row actions: toggle Status, Change Password, Delete document.getElementById('users-rows').addEventListener('click', async (e) => { const { target } = e; // The status badge itself is the toggle button — same convention as the // menu items and announcements lists. Self's badge renders as a plain // (non-button) span, so this never matches your own row. if (target.classList.contains('btn-toggle-user')) { target.disabled = true; const fd = new FormData(); fd.append('action', 'toggle_user_enabled'); fd.append('user_id', target.dataset.id); fd.append('csrf_token', csrfToken); try { const data = await postForm(fd); if (data.success) { target.className = `status-badge btn-toggle-user ${data.status_class}`; target.textContent = data.status_badge; target.title = `Click to mark as ${data.is_enabled ? 'disabled' : 'enabled'}`; } else { alert(data.error || 'Failed to update user status.'); } } catch { alert('Network error. Please try again.'); } finally { target.disabled = false; } return; } if (target.classList.contains('btn-edit-user')) { openEditModal(target.dataset.id, target.dataset.username, target.dataset.role); return; } if (target.classList.contains('btn-change-password')) { openPasswordModal(target.dataset.id, target.dataset.username); return; } if (target.classList.contains('btn-delete-user')) { if (!confirm('Are you sure you want to delete this user? This cannot be undone.')) return; const row = target.closest('.user-row'); const fd = new FormData(); fd.append('action', 'delete_user'); fd.append('user_id', target.dataset.id); fd.append('csrf_token', csrfToken); try { const data = await postForm(fd); if (data.success && row) { row.style.transition = 'opacity 0.2s ease, transform 0.2s ease'; row.style.opacity = '0'; row.style.transform = 'translateX(-10px)'; setTimeout(() => row.remove(), 200); } else if (!data.success) { alert(data.error || 'Failed to delete user.'); } } catch { alert('Network error. Please try again.'); } } }); function insertUserRow(user) { const rowsContainer = document.getElementById('users-rows'); const row = document.createElement('div'); row.className = 'user-row'; row.id = `user-row-${user.id}`; row.dataset.userId = user.id; // data-username is set via dataset below, never interpolated into the // HTML string — escapeHtml() only escapes for text-content positions, // not attribute values, so it can't safely build an attribute string. // A user you just created is never "you" — the current admin's own row // already exists — so its Status badge always renders as the clickable // toggle, never the plain self span (see users.php for that variant). row.innerHTML = `