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 [smtp, setSmtp] = useState<SmtpConfig>({ host: '', port: '587', user: '', pass: '', from: '' });
|
||||||
const [showSmtpPass, setShowSmtpPass] = useState(false);
|
const [showSmtpPass, setShowSmtpPass] = useState(false);
|
||||||
const [savingSmtp, setSavingSmtp] = 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(() => {
|
const load = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -77,13 +80,15 @@ export default function SettingsPage() {
|
|||||||
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies'),
|
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies'),
|
||||||
api.get<SmtpConfig | null>('/settings/smtp').catch(() => null),
|
api.get<SmtpConfig | null>('/settings/smtp').catch(() => null),
|
||||||
api.get<{ connected: boolean }>('/verifik/status').catch(() => ({ connected: false })),
|
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 || {});
|
setGlobal(globalData || {});
|
||||||
setMapsConfigured(mapsData.configured);
|
setMapsConfigured(mapsData.configured);
|
||||||
setPolicies({ privacy: policiesData.privacy || '', terms: policiesData.terms || '' });
|
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 || '' });
|
if (smtpData) setSmtp({ host: smtpData.host || '', port: smtpData.port || '587', user: smtpData.user || '', pass: smtpData.pass || '', from: smtpData.from || '' });
|
||||||
setVerifikConnected(verifikStatus.connected);
|
setVerifikConnected(verifikStatus.connected);
|
||||||
|
setMailLogs(logsData.data || []);
|
||||||
})
|
})
|
||||||
.catch(() => toast.error('Error al cargar configuración'))
|
.catch(() => toast.error('Error al cargar configuración'))
|
||||||
.finally(() => setLoading(false));
|
.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 () => {
|
const sendVerifikOtp = async () => {
|
||||||
if (!verifikEmail.trim()) return toast.error('Ingresa tu email de Verifik');
|
if (!verifikEmail.trim()) return toast.error('Ingresa tu email de Verifik');
|
||||||
setVerifikLoading(true);
|
setVerifikLoading(true);
|
||||||
@@ -469,6 +488,70 @@ export default function SettingsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</section>
|
</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 ── */}
|
{/* ── Verifik ── */}
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">Verifik — Validación de profesionales</h2>
|
<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 { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { SettingsService } from './settings.service';
|
import { SettingsService } from './settings.service';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { EmailOtpService } from '../auth/email-otp.service';
|
import { EmailOtpService } from '../auth/email-otp.service';
|
||||||
|
import { MailService } from '../mail/mail.service';
|
||||||
|
|
||||||
@ApiTags('Settings')
|
@ApiTags('Settings')
|
||||||
@Controller('settings')
|
@Controller('settings')
|
||||||
export class SettingsController {
|
export class SettingsController {
|
||||||
constructor(private settings: SettingsService, private emailOtp: EmailOtpService) {}
|
constructor(private settings: SettingsService, private emailOtp: EmailOtpService, private mail: MailService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getGlobal() { return this.settings.getGlobal(); }
|
getGlobal() { return this.settings.getGlobal(); }
|
||||||
@@ -44,6 +45,21 @@ export class SettingsController {
|
|||||||
return this.emailOtp.saveSmtpConfig(body);
|
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
|
// Message logs
|
||||||
@Get('message-logs')
|
@Get('message-logs')
|
||||||
@UseGuards(JwtAuthGuard) @ApiBearerAuth()
|
@UseGuards(JwtAuthGuard) @ApiBearerAuth()
|
||||||
|
|||||||
Reference in New Issue
Block a user