Extract backend user routes
This commit is contained in:
199
backend/index.js
199
backend/index.js
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user