const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const { hashSecret, maskSecret } = require('../utils/security'); const { requireRole } = require('../middleware/auth'); const createAuthRouter = ({ pool, transporter, getBaseUrl, jwtSecret }) => { const router = express.Router(); router.post('/auth/register', async (req, res) => { const { name, email, password, organizationName } = req.body; try { 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 passwordHash = await bcrypt.hash(password, 10); const verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); await pool.query( 'INSERT INTO pending_registrations (email, password_hash, full_name, organization_name, verification_code, expires_at) VALUES (?, ?, ?, ?, ?, ?)', [email, passwordHash, name, organizationName, verificationCode, expiresAt] ); await transporter.sendMail({ from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`, to: email, subject: 'Seu código de verificação Fasto', text: `Olá ${name}, seu código de verificação é: ${verificationCode}`, html: `

Bem-vindo ao Fasto!

Seu código: ${verificationCode}

` }); res.json({ message: 'Código enviado.' }); } catch (error) { console.error('Register error:', error); res.status(500).json({ error: error.message }); } }); router.post('/auth/verify', async (req, res) => { const { email, code } = req.body; const connection = await pool.getConnection(); try { await connection.beginTransaction(); const [pending] = await connection.query( 'SELECT * FROM pending_registrations WHERE email = ? AND verification_code = ? AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1', [email, code] ); if (pending.length === 0) return res.status(400).json({ error: 'Código inválido.' }); const data = pending[0]; const tenantId = `tenant_${crypto.randomUUID().split('-')[0]}`; const userId = `u_${crypto.randomUUID().split('-')[0]}`; await connection.query('INSERT INTO tenants (id, name, slug, admin_email) VALUES (?, ?, ?, ?)', [tenantId, data.organization_name, data.organization_name.toLowerCase().replace(/ /g, '-'), email]); await connection.query('INSERT INTO users (id, tenant_id, name, email, password_hash, role) VALUES (?, ?, ?, ?, ?, ?)', [userId, tenantId, data.full_name, email, data.password_hash, 'admin']); await connection.query('DELETE FROM pending_registrations WHERE email = ?', [email]); await connection.commit(); res.json({ message: 'Sucesso.' }); } catch (error) { await connection.rollback(); res.status(500).json({ error: error.message }); } finally { connection.release(); } }); router.post('/auth/login', async (req, res) => { const { email, password } = req.body; try { const [users] = await pool.query('SELECT * FROM users WHERE email = ?', [email]); if (users.length === 0) return res.status(401).json({ error: 'Credenciais inválidas.' }); const user = users[0]; if (user.status !== 'active') { return res.status(403).json({ error: 'Sua conta está inativa. Contate o administrador.' }); } const valid = await bcrypt.compare(password, user.password_hash); if (!valid) return res.status(401).json({ error: 'Credenciais inválidas.' }); const token = jwt.sign({ id: user.id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, jwtSecret, { expiresIn: '15m' }); const refreshToken = crypto.randomBytes(40).toString('hex'); const refreshId = `rt_${crypto.randomUUID().split('-')[0]}`; await pool.query( 'INSERT INTO refresh_tokens (id, user_id, token, token_hash, expires_at) VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))', [refreshId, user.id, maskSecret(refreshId, refreshToken), hashSecret(refreshToken)] ); res.json({ token, refreshToken, user: { id: user.id, name: user.name, email: user.email, role: user.role, tenant_id: user.tenant_id, team_id: user.team_id, slug: user.slug } }); } catch (error) { res.status(500).json({ error: error.message }); } }); router.post('/auth/refresh', async (req, res) => { const { refreshToken } = req.body; if (!refreshToken) return res.status(400).json({ error: 'Refresh token não fornecido.' }); try { const [tokens] = await pool.query( 'SELECT r.user_id, u.tenant_id, u.role, u.team_id, u.slug, u.status FROM refresh_tokens r JOIN users u ON r.user_id = u.id WHERE (r.token_hash = ? OR r.token = ?) AND r.expires_at > NOW()', [hashSecret(refreshToken), refreshToken] ); if (tokens.length === 0) { await pool.query('DELETE FROM refresh_tokens WHERE token_hash = ? OR token = ?', [hashSecret(refreshToken), refreshToken]); return res.status(401).json({ error: 'Sessão expirada. Faça login novamente.' }); } const user = tokens[0]; if (user.status !== 'active') { await pool.query('DELETE FROM refresh_tokens WHERE token_hash = ? OR token = ?', [hashSecret(refreshToken), refreshToken]); return res.status(403).json({ error: 'Sua conta está inativa.' }); } await pool.query('UPDATE refresh_tokens SET expires_at = DATE_ADD(NOW(), INTERVAL 30 DAY) WHERE token_hash = ? OR token = ?', [hashSecret(refreshToken), refreshToken]); const newAccessToken = jwt.sign( { id: user.user_id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, jwtSecret, { expiresIn: '15m' } ); res.json({ token: newAccessToken }); } catch (error) { res.status(500).json({ error: error.message }); } }); router.post('/auth/logout', async (req, res) => { const { refreshToken } = req.body; try { if (refreshToken) { await pool.query('DELETE FROM refresh_tokens WHERE token_hash = ? OR token = ?', [hashSecret(refreshToken), refreshToken]); } res.json({ message: 'Logout bem-sucedido.' }); } catch (error) { res.status(500).json({ error: error.message }); } }); router.post('/impersonate/:tenantId', requireRole(['super_admin']), async (req, res) => { try { const [users] = await pool.query("SELECT * FROM users WHERE tenant_id = ? AND role = 'admin' LIMIT 1", [req.params.tenantId]); if (users.length === 0) { return res.status(404).json({ error: 'Nenhum administrador encontrado nesta organização para assumir a identidade.' }); } const user = users[0]; if (user.status !== 'active') { return res.status(403).json({ error: 'A conta do admin desta organização está inativa.' }); } const token = jwt.sign({ id: user.id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, jwtSecret, { expiresIn: '2h' }); res.json({ token, user: { id: user.id, name: user.name, email: user.email, role: user.role, tenant_id: user.tenant_id, team_id: user.team_id, slug: user.slug } }); } catch (error) { res.status(500).json({ error: error.message }); } }); router.post('/auth/forgot-password', async (req, res) => { const { email } = req.body; try { const [users] = await pool.query('SELECT name FROM users WHERE email = ?', [email]); if (users.length > 0) { 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]); const link = `${getBaseUrl(req)}/#/reset-password?token=${token}`; await transporter.sendMail({ from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`, to: email, subject: 'Recuperação de Senha - Fasto', html: `

Olá, ${users[0].name}!

Você solicitou a recuperação de senha da sua conta no Fasto.

Clique no botão abaixo para criar uma nova senha:

Redefinir Minha Senha

Este link expira em 15 minutos. Se você não solicitou isso, pode ignorar este e-mail.

Desenvolvido por Blyzer

` }); } res.json({ message: 'Se o e-mail existir, enviamos as instruções.' }); } catch (error) { console.error('Forgot password error:', error); res.status(500).json({ error: error.message }); } }); router.post('/auth/reset-password', async (req, res) => { const { token, password, name } = req.body; try { const [resets] = await pool.query('SELECT email FROM password_resets WHERE token = ? AND expires_at > NOW()', [token]); if (resets.length === 0) return res.status(400).json({ error: 'Token inválido ou expirado.' }); const hash = await bcrypt.hash(password, 10); if (name) { await pool.query('UPDATE users SET password_hash = ?, name = ? WHERE email = ?', [hash, name, resets[0].email]); const [u] = await pool.query('SELECT id, name, tenant_id, role, team_id FROM users WHERE email = ?', [resets[0].email]); if (u.length > 0) { const user = u[0]; const [notifiable] = await pool.query( "SELECT id FROM users WHERE tenant_id = ? AND role IN ('admin', 'manager', 'super_admin') AND id != ?", [user.tenant_id, user.id] ); for (const n of notifiable) { await pool.query( 'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)', [crypto.randomUUID(), n.id, 'info', 'Novo Membro Ativo', `${name} concluiu o cadastro e já pode acessar o sistema.`, `/users/${user.id}`] ); } if (user.role === 'admin') { const [superAdmins] = await pool.query("SELECT id FROM users WHERE role = 'super_admin'"); for (const sa of superAdmins) { await pool.query( 'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)', [crypto.randomUUID(), sa.id, 'success', 'Admin Ativo', `O admin ${name} da organização configurou sua conta.`, `/super-admin`] ); } } } } else { await pool.query('UPDATE users SET password_hash = ? WHERE email = ?', [hash, resets[0].email]); } await pool.query('DELETE FROM password_resets WHERE email = ?', [resets[0].email]); res.json({ message: 'Senha e perfil atualizados com sucesso.' }); } catch (error) { res.status(500).json({ error: error.message }); } }); return router; }; module.exports = { createAuthRouter };