feat: boton de prueba real en Configuracion > Correo (SMTP)

Agrega POST /api/settings/smtp/test (superadmin, rate-limited) que envia un
correo real usando los valores actuales del formulario y propaga el error
real del servidor SMTP, a diferencia de sendMail() que nunca lanza. El panel
de SMTP ahora tiene un campo de destinatario y boton "Enviar prueba" con el
resultado real (exito o error), en vez de solo "Guardar" sin forma de
verificar que la configuracion funcione.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-06 13:55:34 -05:00
parent 399960e4be
commit 6fab4be39f
9 changed files with 117 additions and 4 deletions
@@ -6,3 +6,7 @@ export const getSmtp = asyncHandler(async (req, res) => ok(res, await service.ge
export const putSmtp = asyncHandler(async (req, res) => {
ok(res, await service.updateSmtpConfig(req.body, req.currentUser.id));
});
export const postTestSmtp = asyncHandler(async (req, res) => {
ok(res, await service.testSmtpConfig(req.body, req.currentUser.id));
});
+6 -1
View File
@@ -1,11 +1,16 @@
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import { requireAuth, requireRole, CAN_MANAGE_SMTP } from '../../middleware/auth.js';
import { getSmtp, putSmtp } from './controller.js';
import { getSmtp, putSmtp, postTestSmtp } from './controller.js';
const router = Router();
// Evita abusar del relay SMTP configurado para mandar correo masivo por esta via de diagnostico.
const testLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 10, standardHeaders: true, legacyHeaders: false });
router.use(requireAuth, requireRole(...CAN_MANAGE_SMTP));
router.get('/smtp', getSmtp);
router.put('/smtp', putSmtp);
router.post('/smtp/test', testLimiter, postTestSmtp);
export default router;
+41
View File
@@ -1,3 +1,4 @@
import nodemailer from 'nodemailer';
import * as repo from './repository.js';
import { encrypt, decrypt } from '../../utils/crypto.js';
import { logAudit } from '../../utils/audit.js';
@@ -42,3 +43,43 @@ export async function updateSmtpConfig(payload, actorId) {
await logAudit({ userId: actorId, action: 'update', entityType: 'settings:smtp', entityId: null });
return getSmtpConfig();
}
// Envia un correo de prueba con los valores ACTUALES del formulario (guardados o no), para poder
// verificar la configuracion antes de confiar en ella. A diferencia de utils/mailer.js (que nunca
// lanza, para no romper registro/recuperacion), esta funcion SI propaga el error real del SMTP
// (credenciales invalidas, host inalcanzable, certificado vencido, etc.) — es una accion de
// diagnostico explicita del superadmin, no un envio "de negocio".
export async function testSmtpConfig(payload, actorId) {
const { to } = payload;
if (!to) throw new HttpError(400, 'Indica un correo destino para la prueba');
if (!payload.host || !payload.user) throw new HttpError(400, 'Host y usuario son obligatorios para probar');
let pass = payload.pass;
if (!pass) {
const stored = await repo.getAll(['smtp_pass_enc']);
if (!stored.smtp_pass_enc) throw new HttpError(400, 'No hay una clave guardada ni escrita para probar');
pass = decrypt(stored.smtp_pass_enc);
}
const transporter = nodemailer.createTransport({
host: payload.host,
port: Number(payload.port || 587),
secure: false,
requireTLS: true,
auth: { user: payload.user, pass },
});
try {
await transporter.sendMail({
from: payload.from || payload.user,
to,
subject: 'Correo de prueba — project.linkedcorp.net',
html: '<p>Si estas leyendo esto, la configuracion SMTP de project.linkedcorp.net funciona correctamente.</p>',
});
} catch (err) {
throw new HttpError(400, `No se pudo enviar el correo de prueba: ${err.message}`);
}
await logAudit({ userId: actorId, action: 'test', entityType: 'settings:smtp', entityId: null, detail: { to } });
return { sent: true };
}