From 87396f72b8fdb4a566c3f97f9ec35bc1ed7a20ed Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 20:00:13 +0000 Subject: [PATCH 1/2] feat: Add user creation form and API endpoint Co-authored-by: mathieumoran1 --- public/control.html | 42 +++++++++++++++++ public/js/app_control.js | 98 ++++++++++++++++++++++++++++++++++++++++ routes/users.js | 55 ++++++++++++++++++++++ 3 files changed, 195 insertions(+) diff --git a/public/control.html b/public/control.html index b98fd5b..a06a506 100644 --- a/public/control.html +++ b/public/control.html @@ -172,6 +172,48 @@
+
+
+

Ajouter un utilisateur

+

+ Crée un compte avec un mot de passe temporaire (affiché une seule fois). L’utilisateur devra le changer à sa prochaine connexion. +

+ + + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
Mot de passe temporaire (affiché une seule fois)
+
+ — + +
+
+
+ +
+ + +
+
+
+
+
diff --git a/public/js/app_control.js b/public/js/app_control.js index 60a1377..e053847 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -218,6 +218,19 @@ window.onload = async function () { const adminUsersCountEl = document.getElementById('admin-users-count'); const adminUsersTabBtn = document.getElementById('admin-users-tab'); + // Admin: add user form elements + const addUserForm = document.getElementById('add-user-form'); + const addUserNameEl = document.getElementById('add_user_name'); + const addUserEmailEl = document.getElementById('add_user_email'); + const addUserAdminEl = document.getElementById('add_user_admin'); + const addUserResponseEl = document.getElementById('add-user-response'); + const addUserResultEl = document.getElementById('add-user-result'); + const addUserTempPasswordEl = document.getElementById('add-user-temp-password'); + const addUserExpiresAtEl = document.getElementById('add-user-expires-at'); + const addUserSubmitBtn = document.getElementById('add-user-submit'); + const addUserResetBtn = document.getElementById('add-user-reset'); + const copyAddUserTempPasswordBtn = document.getElementById('copy-add-user-temp-password'); + // Admin user modals const resetUserPasswordModalEl = document.getElementById('resetUserPasswordModal'); const resetUserPasswordModal = resetUserPasswordModalEl ? new bootstrap.Modal(resetUserPasswordModalEl) : null; @@ -255,6 +268,17 @@ window.onload = async function () { el.textContent = ''; } + function resetAddUserUi(options = {}) { + hideAlert(addUserResponseEl); + if (addUserResultEl) addUserResultEl.classList.add('d-none'); + if (addUserTempPasswordEl) addUserTempPasswordEl.textContent = '—'; + if (addUserExpiresAtEl) addUserExpiresAtEl.textContent = ''; + if (options.resetForm && addUserForm) { + try { addUserForm.reset(); } catch (_) {} + } + if (addUserSubmitBtn) addUserSubmitBtn.disabled = false; + } + function resetResetUserModalUi() { hideAlert(resetUserModalResponseEl); if (resetUserModalResultEl) resetUserModalResultEl.classList.add('d-none'); @@ -531,6 +555,80 @@ window.onload = async function () { }); } + if (copyAddUserTempPasswordBtn) { + copyAddUserTempPasswordBtn.addEventListener('click', async () => { + const text = String(addUserTempPasswordEl?.textContent || '').trim(); + if (!text || text === '—') return; + try { + await navigator.clipboard.writeText(text); + setAlert(addUserResponseEl, 'success', 'Copié.'); + setTimeout(() => hideAlert(addUserResponseEl), 1200); + } catch (_) { + setAlert(addUserResponseEl, 'warning', 'Impossible de copier automatiquement. Copie manuellement.'); + } + }); + } + + if (addUserResetBtn) { + addUserResetBtn.addEventListener('click', () => { + resetAddUserUi({ resetForm: true }); + }); + } + + if (addUserForm) { + addUserForm.addEventListener('submit', async (e) => { + e.preventDefault(); + resetAddUserUi({ resetForm: false }); + + const name = String(addUserNameEl?.value || '').trim(); + const email = String(addUserEmailEl?.value || '').trim(); + const admin = !!addUserAdminEl?.checked; + + if (!name) { + setAlert(addUserResponseEl, 'warning', 'Nom requis.'); + return; + } + if (!email) { + setAlert(addUserResponseEl, 'warning', 'Email requis.'); + return; + } + + if (addUserSubmitBtn) addUserSubmitBtn.disabled = true; + setAlert(addUserResponseEl, 'warning', 'Création...'); + + try { + const res = await fetch('/api/users/admin/create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ name, email, admin }), + }); + + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setAlert(addUserResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`); + if (addUserSubmitBtn) addUserSubmitBtn.disabled = false; + return; + } + + // Show temp password once + if (addUserTempPasswordEl) addUserTempPasswordEl.textContent = data?.tempPassword || '—'; + if (addUserExpiresAtEl) addUserExpiresAtEl.textContent = data?.expiresAt ? `Expire: ${data.expiresAt}` : ''; + if (addUserResultEl) addUserResultEl.classList.remove('d-none'); + setAlert(addUserResponseEl, 'success', data.message || 'Utilisateur créé.'); + if (addUserSubmitBtn) addUserSubmitBtn.disabled = false; + + // Refresh list if already loaded or when user tab is active + await loadAdminUsers({ force: true }); + } catch (err) { + setAlert(addUserResponseEl, 'danger', err.message || 'Erreur réseau'); + if (addUserSubmitBtn) addUserSubmitBtn.disabled = false; + } + }); + } + if (confirmResetUserPasswordBtn) { confirmResetUserPasswordBtn.addEventListener('click', async () => { const target = currentResetTarget; diff --git a/routes/users.js b/routes/users.js index cb0fd99..6486061 100644 --- a/routes/users.js +++ b/routes/users.js @@ -298,6 +298,61 @@ router.get("/admin/list", verifyToken, async (req, res) => { } }); +// Admin: create a user with a random temp password (forces change on first login). +router.post("/admin/create", verifyToken, async (req, res) => { + if (!isAdminFromDecoded(req.user)) { + return res.status(403).json({ message: "You do not have admin privileges" }); + } + + const { name, email, admin } = req.body || {}; + const nameNorm = typeof name === "string" ? name.trim() : ""; + const emailNorm = typeof email === "string" ? email.trim() : ""; + const makeAdmin = !!admin; + + if (!nameNorm) { + return res.status(400).json({ message: "Nom requis." }); + } + if (!emailNorm) { + return res.status(400).json({ message: "Email requis." }); + } + // Basic sanity check (not a full RFC validator) + if (!/^\S+@\S+\.\S+$/.test(emailNorm)) { + return res.status(400).json({ message: "Email invalide." }); + } + + try { + const existingUser = await User.findOne({ email: emailNorm }); + if (existingUser) { + return res.status(409).json({ message: "L'email existe déjà, veuillez en choisir un autre." }); + } + + const tempPassword = generateTempPassword(); + const hashedPassword = await bcrypt.hash(tempPassword, 10); + const expiresAt = new Date(Date.now() + TEMP_PASSWORD_TTL_MS); + + const user = new User({ + name: nameNorm, + email: emailNorm, + password: hashedPassword, + role: makeAdmin ? 69 : 0, + mustChangePassword: true, + tempPasswordIssuedAt: new Date(), + tempPasswordExpiresAt: expiresAt, + }); + + await user.save(); + + return res.status(201).json({ + message: "Utilisateur créé. Mot de passe temporaire généré.", + tempPassword, + expiresAt: expiresAt.toISOString(), + user: { id: String(user._id), name: user.name, email: user.email, admin: user.role === 69, mustChangePassword: true }, + }); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + // Admin: delete a user (and related playback progress) router.post("/admin/delete", verifyToken, async (req, res) => { if (!isAdminFromDecoded(req.user)) { -- 2.54.0 From 1b7a7f87a6ec61381b6ee868cdfd2a5c0c42b615 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Dec 2025 20:01:43 +0000 Subject: [PATCH 2/2] Refactor: Move add user form to a modal Co-authored-by: mathieumoran1 --- public/control.html | 95 ++++++++++++++++++++++------------------ public/js/app_control.js | 24 ++++++++++ 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/public/control.html b/public/control.html index a06a506..72f74f9 100644 --- a/public/control.html +++ b/public/control.html @@ -172,48 +172,6 @@
-
-
-

Ajouter un utilisateur

-

- Crée un compte avec un mot de passe temporaire (affiché une seule fois). L’utilisateur devra le changer à sa prochaine connexion. -

- - - -
-
- - -
-
- - -
-
- - -
- -
-
Mot de passe temporaire (affiché une seule fois)
-
- — - -
-
-
- -
- - -
-
-
-
-
@@ -223,7 +181,10 @@ — utilisateur(s)
- +
+ + +
@@ -433,6 +394,54 @@
+ + +