Add SMTP test email endpoint and mail logs in admin settings

- POST /settings/test-email: send test email to diagnose SMTP config
- Admin settings: test email form + last 20 email logs with status/error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-29 11:20:56 -05:00
co-authored by Claude Sonnet 4.6
parent b0ca6804fa
commit 1f3efb50da
2 changed files with 102 additions and 3 deletions
+84 -1
View File
@@ -68,6 +68,9 @@ export default function SettingsPage() {
const [smtp, setSmtp] = useState<SmtpConfig>({ host: '', port: '587', user: '', pass: '', from: '' });
const [showSmtpPass, setShowSmtpPass] = useState(false);
const [savingSmtp, setSavingSmtp] = useState(false);
const [testEmailTo, setTestEmailTo] = useState('');
const [sendingTest, setSendingTest] = useState(false);
const [mailLogs, setMailLogs] = useState<{ id: string; recipient: string; body: string; status: string; error?: string; created_at: string }[]>([]);
const load = useCallback(() => {
setLoading(true);
@@ -77,13 +80,15 @@ export default function SettingsPage() {
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies'),
api.get<SmtpConfig | null>('/settings/smtp').catch(() => null),
api.get<{ connected: boolean }>('/verifik/status').catch(() => ({ connected: false })),
api.get<{ data: any[] }>('/settings/message-logs?limit=20&channel=email').catch(() => ({ data: [] })),
])
.then(([globalData, mapsData, policiesData, smtpData, verifikStatus]) => {
.then(([globalData, mapsData, policiesData, smtpData, verifikStatus, logsData]) => {
setGlobal(globalData || {});
setMapsConfigured(mapsData.configured);
setPolicies({ privacy: policiesData.privacy || '', terms: policiesData.terms || '' });
if (smtpData) setSmtp({ host: smtpData.host || '', port: smtpData.port || '587', user: smtpData.user || '', pass: smtpData.pass || '', from: smtpData.from || '' });
setVerifikConnected(verifikStatus.connected);
setMailLogs(logsData.data || []);
})
.catch(() => toast.error('Error al cargar configuración'))
.finally(() => setLoading(false));
@@ -143,6 +148,20 @@ export default function SettingsPage() {
}
};
const sendTestEmail = async () => {
if (!testEmailTo.trim()) return toast.error('Ingresa un correo destino');
setSendingTest(true);
try {
await api.post('/settings/test-email', { to: testEmailTo.trim() });
toast.success(`Correo de prueba enviado a ${testEmailTo}`);
load(); // refresh logs
} catch (e: any) {
toast.error(e?.message || 'Error al enviar correo de prueba');
} finally {
setSendingTest(false);
}
};
const sendVerifikOtp = async () => {
if (!verifikEmail.trim()) return toast.error('Ingresa tu email de Verifik');
setVerifikLoading(true);
@@ -469,6 +488,70 @@ export default function SettingsPage() {
</Card>
</section>
{/* ── Test de correo + Logs ── */}
<section className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Mail size={18} className="text-muted-foreground" />
Probar configuración SMTP
</CardTitle>
<CardDescription>Envía un correo de prueba para verificar que el servidor SMTP funciona.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Input
type="email"
placeholder="destino@ejemplo.com"
value={testEmailTo}
onChange={(e) => setTestEmailTo(e.target.value)}
className="flex-1"
/>
<Button onClick={sendTestEmail} disabled={sendingTest}>
{sendingTest ? 'Enviando...' : 'Enviar prueba'}
</Button>
</div>
{mailLogs.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">Últimos 20 correos enviados</p>
<div className="rounded-lg border overflow-hidden">
<table className="w-full text-xs">
<thead className="bg-muted/50">
<tr>
<th className="text-left px-3 py-2 font-medium">Destinatario</th>
<th className="text-left px-3 py-2 font-medium">Asunto</th>
<th className="text-left px-3 py-2 font-medium">Estado</th>
<th className="text-left px-3 py-2 font-medium">Fecha</th>
</tr>
</thead>
<tbody>
{mailLogs.map((log) => (
<tr key={log.id} className="border-t hover:bg-muted/30">
<td className="px-3 py-2 text-muted-foreground">{log.recipient}</td>
<td className="px-3 py-2 max-w-[200px] truncate">{log.body}</td>
<td className="px-3 py-2">
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
log.status === 'sent' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{log.status === 'sent' ? '✓ enviado' : '✗ error'}
</span>
{log.error && <p className="text-xs text-destructive mt-0.5 truncate max-w-[180px]" title={log.error}>{log.error}</p>}
</td>
<td className="px-3 py-2 text-muted-foreground whitespace-nowrap">
{new Date(log.created_at).toLocaleString('es-CO', { dateStyle: 'short', timeStyle: 'short' })}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</CardContent>
</Card>
</section>
{/* ── Verifik ── */}
<section className="space-y-4">
<h2 className="text-lg font-semibold">Verifik Validación de profesionales</h2>
+18 -2
View File
@@ -1,14 +1,15 @@
import { Controller, Get, Patch, Body, UseGuards, Param, Res, Query } from '@nestjs/common';
import { Controller, Get, Post, Patch, Body, UseGuards, Param, Res, Query } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { Response } from 'express';
import { SettingsService } from './settings.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { EmailOtpService } from '../auth/email-otp.service';
import { MailService } from '../mail/mail.service';
@ApiTags('Settings')
@Controller('settings')
export class SettingsController {
constructor(private settings: SettingsService, private emailOtp: EmailOtpService) {}
constructor(private settings: SettingsService, private emailOtp: EmailOtpService, private mail: MailService) {}
@Get()
getGlobal() { return this.settings.getGlobal(); }
@@ -44,6 +45,21 @@ export class SettingsController {
return this.emailOtp.saveSmtpConfig(body);
}
@Post('test-email')
@UseGuards(JwtAuthGuard) @ApiBearerAuth()
async testEmail(@Body() body: { to: string }) {
await this.mail.tryMail(
body.to,
'Correo de prueba — ProsApp',
`<div style="font-family:sans-serif;padding:32px;max-width:480px;margin:auto">
<h2 style="color:#1e293b">✅ Configuración SMTP correcta</h2>
<p style="color:#64748b">Si recibes este correo, el servidor SMTP está funcionando correctamente en ProsApp.</p>
<p style="color:#94a3b8;font-size:12px">Enviado desde el panel de administración.</p>
</div>`,
);
return { message: `Correo de prueba enviado a ${body.to}` };
}
// Message logs
@Get('message-logs')
@UseGuards(JwtAuthGuard) @ApiBearerAuth()