From 0f322e9c8562e315f1cf96a07077003e2dcf8a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 29 May 2026 11:00:16 -0300 Subject: [PATCH] Extract backend tenant routes --- backend/index.js | 86 +--------------------------- backend/routes/tenantsRoutes.js | 99 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 83 deletions(-) create mode 100644 backend/routes/tenantsRoutes.js diff --git a/backend/index.js b/backend/index.js index 9b84e01..d4d7777 100644 --- a/backend/index.js +++ b/backend/index.js @@ -14,6 +14,7 @@ const { authenticateToken, requireRole } = require('./middleware/auth'); const { createAuthRouter } = require('./routes/authRoutes'); const { createUsersRouter } = require('./routes/usersRoutes'); const { createTeamsRouter } = require('./routes/teamsRoutes'); +const { createTenantsRouter } = require('./routes/tenantsRoutes'); const { canReadAttendance } = require('./policies/accessPolicy'); const app = express(); @@ -75,6 +76,8 @@ apiRouter.use(createUsersRouter({ pool, upload, transporter, getBaseUrl })); apiRouter.use(createTeamsRouter({ pool })); +apiRouter.use(createTenantsRouter({ pool, transporter, getBaseUrl })); + // --- Notifications Routes --- apiRouter.get('/notifications', async (req, res) => { try { @@ -730,89 +733,6 @@ apiRouter.post('/integration/attendances', requireRole(['admin']), async (req, r } }); -// --- Tenant Routes --- -apiRouter.get('/tenants', requireRole(['super_admin']), async (req, res) => { - try { - const q = 'SELECT t.*, (SELECT COUNT(*) FROM users u WHERE u.tenant_id = t.id) as user_count, (SELECT COUNT(*) FROM attendances a WHERE a.tenant_id = t.id) as attendance_count FROM tenants t'; - const [rows] = await pool.query(q); - res.json(rows); - } catch (error) { res.status(500).json({ error: error.message }); } -}); - -apiRouter.post('/tenants', requireRole(['super_admin']), async (req, res) => { - const { name, slug, admin_email, status } = req.body; - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - const tid = `tenant_${crypto.randomUUID().split('-')[0]}`; - await connection.query('INSERT INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', [tid, name, slug, admin_email, status || 'active']); - - // Check if user already exists - const [existingUser] = await connection.query('SELECT id FROM users WHERE email = ?', [admin_email]); - if (existingUser.length === 0) { - const uid = `u_${crypto.randomUUID().split('-')[0]}`; - const userSlug = `admin-${crypto.randomBytes(4).toString('hex')}`; - const placeholderHash = 'pending_setup'; - await connection.query('INSERT INTO users (id, tenant_id, name, email, password_hash, slug, role) VALUES (?, ?, ?, ?, ?, ?, ?)', [uid, tid, 'Admin', admin_email, placeholderHash, userSlug, 'admin']); - - const token = crypto.randomBytes(32).toString('hex'); - // Adicionar aos pending_registrations em vez de criar um usuário direto se quisermos que eles completem o cadastro. - // Ou, se quisermos apenas definir a senha, mantemos o password_resets. - // O usuário quer "like a register", então vamos enviar para uma página onde eles definem a senha. - await connection.query('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))', [admin_email, token]); - - const setupLink = `${getBaseUrl(req)}/#/setup-account?token=${token}`; - - // Add Notification for Super Admins - const [superAdmins] = await connection.query("SELECT id FROM users WHERE role = 'super_admin'"); - for (const sa of superAdmins) { - await connection.query( - 'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)', - [crypto.randomUUID(), sa.id, 'success', 'Nova Organização', `A organização ${name} foi criada.`, '/super-admin'] - ); - } - - await transporter.sendMail({ - from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`, - to: admin_email, - subject: 'Bem-vindo ao Fasto - Conclua seu cadastro de Admin', - html: ` -
-

Sua organização foi criada

-

Você foi definido como administrador da organização ${name}.

-

Por favor, clique no botão abaixo para definir sua senha e concluir seu cadastro.

-
- Finalizar Cadastro -
-

Este link expira em 15 minutos.

