263 lines
12 KiB
JavaScript
263 lines
12 KiB
JavaScript
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: `
|
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 12px;">
|
|
<h2 style="color: #0f172a;">Bem-vindo ao Fasto!</h2>
|
|
<p style="color: #475569;">Seu código: <strong>${verificationCode}</strong></p>
|
|
</div>`
|
|
});
|
|
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: `
|
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 12px;">
|
|
<h2 style="color: #0f172a;">Olá, ${users[0].name}!</h2>
|
|
<p style="color: #475569;">Você solicitou a recuperação de senha da sua conta no Fasto.</p>
|
|
<p style="color: #475569;">Clique no botão abaixo para criar uma nova senha:</p>
|
|
<div style="text-align: center; margin: 30px 0;">
|
|
<a href="${link}" style="background-color: #0f172a; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">Redefinir Minha Senha</a>
|
|
</div>
|
|
<p style="font-size: 12px; color: #94a3b8;">Este link expira em 15 minutos. Se você não solicitou isso, pode ignorar 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.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 };
|