From f073f81c576ee1cee2c6cc9cd83ab12e3d9d459b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 20 Dec 2025 23:52:48 +0000 Subject: [PATCH] Add admin user list functionality Co-authored-by: mathieumoran1 --- public/control.html | 29 ++++++++++ public/js/app_control.js | 119 +++++++++++++++++++++++++++++++++++++++ routes/users.js | 25 ++++++++ 3 files changed, 173 insertions(+) diff --git a/public/control.html b/public/control.html index fc5290c..c11baa6 100644 --- a/public/control.html +++ b/public/control.html @@ -195,6 +195,35 @@ + +
+
+
+
+

Liste des utilisateurs

+
+ — utilisateur(s) +
+
+ +
+ +
+ + + + + + + + + + + +
NomEmailStatut
Charge la liste…
+
+
+
diff --git a/public/js/app_control.js b/public/js/app_control.js index 07b4694..ca9c142 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -213,6 +213,14 @@ window.onload = async function () { const userResetForm = document.getElementById('reset-user-password-form'); const userResetEmailEl = document.getElementById('reset_user_email'); + // Admin users list elements + const adminUsersBody = document.getElementById('admin-users-body'); + const refreshUsersBtn = document.getElementById('refresh-users'); + const adminUsersCountEl = document.getElementById('admin-users-count'); + const adminUsersTabBtn = document.getElementById('admin-users-tab'); + + let usersLoadedOnce = false; + function showUserResetResponse(kind, message) { if (!userResetResponseEl) return; userResetResponseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning'); @@ -220,6 +228,98 @@ window.onload = async function () { userResetResponseEl.textContent = message; } + function setUsersCount(count) { + if (!adminUsersCountEl) return; + adminUsersCountEl.textContent = Number.isInteger(count) ? String(count) : '—'; + } + + function renderAdminUsers(users) { + if (!adminUsersBody) return; + const list = Array.isArray(users) ? users : []; + setUsersCount(list.length); + + if (!list.length) { + adminUsersBody.innerHTML = 'Aucun utilisateur.'; + return; + } + + adminUsersBody.innerHTML = ''; + + list.forEach((u) => { + const tr = document.createElement('tr'); + + const tdName = document.createElement('td'); + tdName.textContent = u?.name || '(sans nom)'; + + const tdEmail = document.createElement('td'); + tdEmail.textContent = u?.email || ''; + + const tdStatus = document.createElement('td'); + tdStatus.className = 'text-nowrap'; + + const badges = []; + if (u?.admin) badges.push({ text: 'Admin', cls: 'bg-warning text-dark' }); + if (u?.mustChangePassword) badges.push({ text: 'Reset MDP', cls: 'bg-danger' }); + + if (!badges.length) { + const span = document.createElement('span'); + span.className = 'badge bg-secondary'; + span.textContent = 'OK'; + tdStatus.appendChild(span); + } else { + badges.forEach((b, idx) => { + const span = document.createElement('span'); + span.className = `badge ${b.cls}`; + span.textContent = b.text; + tdStatus.appendChild(span); + if (idx < badges.length - 1) { + tdStatus.appendChild(document.createTextNode(' ')); + } + }); + } + + tr.appendChild(tdName); + tr.appendChild(tdEmail); + tr.appendChild(tdStatus); + adminUsersBody.appendChild(tr); + }); + } + + async function fetchAdminUsers() { + const res = await fetch('/api/users/admin/list', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + } + }); + + const data = await res.json().catch(() => ([])); + if (!res.ok) { + const msg = data?.message || data?.error || `Erreur (${res.status})`; + throw new Error(msg); + } + return Array.isArray(data) ? data : []; + } + + async function loadAdminUsers(options = {}) { + const force = !!options.force; + if (!adminUsersBody) return; + if (usersLoadedOnce && !force) return; + + adminUsersBody.innerHTML = 'Chargement…'; + setUsersCount(null); + + try { + const users = await fetchAdminUsers(); + usersLoadedOnce = true; + users.sort((a, b) => (a?.name || '').localeCompare((b?.name || ''), 'fr', { sensitivity: 'base' })); + renderAdminUsers(users); + } catch (err) { + adminUsersBody.innerHTML = `${err.message || 'Erreur réseau'}`; + setUsersCount(null); + } + } + function parseYear(value) { const n = Number(value); if (!Number.isInteger(n)) return null; @@ -359,6 +459,25 @@ window.onload = async function () { }); } + if (refreshUsersBtn) { + refreshUsersBtn.addEventListener('click', async () => { + refreshUsersBtn.disabled = true; + const oldText = refreshUsersBtn.textContent; + refreshUsersBtn.textContent = 'Chargement...'; + try { + await loadAdminUsers({ force: true }); + } finally { + refreshUsersBtn.disabled = false; + refreshUsersBtn.textContent = oldText; + } + }); + } + + if (adminUsersTabBtn) { + // Only load when the tab is shown (and then cache), to keep initial load snappy. + adminUsersTabBtn.addEventListener('shown.bs.tab', () => loadAdminUsers({ force: false })); + } + form.addEventListener('submit', async function (e) { e.preventDefault(); diff --git a/routes/users.js b/routes/users.js index 437a558..b99d7a4 100644 --- a/routes/users.js +++ b/routes/users.js @@ -261,6 +261,31 @@ router.post("/admin/reset-temp-password", verifyToken, async (req, res) => { } }); +// Admin: list users (safe fields only) +router.get("/admin/list", verifyToken, async (req, res) => { + if (!isAdminFromDecoded(req.user)) { + return res.status(403).json({ message: "You do not have admin privileges" }); + } + + try { + const users = await User.find() + .select("name email role mustChangePassword") + .sort({ name: 1, email: 1 }); + + return res.status(200).json( + users.map((u) => ({ + id: String(u._id), + name: u.name, + email: u.email, + admin: u.role === 69, + mustChangePassword: !!u.mustChangePassword, + })) + ); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + // User: change password (used after temp-password login). Returns a fresh token. router.post("/change-password", verifyToken, async (req, res) => { const { newPassword } = req.body || {}; -- 2.54.0