module.exports = function registerAdminUsersRoutes(app, deps) { const pool = deps.pool; const pages = deps.pages; const formatDashboardDate = deps.formatDashboardDate; const getAuditUserId = deps.getAuditUserId; const hashPassword = deps.hashPassword; const readArrayField = deps.readArrayField; const rbacData = deps.rbacData; const requirePermission = deps.requirePermission; async function fetchRoleOptions() { return rbacData.fetchRoles(pool); } function mapRolesForForm(roles, selectedRoleIds) { const selectedIds = new Set((Array.isArray(selectedRoleIds) ? selectedRoleIds : []).map(function (roleId) { return Number(roleId); }).filter(function (roleId) { return Number.isInteger(roleId) && roleId > 0; })); return (Array.isArray(roles) ? roles : []).map(function (role) { return Object.assign({}, role, { isSelected: selectedIds.has(Number(role.id)) }); }); } async function validateRoleIds(roleIds) { const availableRoles = await fetchRoleOptions(); const validRoleIds = new Set(availableRoles.map(function (role) { return Number(role.id); })); const normalizedRoleIds = Array.from(new Set((Array.isArray(roleIds) ? roleIds : []).map(function (roleId) { return Number(roleId); }).filter(function (roleId) { return Number.isInteger(roleId) && roleId > 0; }))); if (!normalizedRoleIds.length) { return { ok: false, message: 'Select at least one role.' }; } if (normalizedRoleIds.some(function (roleId) { return !validRoleIds.has(roleId); })) { return { ok: false, message: 'One or more selected roles are invalid.' }; } return { ok: true, roleIds: normalizedRoleIds }; } app.get('/admin/users', requirePermission('users.read'), async function (req, res, next) { try { const users = await rbacData.fetchUsersWithRoles(pool); const mappedUsers = users.map(function (user) { return Object.assign({}, user, { isCurrentUser: Number(user.id) === Number(req.currentUser.id), createdAtLabel: formatDashboardDate(user.created_at), modifiedAtLabel: formatDashboardDate(user.modified_at), roleNames: String(user.roleNames || '').trim() || 'No roles assigned' }); }); res.send(pages.renderUsersPage({ users: mappedUsers }, req.query.message ? String(req.query.message) : '', req.currentUser)); } catch (error) { next(error); } }); app.get('/admin/users/new', requirePermission('users.create'), function (req, res) { fetchRoleOptions().then(function (roles) { res.send(pages.renderUsersAddPage(req.query.message ? String(req.query.message) : '', req.currentUser, mapRolesForForm(roles, []), {}, 'primary')); }).catch(function (error) { res.status(500).send(error.message || 'Unable to load roles.'); }); }); app.get('/admin/users/:id/edit', requirePermission('users.edit'), async function (req, res, next) { try { const userId = Number(req.params.id); if (!Number.isInteger(userId) || userId <= 0) { return res.status(400).send('Invalid user.'); } const user = await rbacData.fetchUserWithRoles(pool, userId); if (!user) { return res.status(404).send('User not found.'); } if (Number(req.currentUser.id) === userId) { return res.redirect('/admin/account?message=' + encodeURIComponent('Use My Account to update your own login details.')); } const roles = await fetchRoleOptions(); res.send(pages.renderUsersEditPage(Object.assign({}, user, { isCurrentUser: false, createdAtLabel: formatDashboardDate(user.created_at), modifiedAtLabel: formatDashboardDate(user.modified_at), roleNames: String(user.roleNames || '').trim() || 'No roles assigned' }), req.query.message ? String(req.query.message) : '', req.currentUser, mapRolesForForm(roles, user.roleIds))); } catch (error) { next(error); } }); app.post('/admin/users', requirePermission('users.create'), async function (req, res, next) { const connection = await pool.getConnection(); try { const name = String(req.body.name || '').trim(); const username = String(req.body.username || '').trim(); const password = String(req.body.password || ''); const confirmPassword = String(req.body.confirm_password || ''); const selectedRoleIds = readArrayField(req.body, ['role_ids[]', 'role_ids']); const roleCheck = await validateRoleIds(selectedRoleIds); const formValues = { username: username, name: name }; async function renderValidationError(message) { const roles = await fetchRoleOptions(); return res.status(400).send(pages.renderUsersAddPage(message, req.currentUser, mapRolesForForm(roles, selectedRoleIds), formValues, 'warning')); } if (!name) { return renderValidationError('Name is required.'); } if (!username) { return renderValidationError('Username is required.'); } if (!password || password.length < 8) { return renderValidationError('Password must be at least 8 characters.'); } if (password !== confirmPassword) { return renderValidationError('Passwords do not match.'); } if (!roleCheck.ok) { return renderValidationError(roleCheck.message); } const [existingRows] = await connection.query('SELECT id FROM users WHERE username = ? LIMIT 1', [username]); if (existingRows.length) { return renderValidationError('That username already exists.'); } const passwordRecord = hashPassword(password); const actorId = getAuditUserId(req); await connection.beginTransaction(); const [result] = await connection.query( 'INSERT INTO users (name, username, password_hash, password_salt, password_iterations, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)', [name, username, passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, actorId, actorId] ); await rbacData.syncUserRoles(connection, result.insertId, roleCheck.roleIds); await connection.commit(); res.redirect('/admin/users?message=' + encodeURIComponent('User created.')); } catch (error) { try { await connection.rollback(); } catch (_rollbackError) { // Ignore rollback failures and surface the original error. } next(error); } finally { connection.release(); } }); app.post('/admin/users/:id/roles', requirePermission('users.edit'), async function (req, res, next) { try { const userId = Number(req.params.id); if (!Number.isInteger(userId) || userId <= 0) { return res.status(400).send('Invalid user.'); } if (Number(req.currentUser.id) === userId) { return res.redirect('/admin/account?message=' + encodeURIComponent('Use My Account to update your own login details.')); } const [rows] = await pool.query('SELECT id FROM users WHERE id = ? LIMIT 1', [userId]); if (!rows.length) { return res.status(404).send('User not found.'); } const selectedRoleIds = readArrayField(req.body, ['role_ids[]', 'role_ids']); const roleCheck = await validateRoleIds(selectedRoleIds); if (!roleCheck.ok) { return res.redirect('/admin/users/' + userId + '/edit?message=' + encodeURIComponent(roleCheck.message)); } await rbacData.syncUserRoles(pool, userId, roleCheck.roleIds); res.redirect('/admin/users/' + userId + '/edit?message=' + encodeURIComponent('Roles updated.')); } catch (error) { next(error); } }); app.post('/admin/users/:id/username', requirePermission('users.edit'), async function (req, res, next) { try { const userId = Number(req.params.id); const name = String(req.body.name || '').trim(); const username = String(req.body.username || '').trim(); if (!Number.isInteger(userId) || userId <= 0) { return res.status(400).send('Invalid user.'); } if (!name) { return res.redirect('/admin/users/' + userId + '/edit?message=' + encodeURIComponent('Name is required.')); } if (Number(req.currentUser.id) === userId) { return res.redirect('/admin/account?message=' + encodeURIComponent('Use My Account to update your own username or password.')); } if (!username) { return res.redirect('/admin/users/' + userId + '/edit?message=' + encodeURIComponent('Username is required.')); } const [existingRows] = await pool.query('SELECT id FROM users WHERE username = ? AND id <> ? LIMIT 1', [username, userId]); if (existingRows.length) { return res.redirect('/admin/users/' + userId + '/edit?message=' + encodeURIComponent('That username already exists.')); } const [result] = await pool.query('UPDATE users SET name = ?, username = ?, modified_by = ? WHERE id = ?', [name, username, getAuditUserId(req), userId]); if (!result.affectedRows) { return res.status(404).send('User not found.'); } res.redirect('/admin/users?message=' + encodeURIComponent('Username updated.')); } catch (error) { next(error); } }); app.post('/admin/users/:id/password', requirePermission('users.edit'), async function (req, res, next) { try { const userId = Number(req.params.id); const password = String(req.body.password || ''); const confirmPassword = String(req.body.confirm_password || ''); if (!Number.isInteger(userId) || userId <= 0) { return res.status(400).send('Invalid user.'); } if (Number(req.currentUser.id) === userId) { return res.redirect('/admin/account?message=' + encodeURIComponent('Use My Account to change your own password.')); } if (!password || password.length < 8) { return res.redirect('/admin/users?message=' + encodeURIComponent('Password must be at least 8 characters.')); } if (password !== confirmPassword) { return res.redirect('/admin/users?message=' + encodeURIComponent('Passwords do not match.')); } const [rows] = await pool.query('SELECT id FROM users WHERE id = ? LIMIT 1', [userId]); if (!rows.length) { return res.status(404).send('User not found.'); } const passwordRecord = hashPassword(password); await pool.query( 'UPDATE users SET password_hash = ?, password_salt = ?, password_iterations = ?, modified_by = ? WHERE id = ?', [passwordRecord.hash, passwordRecord.salt, passwordRecord.iterations, getAuditUserId(req), userId] ); await pool.query('DELETE FROM auth_sessions WHERE user_id = ?', [userId]); res.redirect('/admin/users?message=' + encodeURIComponent('Password updated.')); } catch (error) { next(error); } }); app.post('/admin/users/:id/delete', requirePermission('users.delete'), async function (req, res, next) { try { const userId = Number(req.params.id); if (!Number.isInteger(userId) || userId <= 0) { return res.status(400).send('Invalid user.'); } if (Number(req.currentUser.id) === userId) { return res.redirect('/admin/users?message=' + encodeURIComponent('You cannot delete your own account from the users page.')); } const [countRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users'); if (!countRows.length || Number(countRows[0].user_count) <= 1) { return res.redirect('/admin/users?message=' + encodeURIComponent('At least one user must remain.')); } const [result] = await pool.query('DELETE FROM users WHERE id = ?', [userId]); if (!result.affectedRows) { return res.status(404).send('User not found.'); } res.redirect('/admin/users?message=' + encodeURIComponent('User deleted.')); } catch (error) { next(error); } }); };