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:
Este link expira em 15 minutos. Se você não solicitou isso, pode ignorar este e-mail.
`
});
}
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 };