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
+12
View File
@@ -53,6 +53,12 @@ export const messages = {
smtp_pass: 'Clave',
smtp_pass_configured_hint: 'Configurada — dejar en blanco para no cambiarla',
smtp_not_configured: 'Sin configurar: los correos se registran en el log del servidor (modo dev), no se envian de verdad.',
smtp_test_title: 'Probar configuracion',
smtp_test_hint: 'Envia un correo de verdad con los datos de arriba (guardados o no) para confirmar que funcionan antes de confiar en ellos.',
smtp_test_to: 'Enviar prueba a',
smtp_test_send: 'Enviar prueba',
smtp_test_sending: 'Enviando...',
smtp_test_success: 'Correo de prueba enviado correctamente a',
common_new: 'Nuevo',
common_edit: 'Editar',
common_save: 'Guardar',
@@ -230,6 +236,12 @@ export const messages = {
smtp_pass: 'Password',
smtp_pass_configured_hint: 'Configured — leave blank to keep it unchanged',
smtp_not_configured: 'Not configured: emails are logged on the server (dev mode), not actually sent.',
smtp_test_title: 'Test configuration',
smtp_test_hint: 'Sends a real email with the settings above (saved or not) so you can confirm they work before trusting them.',
smtp_test_to: 'Send test to',
smtp_test_send: 'Send test',
smtp_test_sending: 'Sending...',
smtp_test_success: 'Test email sent successfully to',
common_new: 'New',
common_edit: 'Edit',
common_save: 'Save',
@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import { Save } from 'lucide-react';
import { Save, Send } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import { getSmtpConfig, updateSmtpConfig } from '../../services/settings.js';
import { getSmtpConfig, updateSmtpConfig, testSmtpConfig } from '../../services/settings.js';
import { Field, FieldRow } from '../../components/Field.jsx';
import { Button } from '../../components/Button.jsx';
import { InfoBar } from '../../components/InfoBar.jsx';
@@ -12,6 +12,9 @@ export default function SmtpSettingsPanel() {
const runAction = useAction();
const { data: config, reload } = useAsync(getSmtpConfig, []);
const [form, setForm] = useState({ host: '', port: 587, user: '', from: '', pass: '' });
const [testTo, setTestTo] = useState('');
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState(null); // { ok: bool, message: string }
useEffect(() => {
if (config) setForm({ host: config.host, port: config.port, user: config.user, from: config.from, pass: '' });
@@ -26,8 +29,23 @@ export default function SmtpSettingsPanel() {
});
}
async function sendTest() {
setTestResult(null);
setTesting(true);
try {
await testSmtpConfig({ ...form, to: testTo });
setTestResult({ ok: true, message: `${t('smtp_test_success')} ${testTo}` });
} catch (err) {
setTestResult({ ok: false, message: err.message });
} finally {
setTesting(false);
}
}
if (!config) return <div className="text-muted">{t('common_loading')}</div>;
const canTest = form.host && form.user && testTo && !testing;
return (
<div>
{!config.configured && <InfoBar tone="warning" dismissible={false}>{t('smtp_not_configured')}</InfoBar>}
@@ -51,6 +69,22 @@ export default function SmtpSettingsPanel() {
<input className="input" type="password" value={form.pass} onChange={(e) => setForm((f) => ({ ...f, pass: e.target.value }))} />
</Field>
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
<div className="panel-card" style={{ marginTop: 16 }}>
<h4 style={{ marginTop: 0 }}>{t('smtp_test_title')}</h4>
<div className="text-muted" style={{ fontSize: '.76rem', marginBottom: 10 }}>{t('smtp_test_hint')}</div>
<FieldRow>
<Field label={t('smtp_test_to')}>
<input className="input" type="email" value={testTo} onChange={(e) => setTestTo(e.target.value)} placeholder="tucorreo@linkedcorp.net" />
</Field>
</FieldRow>
{testResult && (
<InfoBar tone={testResult.ok ? 'success' : 'error'}>{testResult.message}</InfoBar>
)}
<Button variant="standard" icon={Send} onClick={sendTest} disabled={!canTest}>
{testing ? t('smtp_test_sending') : t('smtp_test_send')}
</Button>
</div>
</div>
);
}
+1
View File
@@ -2,3 +2,4 @@ import { api } from './api.js';
export const getSmtpConfig = () => api.get('/settings/smtp');
export const updateSmtpConfig = (data) => api.put('/settings/smtp', data);
export const testSmtpConfig = (data) => api.post('/settings/smtp/test', data);