Initial commit
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const { authRequired } = require('../middleware/auth');
|
||||
const { requireRole } = require('../middleware/rbac');
|
||||
const { resolveTenant } = require('../middleware/tenant');
|
||||
const { createSurveyPeriod, activateSurvey, closeSurvey } = require('../services/surveyService');
|
||||
const { importUsersFromExcel } = require('../services/importService');
|
||||
const { getParticipationSummary } = require('../services/participationService');
|
||||
const {
|
||||
listQuestions,
|
||||
exportQuestionsExcel,
|
||||
exportQuestionsTemplate,
|
||||
importQuestionsExcel,
|
||||
listQuestionSets,
|
||||
listDimensions,
|
||||
createDimension,
|
||||
createQuestionSetFromCurrent,
|
||||
createQuestionSetEmpty,
|
||||
addQuestionToSet
|
||||
} = require('../services/questionService');
|
||||
|
||||
const upload = multer();
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authRequired, requireRole('admin', 'superadmin'), resolveTenant);
|
||||
|
||||
router.get('/survey-periods', async (req, res, next) => {
|
||||
try {
|
||||
const [rows] = await req.tenantPool.query('SELECT * FROM survey_periods ORDER BY id DESC');
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/survey-periods', async (req, res, next) => {
|
||||
try {
|
||||
const { year, month, fecha_inicio, fecha_fin } = req.body;
|
||||
if (!year || !month || !fecha_inicio || !fecha_fin) return res.status(400).json({ error: 'Campos requeridos' });
|
||||
const period = await createSurveyPeriod(req.tenantPool, { year, month, fecha_inicio, fecha_fin });
|
||||
res.json(period);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/survey-periods/:id/activate', async (req, res, next) => {
|
||||
try {
|
||||
await activateSurvey(req.tenantPool, req.params.id);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/survey-periods/:id/close', async (req, res, next) => {
|
||||
try {
|
||||
const result = await closeSurvey(req.tenantPool, req.params.id);
|
||||
res.json({ ok: true, result });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/dashboard', async (req, res, next) => {
|
||||
try {
|
||||
const [[current]] = await req.tenantPool.query(
|
||||
'SELECT score, classification, participation_rate FROM survey_results WHERE scope = "global" ORDER BY survey_period_id DESC LIMIT 1'
|
||||
);
|
||||
const [dimensions] = await req.tenantPool.query(
|
||||
'SELECT d.name, r.score, r.classification FROM survey_results r JOIN dimensions d ON d.id = r.dimension_id WHERE r.scope = "dimension" AND r.survey_period_id = (SELECT MAX(id) FROM survey_periods WHERE estado = "cerrada")'
|
||||
);
|
||||
const [history] = await req.tenantPool.query(
|
||||
'SELECT sp.codigo_medicion, r.score FROM survey_results r JOIN survey_periods sp ON sp.id = r.survey_period_id WHERE r.scope = "global" ORDER BY sp.id DESC'
|
||||
);
|
||||
const participation = await getParticipationSummary(req.tenantPool);
|
||||
res.json({ current, dimensions, history, participation });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/users/import', upload.single('file'), async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ error: 'Archivo requerido' });
|
||||
const result = await importUsersFromExcel(req.file.buffer, req.tenantPool);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/questions', async (req, res, next) => {
|
||||
try {
|
||||
const rows = await listQuestions(req.tenantPool);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/questions/export', async (req, res, next) => {
|
||||
try {
|
||||
const buffer = await exportQuestionsExcel(req.tenantPool);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=\"preguntas.xlsx\"');
|
||||
res.send(buffer);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/questions/template', async (req, res, next) => {
|
||||
try {
|
||||
const buffer = await exportQuestionsTemplate();
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=\"preguntas_template.xlsx\"');
|
||||
res.send(buffer);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/questions/import', upload.single('file'), async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ error: 'Archivo requerido' });
|
||||
const setId = req.body?.set_id ? Number(req.body.set_id) : null;
|
||||
const result = await importQuestionsExcel(req.file.buffer, req.tenantPool, setId);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/question-sets', async (req, res, next) => {
|
||||
try {
|
||||
const rows = await listQuestionSets(req.tenantPool);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/question-sets', async (req, res, next) => {
|
||||
try {
|
||||
const { name, empty } = req.body;
|
||||
const result = empty ? await createQuestionSetEmpty(req.tenantPool, name) : await createQuestionSetFromCurrent(req.tenantPool, name);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/question-sets/:id', async (req, res, next) => {
|
||||
try {
|
||||
const [rows] = await req.tenantPool.query('SELECT id, code, is_active FROM question_sets WHERE id = ?', [req.params.id]);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Set no encontrado' });
|
||||
const setRow = rows[0];
|
||||
if (setRow.is_active) {
|
||||
return res.status(400).json({ error: 'No se puede eliminar un set activo' });
|
||||
}
|
||||
const [[{ used }]] = await req.tenantPool.query('SELECT COUNT(*) AS used FROM survey_periods WHERE question_set_code = ?', [setRow.code]);
|
||||
if (used > 0) {
|
||||
return res.status(400).json({ error: 'No se puede eliminar: ya tiene mediciones' });
|
||||
}
|
||||
await req.tenantPool.query('DELETE FROM question_set_questions WHERE question_set_id = ?', [setRow.id]);
|
||||
await req.tenantPool.query('DELETE FROM question_sets WHERE id = ?', [setRow.id]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/question-sets/:id/questions', async (req, res, next) => {
|
||||
try {
|
||||
const [rows] = await req.tenantPool.query(
|
||||
'SELECT qs.question_code, qs.text_snapshot, d.name AS dimension FROM question_set_questions qs JOIN dimensions d ON d.id = qs.dimension_id WHERE qs.question_set_id = ? ORDER BY qs.sort_order',
|
||||
[req.params.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/dimensions', async (req, res, next) => {
|
||||
try {
|
||||
const rows = await listDimensions(req.tenantPool);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/dimensions', async (req, res, next) => {
|
||||
try {
|
||||
const { name, code, description } = req.body;
|
||||
if (!name) return res.status(400).json({ error: 'Nombre requerido' });
|
||||
const result = await createDimension(req.tenantPool, { name, code, description });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/question-sets/:id/questions', async (req, res, next) => {
|
||||
try {
|
||||
const { dimension_id, text } = req.body;
|
||||
if (!dimension_id || !text) return res.status(400).json({ error: 'dimension_id y text requeridos' });
|
||||
const result = await addQuestionToSet(req.tenantPool, { questionSetId: req.params.id, dimensionId: dimension_id, text });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,47 @@
|
||||
const express = require('express');
|
||||
const { getCentralPool } = require('../db/central');
|
||||
const { resolveTenant } = require('../middleware/tenant');
|
||||
const { comparePassword } = require('../utils/password');
|
||||
const { signToken } = require('../utils/jwt');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/login', async (req, res, next) => {
|
||||
try {
|
||||
const { email, password, role, company_code } = req.body;
|
||||
if (!email || !password || !role) {
|
||||
return res.status(400).json({ error: 'email, password y role requeridos' });
|
||||
}
|
||||
|
||||
if (role === 'superadmin') {
|
||||
const central = getCentralPool();
|
||||
const [rows] = await central.query('SELECT * FROM superadmins WHERE email = ? AND status = "active"', [email]);
|
||||
if (!rows.length) return res.status(401).json({ error: 'Credenciales inválidas' });
|
||||
const ok = await comparePassword(password, rows[0].password_hash);
|
||||
if (!ok) return res.status(401).json({ error: 'Credenciales inválidas' });
|
||||
|
||||
const token = signToken({ id: rows[0].id, role: 'superadmin' });
|
||||
return res.json({ token, role: 'superadmin' });
|
||||
}
|
||||
|
||||
if (!company_code) return res.status(400).json({ error: 'company_code requerido' });
|
||||
|
||||
req.body.company_code = company_code;
|
||||
await resolveTenant(req, res, async (err) => {
|
||||
if (err) return next(err);
|
||||
const pool = req.tenantPool;
|
||||
const table = role === 'admin' ? 'admins' : 'users';
|
||||
const [rows] = await pool.query(`SELECT * FROM ${table} WHERE email = ? AND status = 'active'`, [email]);
|
||||
if (!rows.length) return res.status(401).json({ error: 'Credenciales inválidas' });
|
||||
const ok = await comparePassword(password, rows[0].password_hash);
|
||||
if (!ok) return res.status(401).json({ error: 'Credenciales inválidas' });
|
||||
|
||||
const token = signToken({ id: rows[0].id, role, company_code });
|
||||
return res.json({ token, role, company_code });
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,31 @@
|
||||
const express = require('express');
|
||||
const { authRequired } = require('../middleware/auth');
|
||||
const { requireRole } = require('../middleware/rbac');
|
||||
const { resolveTenant } = require('../middleware/tenant');
|
||||
const { generatePdfReport, generateExcelReport } = require('../services/reportService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/pdf/:surveyPeriodId', authRequired, requireRole('admin'), resolveTenant, async (req, res, next) => {
|
||||
try {
|
||||
const buffer = await generatePdfReport(req.tenantPool, req.params.surveyPeriodId, req.company.name);
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="reporte.pdf"');
|
||||
res.send(buffer);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/excel/:surveyPeriodId', authRequired, requireRole('admin'), resolveTenant, async (req, res, next) => {
|
||||
try {
|
||||
const buffer = await generateExcelReport(req.tenantPool, req.params.surveyPeriodId);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="reporte.xlsx"');
|
||||
res.send(buffer);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,180 @@
|
||||
const express = require('express');
|
||||
const { authRequired } = require('../middleware/auth');
|
||||
const { requireRole } = require('../middleware/rbac');
|
||||
const { getCentralPool } = require('../db/central');
|
||||
const { provisionTenant } = require('../services/tenantProvision');
|
||||
const { getTenantPool } = require('../db/tenant');
|
||||
const { hashPassword } = require('../utils/password');
|
||||
const { getParticipationSummary } = require('../services/participationService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/companies', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const central = getCentralPool();
|
||||
const [rows] = await central.query('SELECT id, name, code, status, start_date, end_date, db_host, db_port, db_name, created_at FROM companies ORDER BY id DESC');
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/companies', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const { name, code, start_date, end_date, db_name, db_host, db_port, db_user, db_pass, admin_name, admin_email, admin_password } = req.body;
|
||||
if (!name || !code || !db_name || !db_host || !db_user || !db_pass || !admin_name || !admin_email || !admin_password) {
|
||||
return res.status(400).json({ error: 'Faltan campos requeridos' });
|
||||
}
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ error: 'Fecha inicio y fin son obligatorias' });
|
||||
}
|
||||
if (start_date > end_date) {
|
||||
return res.status(400).json({ error: 'Fecha inicio no puede ser mayor que fecha fin' });
|
||||
}
|
||||
|
||||
await provisionTenant({
|
||||
name,
|
||||
code,
|
||||
dbName: db_name,
|
||||
dbHost: db_host,
|
||||
dbPort: db_port || 3306,
|
||||
dbUser: db_user,
|
||||
dbPass: db_pass,
|
||||
adminName: admin_name,
|
||||
adminEmail: admin_email,
|
||||
adminPassword: admin_password,
|
||||
startDate: start_date,
|
||||
endDate: end_date
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/companies/:id/status', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
if (!['active', 'suspended'].includes(status)) {
|
||||
return res.status(400).json({ error: 'Estado inválido' });
|
||||
}
|
||||
const central = getCentralPool();
|
||||
await central.query('UPDATE companies SET status = ? WHERE id = ?', [status, req.params.id]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/companies/:id/dates', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const { start_date, end_date } = req.body;
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ error: 'Fecha inicio y fin son obligatorias' });
|
||||
}
|
||||
if (start_date > end_date) {
|
||||
return res.status(400).json({ error: 'Fecha inicio no puede ser mayor que fecha fin' });
|
||||
}
|
||||
const central = getCentralPool();
|
||||
await central.query('UPDATE companies SET start_date = ?, end_date = ? WHERE id = ?', [
|
||||
start_date || null,
|
||||
end_date || null,
|
||||
req.params.id
|
||||
]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/companies/:id/participation', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const central = getCentralPool();
|
||||
const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]);
|
||||
if (!company) return res.status(404).json({ error: 'Empresa no encontrada' });
|
||||
const tenantPool = getTenantPool(company);
|
||||
const participation = await getParticipationSummary(tenantPool);
|
||||
res.json({ company_id: company.id, participation });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/companies/:id', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const central = getCentralPool();
|
||||
const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]);
|
||||
if (!company) return res.status(404).json({ error: 'Empresa no encontrada' });
|
||||
|
||||
const tenantPool = getTenantPool(company);
|
||||
const [[{ periods }]] = await tenantPool.query('SELECT COUNT(*) AS periods FROM survey_periods');
|
||||
const [[{ responses }]] = await tenantPool.query('SELECT COUNT(*) AS responses FROM responses');
|
||||
|
||||
if (periods > 0 || responses > 0) {
|
||||
return res.status(400).json({ error: 'No se puede eliminar: existen encuestas registradas' });
|
||||
}
|
||||
|
||||
await central.query('DROP DATABASE IF EXISTS `' + company.db_name + '`');
|
||||
await central.query('DELETE FROM companies WHERE id = ?', [company.id]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/companies/:id/admins', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const { name, email, password } = req.body;
|
||||
if (!name || !email || !password) {
|
||||
return res.status(400).json({ error: 'Nombre, email y password requeridos' });
|
||||
}
|
||||
const central = getCentralPool();
|
||||
const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]);
|
||||
if (!company) return res.status(404).json({ error: 'Empresa no encontrada' });
|
||||
|
||||
const tenantPool = getTenantPool(company);
|
||||
const passwordHash = await hashPassword(password);
|
||||
await tenantPool.query(
|
||||
'INSERT INTO admins (name, email, password_hash, role, status) VALUES (?, ?, ?, "admin", "active")',
|
||||
[name, email, passwordHash]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/companies/:id/admins', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const central = getCentralPool();
|
||||
const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]);
|
||||
if (!company) return res.status(404).json({ error: 'Empresa no encontrada' });
|
||||
|
||||
const tenantPool = getTenantPool(company);
|
||||
const [rows] = await tenantPool.query('SELECT id, name, email, status, created_at FROM admins ORDER BY id DESC');
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch('/companies/:id/admins/:adminId/status', authRequired, requireRole('superadmin'), async (req, res, next) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
if (!['active', 'inactive'].includes(status)) {
|
||||
return res.status(400).json({ error: 'Estado inválido' });
|
||||
}
|
||||
const central = getCentralPool();
|
||||
const [[company]] = await central.query('SELECT * FROM companies WHERE id = ?', [req.params.id]);
|
||||
if (!company) return res.status(404).json({ error: 'Empresa no encontrada' });
|
||||
|
||||
const tenantPool = getTenantPool(company);
|
||||
await tenantPool.query('UPDATE admins SET status = ? WHERE id = ?', [status, req.params.adminId]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,58 @@
|
||||
const express = require('express');
|
||||
const { authRequired } = require('../middleware/auth');
|
||||
const { resolveTenant } = require('../middleware/tenant');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/active', resolveTenant, async (req, res, next) => {
|
||||
try {
|
||||
const [[period]] = await req.tenantPool.query('SELECT * FROM survey_periods WHERE estado = "activa" ORDER BY id DESC LIMIT 1');
|
||||
if (!period) return res.json({ period: null, questions: [] });
|
||||
|
||||
const [questions] = await req.tenantPool.query(
|
||||
'SELECT sq.question_id, sq.question_code, sq.text_snapshot AS text, d.name AS dimension FROM survey_questions sq JOIN dimensions d ON d.id = sq.dimension_id WHERE sq.survey_period_id = ? ORDER BY sq.sort_order ASC',
|
||||
[period.id]
|
||||
);
|
||||
|
||||
res.json({ period, questions });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/responses', authRequired, resolveTenant, async (req, res, next) => {
|
||||
try {
|
||||
const { survey_period_id, answers } = req.body;
|
||||
if (!survey_period_id || !Array.isArray(answers)) {
|
||||
return res.status(400).json({ error: 'survey_period_id y answers requeridos' });
|
||||
}
|
||||
const [[{ total_questions }]] = await req.tenantPool.query(
|
||||
'SELECT COUNT(*) AS total_questions FROM survey_questions WHERE survey_period_id = ?',
|
||||
[survey_period_id]
|
||||
);
|
||||
const uniqueIds = new Set(answers.map((a) => a.question_id));
|
||||
if (uniqueIds.size !== total_questions) {
|
||||
return res.status(400).json({ error: 'Resultados incompletos' });
|
||||
}
|
||||
|
||||
for (const ans of answers) {
|
||||
if (![1,2,3,4,5].includes(Number(ans.value))) {
|
||||
return res.status(400).json({ error: 'Valor inválido' });
|
||||
}
|
||||
const [[qrow]] = await req.tenantPool.query(
|
||||
'SELECT question_code FROM survey_questions WHERE survey_period_id = ? AND question_id = ? LIMIT 1',
|
||||
[survey_period_id, ans.question_id]
|
||||
);
|
||||
await req.tenantPool.query(
|
||||
'INSERT INTO responses (survey_period_id, user_id, question_id, question_code, value) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)',
|
||||
[survey_period_id, req.user.id, ans.question_id, qrow?.question_code || '', ans.value]
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user