Extract backend tenant routes
This commit is contained in:
@@ -14,6 +14,7 @@ const { authenticateToken, requireRole } = require('./middleware/auth');
|
|||||||
const { createAuthRouter } = require('./routes/authRoutes');
|
const { createAuthRouter } = require('./routes/authRoutes');
|
||||||
const { createUsersRouter } = require('./routes/usersRoutes');
|
const { createUsersRouter } = require('./routes/usersRoutes');
|
||||||
const { createTeamsRouter } = require('./routes/teamsRoutes');
|
const { createTeamsRouter } = require('./routes/teamsRoutes');
|
||||||
|
const { createTenantsRouter } = require('./routes/tenantsRoutes');
|
||||||
const { canReadAttendance } = require('./policies/accessPolicy');
|
const { canReadAttendance } = require('./policies/accessPolicy');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -75,6 +76,8 @@ apiRouter.use(createUsersRouter({ pool, upload, transporter, getBaseUrl }));
|
|||||||
|
|
||||||
apiRouter.use(createTeamsRouter({ pool }));
|
apiRouter.use(createTeamsRouter({ pool }));
|
||||||
|
|
||||||
|
apiRouter.use(createTenantsRouter({ pool, transporter, getBaseUrl }));
|
||||||
|
|
||||||
// --- Notifications Routes ---
|
// --- Notifications Routes ---
|
||||||
apiRouter.get('/notifications', async (req, res) => {
|
apiRouter.get('/notifications', async (req, res) => {
|
||||||
try {
|
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: `
|
|
||||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 12px; background: #ffffff; color: #0f172a;">
|
|
||||||
<h2 style="color: #0f172a;">Sua organização foi criada</h2>
|
|
||||||
<p style="color: #475569;">Você foi definido como administrador da organização <strong>${name}</strong>.</p>
|
|
||||||
<p style="color: #475569;">Por favor, clique no botão abaixo para definir sua senha e concluir seu cadastro.</p>
|
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
|
||||||
<a href="${setupLink}" style="background-color: #0f172a; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">Finalizar Cadastro</a>
|
|
||||||
</div>
|
|
||||||
<p style="font-size: 12px; color: #94a3b8;">Este link expira em 15 minutos.</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}).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
|
// Mount the API Router
|
||||||
app.use('/api', apiRouter);
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
|
|||||||
99
backend/routes/tenantsRoutes.js
Normal file
99
backend/routes/tenantsRoutes.js
Normal file
@@ -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: `
|
||||||
|
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 12px; background: #ffffff; color: #0f172a;">
|
||||||
|
<h2 style="color: #0f172a;">Sua organização foi criada</h2>
|
||||||
|
<p style="color: #475569;">Você foi definido como administrador da organização <strong>${name}</strong>.</p>
|
||||||
|
<p style="color: #475569;">Por favor, clique no botão abaixo para definir sua senha e concluir seu cadastro.</p>
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="${setupLink}" style="background-color: #0f172a; color: white; padding: 12px 24px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block;">Finalizar Cadastro</a>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 12px; color: #94a3b8;">Este link expira em 15 minutos.</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}).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 };
|
||||||
Reference in New Issue
Block a user