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:
co-authored by
Claude Sonnet 4.6
parent
b0ca6804fa
commit
1f3efb50da
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user