Allow admins to manage user passwords

This commit is contained in:
Cauê Faleiros
2026-06-09 14:26:00 -03:00
parent 40c7807a65
commit f77ee406ec
5 changed files with 121 additions and 18 deletions

View File

@@ -23,6 +23,7 @@ const canUpdateUser = (actor, targetUser) => {
const canManageUserStatus = (actor) => actor.role === 'super_admin' || actor.role === 'admin';
const canChangeUserEmail = (actor, targetUser) => actor.id === targetUser.id || actor.role === 'super_admin' || actor.role === 'admin';
const canManageUserRoleOrTeam = (actor) => actor.role === 'super_admin' || actor.role === 'admin';
const canManageUserPassword = (actor) => actor.role === 'super_admin' || actor.role === 'admin';
const canReadAttendance = (actor, attendance) => {
if (!actor || !attendance || !sameTenant(actor, attendance)) return false;
@@ -38,5 +39,6 @@ module.exports = {
canManageUserStatus,
canChangeUserEmail,
canManageUserRoleOrTeam,
canManageUserPassword,
canReadAttendance,
};

View File

@@ -1,4 +1,5 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const { requireRole } = require('../middleware/auth');
const {
@@ -7,10 +8,12 @@ const {
canManageUserStatus,
canChangeUserEmail,
canManageUserRoleOrTeam,
canManageUserPassword,
} = require('../policies/accessPolicy');
const { recordActivity } = require('../services/activityService');
const USER_PUBLIC_FIELDS = 'id, tenant_id, team_id, name, email, slug, role, status, bio, avatar_url, created_at';
const MIN_PASSWORD_LENGTH = 8;
const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
const router = express.Router();
@@ -60,8 +63,9 @@ const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
});
router.post('/users', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
const { name, email, role, team_id, tenant_id } = req.body;
const { name, email, role, team_id, tenant_id, password } = req.body;
const effectiveTenantId = req.user.role === 'super_admin' ? tenant_id : req.user.tenant_id;
const shouldSetPassword = typeof password === 'string' && password.length > 0;
let finalRole = role || 'agent';
let finalTeamId = team_id || null;
@@ -72,18 +76,32 @@ const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
finalTeamId = req.user.team_id;
}
try {
if (shouldSetPassword && !canManageUserPassword(req.user)) {
return res.status(403).json({ error: 'Apenas admins podem definir senha de usuários.' });
}
if (shouldSetPassword && password.length < MIN_PASSWORD_LENGTH) {
return res.status(400).json({ error: `A senha deve ter pelo menos ${MIN_PASSWORD_LENGTH} caracteres.` });
}
if (finalRole === 'super_admin' && req.user.role !== 'super_admin') {
return res.status(403).json({ error: 'Apenas super admins podem criar super admins.' });
}
const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
if (existing.length > 0) return res.status(400).json({ error: 'E-mail já cadastrado.' });
const uid = `u_${crypto.randomUUID().split('-')[0]}`;
const slug = `${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${crypto.randomBytes(4).toString('hex')}`;
const placeholderHash = 'pending_setup';
const passwordHash = shouldSetPassword ? await bcrypt.hash(password, 10) : 'pending_setup';
await pool.query(
'INSERT INTO users (id, tenant_id, team_id, name, email, password_hash, slug, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[uid, effectiveTenantId, finalTeamId, name, email, placeholderHash, slug, finalRole, 'active']
[uid, effectiveTenantId, finalTeamId, name, email, passwordHash, slug, finalRole, 'active']
);
if (shouldSetPassword) {
return res.status(201).json({ id: uid, message: 'Membro criado com senha definida.' });
}
const token = crypto.randomBytes(32).toString('hex');
await pool.query(
'INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))',
@@ -119,12 +137,22 @@ const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
});
router.put('/users/:id', async (req, res) => {
const { name, bio, role, team_id, status, email } = req.body;
const { name, bio, role, team_id, status, email, password } = req.body;
const shouldUpdatePassword = typeof password === 'string' && password.length > 0;
try {
const [existing] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
if (!canUpdateUser(req.user, existing[0])) return res.status(403).json({ error: 'Acesso negado.' });
if (shouldUpdatePassword && !canManageUserPassword(req.user)) {
return res.status(403).json({ error: 'Apenas admins podem alterar senha de usuários.' });
}
if (shouldUpdatePassword && password.length < MIN_PASSWORD_LENGTH) {
return res.status(400).json({ error: `A senha deve ter pelo menos ${MIN_PASSWORD_LENGTH} caracteres.` });
}
if (role === 'super_admin' && req.user.role !== 'super_admin') {
return res.status(403).json({ error: 'Apenas super admins podem definir super admins.' });
}
const finalRole = canManageUserRoleOrTeam(req.user) && role !== undefined ? role : existing[0].role;
const finalTeamId = canManageUserRoleOrTeam(req.user) && team_id !== undefined ? team_id : existing[0].team_id;
@@ -141,6 +169,12 @@ const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
[name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, req.params.id]
);
if (shouldUpdatePassword) {
const passwordHash = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, req.params.id]);
await pool.query('DELETE FROM password_resets WHERE email = ?', [finalEmail]);
}
if (finalTeamId && finalTeamId !== existing[0].team_id && existing[0].status === 'active') {
const [team] = await pool.query('SELECT name FROM teams WHERE id = ?', [finalTeamId]);
if (team.length > 0) {

View File

@@ -6,6 +6,7 @@ const {
canManageUserStatus,
canChangeUserEmail,
canManageUserRoleOrTeam,
canManageUserPassword,
canReadAttendance,
} = require('../policies/accessPolicy');
@@ -48,6 +49,13 @@ test('only admins can manage role, team, and status fields', () => {
assert.equal(canChangeUserEmail(manager, agent), false);
});
test('only admins can manage user passwords', () => {
assert.equal(canManageUserPassword(superAdmin), true);
assert.equal(canManageUserPassword(admin), true);
assert.equal(canManageUserPassword(manager), false);
assert.equal(canManageUserPassword(agent), false);
});
test('attendance detail policy matches role boundaries', () => {
const ownAttendance = { id: 'att_1', tenant_id: 'tenant_a', user_id: 'u_agent', team_id: 'team_a' };
const teamAttendance = { id: 'att_2', tenant_id: 'tenant_a', user_id: 'u_another', team_id: 'team_a' };