diff --git a/Dockerfile b/Dockerfile
index 1a94c07..a90cb55 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,6 +29,7 @@ COPY backend/db.js ./db.js
COPY backend/config ./config
COPY backend/middleware ./middleware
COPY backend/policies ./policies
+COPY backend/routes ./routes
COPY backend/services ./services
COPY backend/utils ./utils
diff --git a/backend/index.js b/backend/index.js
index b8dd99c..686e4b6 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -1,8 +1,6 @@
require('dotenv').config();
const express = require('express');
const path = require('path');
-const bcrypt = require('bcryptjs');
-const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const multer = require('multer');
const { v4: uuidv4 } = require('uuid');
@@ -13,6 +11,7 @@ const transporter = require('./services/mailer');
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,
@@ -76,277 +75,7 @@ apiRouter.use(authenticateToken);
// --- Auth Routes ---
-// Register
-apiRouter.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]
- );
-
- const mailOptions = {
- 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}
-
`
- };
- await transporter.sendMail(mailOptions);
- res.json({ message: 'Código enviado.' });
- } catch (error) {
- console.error('Register error:', error);
- res.status(500).json({ error: error.message });
- }
-});
-
-// Verify
-apiRouter.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();
- }
-});
-
-// Login
-apiRouter.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];
-
- // Verificar se o usuário está ativo
- 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.' });
-
- // Generate Access Token (short-lived)
- const token = jwt.sign({ id: user.id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, JWT_SECRET, { expiresIn: '15m' });
-
- // Generate Refresh Token (long-lived)
- const refreshToken = crypto.randomBytes(40).toString('hex');
- const refreshId = `rt_${crypto.randomUUID().split('-')[0]}`;
-
- // Store Refresh Token in database (expires in 30 days)
- 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 });
- }
-});
-
-// Refresh Token
-apiRouter.post('/auth/refresh', async (req, res) => {
- const { refreshToken } = req.body;
- if (!refreshToken) return res.status(400).json({ error: 'Refresh token não fornecido.' });
-
- try {
- // Verifies if the token exists and hasn't expired
- 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) {
- // If invalid, optionally delete the bad token if it's just expired
- 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.' });
- }
-
- // Sliding Expiration: Extend the refresh token's life by another 30 days
- await pool.query('UPDATE refresh_tokens SET expires_at = DATE_ADD(NOW(), INTERVAL 30 DAY) WHERE token_hash = ? OR token = ?', [hashSecret(refreshToken), refreshToken]);
-
- // Issue a new short-lived access token
- const newAccessToken = jwt.sign(
- { id: user.user_id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug },
- JWT_SECRET,
- { expiresIn: '15m' }
- );
-
- res.json({ token: newAccessToken });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-// Logout (Revoke Refresh Token)
-apiRouter.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 });
- }
-});
-
-// God Mode (Impersonate Tenant)
-apiRouter.post('/impersonate/:tenantId', requireRole(['super_admin']), async (req, res) => {
- try {
- // Buscar o primeiro admin (ou qualquer usuário) do tenant para assumir a identidade
- 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.' });
- }
-
- // Gerar um token JWT como se fôssemos o admin do tenant
- const token = jwt.sign({ id: user.id, tenant_id: user.tenant_id, role: user.role, team_id: user.team_id, slug: user.slug }, JWT_SECRET, { 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 });
- }
-});
-
-// Forgot Password
-apiRouter.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}`;
-
- const mailOptions = { 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.
-
-
- `
- };
- await transporter.sendMail(mailOptions);
- }
- 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 });
- }
-});
-
-// Reset Password
-apiRouter.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) {
- // If a name is provided (like in the initial admin setup flow), update it along with the password
- await pool.query('UPDATE users SET password_hash = ?, name = ? WHERE email = ?', [hash, name, resets[0].email]);
-
- // Notify managers/admins of the same tenant
- 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 the new user is an admin, notify super_admins too
- 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 {
- // Standard password reset, just update the hash
- 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 });
- }
-});
+apiRouter.use(createAuthRouter({ pool, transporter, getBaseUrl, jwtSecret: JWT_SECRET }));
// --- User Routes ---
apiRouter.get('/users', async (req, res) => {
diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js
new file mode 100644
index 0000000..f97ce94
--- /dev/null
+++ b/backend/routes/authRoutes.js
@@ -0,0 +1,262 @@
+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 };