diff --git a/public/js/app_control.js b/public/js/app_control.js
index 62722d4..60a1377 100644
--- a/public/js/app_control.js
+++ b/public/js/app_control.js
@@ -212,23 +212,79 @@ window.onload = async function () {
}
}
- const userResetResponseEl = document.getElementById('user-reset-response');
- 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;
+ // Admin user modals
+ const resetUserPasswordModalEl = document.getElementById('resetUserPasswordModal');
+ const resetUserPasswordModal = resetUserPasswordModalEl ? new bootstrap.Modal(resetUserPasswordModalEl) : null;
+ const resetUserLabelEl = document.getElementById('reset_user_label');
+ const resetUserEmailLabelEl = document.getElementById('reset_user_email_label');
+ const resetUserModalResponseEl = document.getElementById('reset-user-modal-response');
+ const resetUserModalResultEl = document.getElementById('reset-user-modal-result');
+ const resetUserTempPasswordEl = document.getElementById('reset-user-temp-password');
+ const resetUserExpiresAtEl = document.getElementById('reset-user-expires-at');
+ const confirmResetUserPasswordBtn = document.getElementById('confirm-reset-user-password');
+ const copyTempPasswordBtn = document.getElementById('copy-temp-password');
- function showUserResetResponse(kind, message) {
- if (!userResetResponseEl) return;
- userResetResponseEl.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
- userResetResponseEl.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
- userResetResponseEl.textContent = message;
+ const deleteUserModalEl = document.getElementById('deleteUserModal');
+ const deleteUserModal = deleteUserModalEl ? new bootstrap.Modal(deleteUserModalEl) : null;
+ const deleteUserLabelEl = document.getElementById('delete_user_label');
+ const deleteUserEmailLabelEl = document.getElementById('delete_user_email_label');
+ const deleteUserVerifyInputEl = document.getElementById('delete_user_verify_input');
+ const deleteUserModalResponseEl = document.getElementById('delete-user-modal-response');
+ const confirmDeleteUserBtn = document.getElementById('confirm-delete-user');
+
+ let usersLoadedOnce = false;
+ let currentResetTarget = null;
+ let currentDeleteTarget = null;
+
+ function setAlert(el, kind, message) {
+ if (!el) return;
+ el.classList.remove('d-none', 'alert-success', 'alert-danger', 'alert-warning');
+ el.classList.add(kind === 'success' ? 'alert-success' : kind === 'warning' ? 'alert-warning' : 'alert-danger');
+ el.textContent = message;
+ }
+
+ function hideAlert(el) {
+ if (!el) return;
+ el.classList.add('d-none');
+ el.textContent = '';
+ }
+
+ function resetResetUserModalUi() {
+ hideAlert(resetUserModalResponseEl);
+ if (resetUserModalResultEl) resetUserModalResultEl.classList.add('d-none');
+ if (resetUserTempPasswordEl) resetUserTempPasswordEl.textContent = '—';
+ if (resetUserExpiresAtEl) resetUserExpiresAtEl.textContent = '';
+ if (confirmResetUserPasswordBtn) confirmResetUserPasswordBtn.disabled = false;
+ }
+
+ function openResetUserModal(user) {
+ currentResetTarget = user || null;
+ if (!resetUserPasswordModal) return;
+ resetResetUserModalUi();
+ if (resetUserLabelEl) resetUserLabelEl.textContent = user?.name ? user.name : '(sans nom)';
+ if (resetUserEmailLabelEl) resetUserEmailLabelEl.textContent = user?.email || '';
+ resetUserPasswordModal.show();
+ }
+
+ function resetDeleteUserModalUi() {
+ hideAlert(deleteUserModalResponseEl);
+ if (deleteUserVerifyInputEl) deleteUserVerifyInputEl.value = '';
+ if (confirmDeleteUserBtn) confirmDeleteUserBtn.disabled = true;
+ }
+
+ function openDeleteUserModal(user) {
+ currentDeleteTarget = user || null;
+ if (!deleteUserModal) return;
+ resetDeleteUserModalUi();
+ if (deleteUserLabelEl) deleteUserLabelEl.textContent = user?.name ? user.name : '(sans nom)';
+ if (deleteUserEmailLabelEl) deleteUserEmailLabelEl.textContent = user?.email || '';
+ deleteUserModal.show();
}
function setUsersCount(count) {
@@ -242,7 +298,7 @@ window.onload = async function () {
setUsersCount(list.length);
if (!list.length) {
- adminUsersBody.innerHTML = '
| Aucun utilisateur. |
';
+ adminUsersBody.innerHTML = '| Aucun utilisateur. |
';
return;
}
@@ -281,9 +337,34 @@ window.onload = async function () {
});
}
+ const tdActions = document.createElement('td');
+ tdActions.className = 'text-nowrap';
+
+ const resetBtn = document.createElement('button');
+ resetBtn.type = 'button';
+ resetBtn.className = 'btn btn-outline-danger btn-sm me-1';
+ resetBtn.textContent = 'Reset MDP';
+ resetBtn.addEventListener('click', () => openResetUserModal(u));
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.type = 'button';
+ deleteBtn.className = 'btn btn-outline-danger btn-sm';
+ deleteBtn.textContent = 'Supprimer';
+
+ const isSelf = String(u?.id || '') && String(u.id) === String(decoded?.id || '');
+ if (isSelf || u?.admin) {
+ deleteBtn.disabled = true;
+ deleteBtn.title = isSelf ? 'Impossible de supprimer ton propre compte' : 'Suppression des admins désactivée';
+ }
+
+ deleteBtn.addEventListener('click', () => openDeleteUserModal(u));
+ tdActions.appendChild(resetBtn);
+ tdActions.appendChild(deleteBtn);
+
tr.appendChild(tdName);
tr.appendChild(tdEmail);
tr.appendChild(tdStatus);
+ tr.appendChild(tdActions);
adminUsersBody.appendChild(tr);
});
}
@@ -309,7 +390,7 @@ window.onload = async function () {
if (!adminUsersBody) return;
if (usersLoadedOnce && !force) return;
- adminUsersBody.innerHTML = '| Chargement… |
';
+ adminUsersBody.innerHTML = '| Chargement… |
';
setUsersCount(null);
try {
@@ -318,7 +399,7 @@ window.onload = async function () {
users.sort((a, b) => (a?.name || '').localeCompare((b?.name || ''), 'fr', { sensitivity: 'base' }));
renderAdminUsers(users);
} catch (err) {
- adminUsersBody.innerHTML = `| ${err.message || 'Erreur réseau'} |
`;
+ adminUsersBody.innerHTML = `| ${err.message || 'Erreur réseau'} |
`;
setUsersCount(null);
}
}
@@ -427,37 +508,108 @@ window.onload = async function () {
}
}
- if (userResetForm && userResetEmailEl) {
- userResetForm.addEventListener('submit', async function (e) {
- e.preventDefault();
- const email = (userResetEmailEl.value || '').trim();
- if (!email) {
- showUserResetResponse('warning', 'Email invalide.');
- return;
+ if (deleteUserVerifyInputEl && confirmDeleteUserBtn) {
+ deleteUserVerifyInputEl.addEventListener('input', () => {
+ const expected = String(currentDeleteTarget?.email || '').trim();
+ const typed = String(deleteUserVerifyInputEl.value || '').trim();
+ confirmDeleteUserBtn.disabled = !(expected && typed && expected === typed);
+ });
+ }
+
+ if (copyTempPasswordBtn) {
+ copyTempPasswordBtn.addEventListener('click', async () => {
+ const text = String(resetUserTempPasswordEl?.textContent || '').trim();
+ if (!text || text === '—') return;
+ try {
+ await navigator.clipboard.writeText(text);
+ setAlert(resetUserModalResponseEl, 'success', 'Copié.');
+ setTimeout(() => hideAlert(resetUserModalResponseEl), 1200);
+ } catch (_) {
+ // Fallback: select text for manual copy
+ setAlert(resetUserModalResponseEl, 'warning', 'Impossible de copier automatiquement. Copie manuellement.');
}
+ });
+ }
+
+ if (confirmResetUserPasswordBtn) {
+ confirmResetUserPasswordBtn.addEventListener('click', async () => {
+ const target = currentResetTarget;
+ const userId = String(target?.id || '').trim();
+ if (!userId) return;
+
+ confirmResetUserPasswordBtn.disabled = true;
+ resetResetUserModalUi();
+ setAlert(resetUserModalResponseEl, 'warning', 'Génération du mot de passe temporaire...');
try {
- showUserResetResponse('warning', 'Génération du mot de passe temporaire...');
const res = await fetch('/api/users/admin/reset-temp-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
- body: JSON.stringify({ email }),
+ body: JSON.stringify({ userId }),
});
-
const data = await res.json().catch(() => ({}));
if (!res.ok) {
- showUserResetResponse('danger', data.message || data.error || `Erreur (${res.status})`);
+ setAlert(resetUserModalResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`);
+ confirmResetUserPasswordBtn.disabled = false;
return;
}
- const label = data?.user?.name ? `${data.user.name} (${data.user.email})` : email;
- const expiresAt = data?.expiresAt ? ` Expire: ${data.expiresAt}` : '';
- showUserResetResponse('success', `Temp password pour ${label}: ${data.tempPassword}.${expiresAt}`);
+ hideAlert(resetUserModalResponseEl);
+ if (resetUserTempPasswordEl) resetUserTempPasswordEl.textContent = data?.tempPassword || '—';
+ if (resetUserExpiresAtEl) {
+ resetUserExpiresAtEl.textContent = data?.expiresAt ? `Expire: ${data.expiresAt}` : '';
+ }
+ if (resetUserModalResultEl) resetUserModalResultEl.classList.remove('d-none');
+ setAlert(resetUserModalResponseEl, 'success', 'Mot de passe temporaire généré. Copie-le et envoie-le à l’utilisateur.');
+ confirmResetUserPasswordBtn.disabled = false;
+ await loadAdminUsers({ force: true });
} catch (err) {
- showUserResetResponse('danger', err.message || 'Erreur réseau');
+ setAlert(resetUserModalResponseEl, 'danger', err.message || 'Erreur réseau');
+ confirmResetUserPasswordBtn.disabled = false;
+ }
+ });
+ }
+
+ if (confirmDeleteUserBtn) {
+ confirmDeleteUserBtn.addEventListener('click', async () => {
+ const target = currentDeleteTarget;
+ const userId = String(target?.id || '').trim();
+ if (!userId) return;
+
+ confirmDeleteUserBtn.disabled = true;
+ hideAlert(deleteUserModalResponseEl);
+ setAlert(deleteUserModalResponseEl, 'warning', 'Suppression...');
+
+ try {
+ const res = await fetch('/api/users/admin/delete', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({ userId }),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) {
+ setAlert(deleteUserModalResponseEl, 'danger', data.message || data.error || `Erreur (${res.status})`);
+ // Keep disabled until the input matches again.
+ const expected = String(target?.email || '').trim();
+ const typed = String(deleteUserVerifyInputEl?.value || '').trim();
+ confirmDeleteUserBtn.disabled = !(expected && typed && expected === typed);
+ return;
+ }
+
+ setAlert(deleteUserModalResponseEl, 'success', data.message || 'Utilisateur supprimé.');
+ await loadAdminUsers({ force: true });
+ // Close shortly after success
+ setTimeout(() => {
+ try { deleteUserModal?.hide(); } catch (_) {}
+ }, 600);
+ } catch (err) {
+ setAlert(deleteUserModalResponseEl, 'danger', err.message || 'Erreur réseau');
}
});
}
diff --git a/routes/users.js b/routes/users.js
index 9468199..cb0fd99 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -5,6 +5,7 @@ const crypto = require("crypto");
const User = require("../models/User");
const Movie = require("../models/Movie");
const Setting = require("../models/Setting");
+const PlaybackProgress = require("../models/PlaybackProgress");
const axios = require("axios");
const router = express.Router();
@@ -297,6 +298,46 @@ router.get("/admin/list", verifyToken, async (req, res) => {
}
});
+// Admin: delete a user (and related playback progress)
+router.post("/admin/delete", verifyToken, async (req, res) => {
+ if (!isAdminFromDecoded(req.user)) {
+ return res.status(403).json({ message: "You do not have admin privileges" });
+ }
+
+ const { email, userId } = req.body || {};
+ const emailNorm = typeof email === "string" ? email.trim() : "";
+ const idNorm = typeof userId === "string" ? userId.trim() : "";
+
+ if (!emailNorm && !idNorm) {
+ return res.status(400).json({ message: "Email ou userId requis." });
+ }
+
+ try {
+ const user = emailNorm ? await User.findOne({ email: emailNorm }) : await User.findById(idNorm);
+ if (!user) return res.status(404).json({ message: "User not found" });
+
+ // Safety: prevent deleting yourself from the admin UI.
+ if (String(user._id) === String(req.user?.id || "")) {
+ return res.status(400).json({ message: "Impossible de supprimer ton propre compte." });
+ }
+
+ // Safety: prevent deleting admin accounts via the UI endpoint.
+ if (user.role === 69) {
+ return res.status(400).json({ message: "Impossible de supprimer un compte admin via cette interface." });
+ }
+
+ await PlaybackProgress.deleteMany({ userId: user._id });
+ await User.deleteOne({ _id: user._id });
+
+ return res.status(200).json({
+ message: "Utilisateur supprimé.",
+ user: { id: String(user._id), name: user.name, email: user.email },
+ });
+ } 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 || {};