-
- ` - }).catch(err => console.error("Email failed:", err)); - } - - await connection.commit(); - res.status(201).json({ id: tid, message: 'Organização criada e convite enviado por e-mail.' }); - } catch (error) { await connection.rollback(); res.status(500).json({ error: error.message }); } finally { connection.release(); } -}); - -apiRouter.put('/tenants/:id', requireRole(['super_admin']), async (req, res) => { - const { name, slug, admin_email, status } = req.body; - try { - await pool.query( - 'UPDATE tenants SET name = ?, slug = ?, admin_email = ?, status = ? WHERE id = ?', - [name, slug || null, admin_email, status, req.params.id] - ); - res.json({ message: 'Tenant updated successfully.' }); - } catch (error) { res.status(500).json({ error: error.message }); } -}); - -apiRouter.delete('/tenants/:id', requireRole(['super_admin']), async (req, res) => { - try { - await pool.query('DELETE FROM tenants WHERE id = ?', [req.params.id]); - res.json({ message: 'Tenant deleted successfully.' }); - } catch (error) { res.status(500).json({ error: error.message }); } -}); - // Mount the API Router app.use('/api', apiRouter); diff --git a/backend/routes/tenantsRoutes.js b/backend/routes/tenantsRoutes.js new file mode 100644 index 0000000..edb61e1 --- /dev/null +++ b/backend/routes/tenantsRoutes.js @@ -0,0 +1,99 @@ +const express = require('express'); +const crypto = require('crypto'); +const { requireRole } = require('../middleware/auth'); + +const createTenantsRouter = ({ pool, transporter, getBaseUrl }) => { + const router = express.Router(); + + router.get('/tenants', requireRole(['super_admin']), async (req, res) => { + try { + const q = 'SELECT t.*, (SELECT COUNT(*) FROM users u WHERE u.tenant_id = t.id) as user_count, (SELECT COUNT(*) FROM attendances a WHERE a.tenant_id = t.id) as attendance_count FROM tenants t'; + const [rows] = await pool.query(q); + res.json(rows); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.post('/tenants', requireRole(['super_admin']), async (req, res) => { + const { name, slug, admin_email, status } = req.body; + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + const tid = `tenant_${crypto.randomUUID().split('-')[0]}`; + await connection.query('INSERT INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', [tid, name, slug, admin_email, status || 'active']); + + const [existingUser] = await connection.query('SELECT id FROM users WHERE email = ?', [admin_email]); + if (existingUser.length === 0) { + const uid = `u_${crypto.randomUUID().split('-')[0]}`; + const userSlug = `admin-${crypto.randomBytes(4).toString('hex')}`; + const placeholderHash = 'pending_setup'; + await connection.query('INSERT INTO users (id, tenant_id, name, email, password_hash, slug, role) VALUES (?, ?, ?, ?, ?, ?, ?)', [uid, tid, 'Admin', admin_email, placeholderHash, userSlug, 'admin']); + + const token = crypto.randomBytes(32).toString('hex'); + await connection.query('INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 15 MINUTE))', [admin_email, token]); + + const setupLink = `${getBaseUrl(req)}/#/setup-account?token=${token}`; + + const [superAdmins] = await connection.query("SELECT id FROM users WHERE role = 'super_admin'"); + for (const sa of superAdmins) { + await connection.query( + 'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)', + [crypto.randomUUID(), sa.id, 'success', 'Nova Organização', `A organização ${name} foi criada.`, '/super-admin'] + ); + } + + await transporter.sendMail({ + from: `"Fasto" <${process.env.MAIL_FROM || 'nao-responda@blyzer.com.br'}>`, + to: admin_email, + subject: 'Bem-vindo ao Fasto - Conclua seu cadastro de Admin', + html: ` +
+

Sua organização foi criada

+

Você foi definido como administrador da organização ${name}.

+

Por favor, clique no botão abaixo para definir sua senha e concluir seu cadastro.

+
+ Finalizar Cadastro +
+

Este link expira em 15 minutos.

+
+ ` + }).catch(err => console.error("Email failed:", err)); + } + + await connection.commit(); + res.status(201).json({ id: tid, message: 'Organização criada e convite enviado por e-mail.' }); + } catch (error) { + await connection.rollback(); + res.status(500).json({ error: error.message }); + } finally { + connection.release(); + } + }); + + router.put('/tenants/:id', requireRole(['super_admin']), async (req, res) => { + const { name, slug, admin_email, status } = req.body; + try { + await pool.query( + 'UPDATE tenants SET name = ?, slug = ?, admin_email = ?, status = ? WHERE id = ?', + [name, slug || null, admin_email, status, req.params.id] + ); + res.json({ message: 'Tenant updated successfully.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + router.delete('/tenants/:id', requireRole(['super_admin']), async (req, res) => { + try { + await pool.query('DELETE FROM tenants WHERE id = ?', [req.params.id]); + res.json({ message: 'Tenant deleted successfully.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } + }); + + return router; +}; + +module.exports = { createTenantsRouter };