// 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'); 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'); document.getElementById('user-field-username').focus(); } function closeUserModal() { userModal.classList.remove('is-open'); } // 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() { passwordModal.classList.remove('is-open'); } 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 table actions: Change Password, Delete document.getElementById('users-tbody').addEventListener('click', async (e) => { const { target } = e; 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('tr'); 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 tbody = document.getElementById('users-tbody'); const tr = document.createElement('tr'); tr.id = `user-row-${user.id}`; tr.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. tr.innerHTML = `