Extract backend user routes

This commit is contained in:
Cauê Faleiros
2026-05-29 10:54:01 -03:00
parent 7b39c57008
commit 40c6587312
2 changed files with 201 additions and 196 deletions

View File

@@ -12,17 +12,10 @@ const { createCorsMiddleware } = require('./config/cors');
const { allowedOrigins, getBaseUrl, getStartupBaseUrl, isProduction, jwtSecret: JWT_SECRET, port: PORT } = require('./config/runtime');
const { authenticateToken, requireRole } = require('./middleware/auth');
const { createAuthRouter } = require('./routes/authRoutes');
const {
canReadUser,
canUpdateUser,
canManageUserStatus,
canChangeUserEmail,
canManageUserRoleOrTeam,
canReadAttendance,
} = require('./policies/accessPolicy');
const { createUsersRouter } = require('./routes/usersRoutes');
const { canReadAttendance } = require('./policies/accessPolicy');
const app = express();
const USER_PUBLIC_FIELDS = 'id, tenant_id, team_id, name, email, slug, role, status, bio, avatar_url, sound_enabled, created_at';
app.use(createCorsMiddleware({ allowedOrigins, isProduction }));
app.use(express.json());
@@ -77,193 +70,7 @@ apiRouter.use(authenticateToken);
apiRouter.use(createAuthRouter({ pool, transporter, getBaseUrl, jwtSecret: JWT_SECRET }));
// --- User Routes ---
apiRouter.get('/users', async (req, res) => {
try {
const { tenantId } = req.query;
const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id;
let q = `SELECT ${USER_PUBLIC_FIELDS} FROM users`;
const params = [];
if (effectiveTenantId && effectiveTenantId !== 'all') {
q += ' WHERE tenant_id = ?';
params.push(effectiveTenantId);
}
// Strict RBAC: Managers can only see users in their own team, or themselves if they don't have a team yet
if (req.user.role === 'manager') {
if (req.user.team_id) {
q += (params.length > 0 ? ' AND' : ' WHERE') + ' (team_id = ? OR id = ?)';
params.push(req.user.team_id, req.user.id);
} else {
q += (params.length > 0 ? ' AND' : ' WHERE') + ' id = ?';
params.push(req.user.id);
}
}
const [rows] = await pool.query(q, params);
res.json(rows);
} catch (error) { res.status(500).json({ error: error.message }); }
});
apiRouter.get('/users/:idOrSlug', async (req, res) => {
try {
const [rows] = await pool.query(`SELECT ${USER_PUBLIC_FIELDS} FROM users WHERE id = ? OR slug = ?`, [req.params.idOrSlug, req.params.idOrSlug]);
if (!rows || rows.length === 0) return res.status(404).json({ error: 'Not found' });
if (!req.user || !req.user.role) return res.status(401).json({ error: 'Não autenticado' });
if (!canReadUser(req.user, rows[0])) return res.status(403).json({ error: 'Acesso negado.' });
res.json(rows[0]);
} catch (error) {
console.error('Error in GET /users/:idOrSlug:', error);
res.status(500).json({ error: error.message });
}
});
// Convidar Novo Membro (Admin criando usuário)
apiRouter.post('/users', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
const { name, email, role, team_id, tenant_id } = req.body;
const effectiveTenantId = req.user.role === 'super_admin' ? tenant_id : req.user.tenant_id;
// Strict RBAC: Managers can only create agents and assign them to their own team
let finalRole = role || 'agent';
let finalTeamId = team_id || null;
if (req.user.role === 'manager') {
if (!req.user.team_id) return res.status(403).json({ error: 'Gerente sem time não pode criar membros.' });
finalRole = 'agent'; // Force manager creations to be agents
finalTeamId = req.user.team_id; // Force assignment to manager's team
}
try {
// 1. Verificar se e-mail já existe
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'; // Usuário não pode logar com isso
// 2. Criar Usuário
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']
);
// 3. Gerar Token de Setup de Senha (reusando lógica de reset)
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))',
[email, token]
);
// 4. Enviar E-mail de Boas-vindas
const setupLink = `${getBaseUrl(req)}/#/reset-password?token=${token}`;
await transporter.sendMail({
from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`,
to: email,
subject: 'Bem-vindo ao Fasto - Finalize seu cadastro',
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 12px; background: #ffffff; color: #0f172a;">
<h2 style="color: #0f172a;">Olá, ${name}!</h2>
<p style="color: #475569;">Você foi convidado para participar da equipe no Fasto.</p>
<p style="color: #475569;">Clique no botão abaixo para definir sua senha e acessar sua conta:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${setupLink}" style="background-color: #0f172a; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">Finalizar Cadastro</a>
</div>
<p style="font-size: 12px; color: #94a3b8;">Este link expira em 15 minutos. Se você não esperava este convite, ignore este e-mail.</p>
<div style="border-top: 1px solid #f1f5f9; margin-top: 20px; padding-top: 20px; text-align: center;">
<p style="font-size: 12px; color: #94a3b8;">Desenvolvido por <a href="https://blyzer.com.br" style="color: #3b82f6; text-decoration: none;">Blyzer</a></p>
</div>
</div>
`
});
res.status(201).json({ id: uid, message: 'Convite enviado com sucesso.' });
} catch (error) {
console.error('Invite error:', error);
res.status(500).json({ error: error.message });
}
});
apiRouter.put('/users/:id', async (req, res) => {
const { name, bio, role, team_id, status, email, sound_enabled } = req.body;
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.' });
// Only Admins can change roles and teams. Managers can only edit basic info of their team members.
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;
const finalStatus = canManageUserStatus(req.user) && status !== undefined ? status : existing[0].status;
const finalEmail = canChangeUserEmail(req.user, existing[0]) && email !== undefined ? email : existing[0].email;
const finalSoundEnabled = req.user.id === req.params.id && sound_enabled !== undefined ? sound_enabled : (existing[0].sound_enabled ?? true);
if (finalEmail !== existing[0].email) {
const [emailCheck] = await pool.query('SELECT id FROM users WHERE email = ? AND id != ?', [finalEmail, req.params.id]);
if (emailCheck.length > 0) return res.status(400).json({ error: 'E-mail já está em uso.' });
}
await pool.query(
'UPDATE users SET name = ?, bio = ?, email = ?, role = ?, team_id = ?, status = ?, sound_enabled = ? WHERE id = ?',
[name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, finalSoundEnabled, req.params.id]
);
// Trigger Notification for Team Change
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) {
await pool.query(
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
[crypto.randomUUID(), req.params.id, 'info', 'Novo Time', `Você foi adicionado ao time ${team[0].name}.`, '/']
);
}
}
res.json({ message: 'User updated successfully.' });
} catch (error) { console.error('Update user error:', error);
res.status(500).json({ error: error.message });
}
});
apiRouter.delete('/users/:id', requireRole(['admin', 'super_admin']), async (req, res) => {
try {
const [existing] = await pool.query('SELECT tenant_id FROM users WHERE id = ?', [req.params.id]);
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
if (req.user.role !== 'super_admin' && existing[0].tenant_id !== req.user.tenant_id) {
return res.status(403).json({ error: 'Acesso negado.' });
}
await pool.query('DELETE FROM users WHERE id = ?', [req.params.id]);
res.json({ message: 'User deleted successfully.' });
} catch (error) {
console.error('Delete user error:', error);
res.status(500).json({ error: error.message });
}
});
// Upload de Avatar
apiRouter.post('/users/:id/avatar', upload.single('avatar'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
// Validar se o usuário está alterando o próprio avatar (ou super_admin)
if (req.user.id !== req.params.id && req.user.role !== 'super_admin') {
return res.status(403).json({ error: 'Acesso negado.' });
}
const avatarUrl = `/uploads/${req.file.filename}`;
await pool.query('UPDATE users SET avatar_url = ? WHERE id = ?', [avatarUrl, req.params.id]);
res.json({ avatarUrl });
} catch (error) {
console.error('Avatar upload error:', error);
res.status(500).json({ error: error.message });
}
});
apiRouter.use(createUsersRouter({ pool, upload, transporter, getBaseUrl }));
// --- Notifications Routes ---
apiRouter.get('/notifications', async (req, res) => {