feature/boton-avisos
This commit is contained in:
@@ -178,7 +178,8 @@ export async function lookupAlertClientData(nodoName: string): Promise<any | nul
|
||||
pu.is_authority_client AS "ClienteAutoridad",
|
||||
pu.full_name AS "Nombre",
|
||||
pu.username AS "Usuario",
|
||||
dn.notification_email AS "CorreoNotificacion"
|
||||
dn.notification_email AS "CorreoNotificacion",
|
||||
dn.node_subnode_key AS "NodoSubNodo"
|
||||
FROM ${qUsers()} pu
|
||||
LEFT JOIN ${qNodes()} dn ON pu.database_node_id = dn.id
|
||||
WHERE pu.username = $1
|
||||
|
||||
166
src/lib/server/email-service.ts
Normal file
166
src/lib/server/email-service.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Servicio SMTP genérico. Si SMTP_HOST no está configurado, las funciones no hacen nada.
|
||||
* Porta el patrón de backend/core/mail.py de a24c a Node.js/nodemailer.
|
||||
*/
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export interface SendEmailOptions {
|
||||
toAddrs: string[];
|
||||
subject: string;
|
||||
plainBody: string;
|
||||
htmlBody?: string;
|
||||
fromDisplayName?: string;
|
||||
highImportance?: boolean;
|
||||
}
|
||||
|
||||
function buildTransporter() {
|
||||
const host = (process.env.SMTP_HOST ?? '').trim();
|
||||
if (!host) return null;
|
||||
|
||||
const port = parseInt(process.env.SMTP_PORT ?? '587', 10);
|
||||
const user = (process.env.SMTP_USER ?? '').trim();
|
||||
const pass = (process.env.SMTP_PASSWORD ?? '').trim();
|
||||
const useTls = (process.env.SMTP_USE_TLS ?? 'true').toLowerCase() !== 'false';
|
||||
|
||||
return nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
secure: port === 465,
|
||||
...(port !== 465 && { requireTLS: useTls }),
|
||||
...(user && { auth: { user, pass } })
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendSmtpEmail(opts: SendEmailOptions): Promise<void> {
|
||||
const host = (process.env.SMTP_HOST ?? '').trim();
|
||||
if (!host || opts.toAddrs.length === 0) return;
|
||||
|
||||
const fromAddr = (process.env.SMTP_FROM || process.env.SMTP_USER || '').trim();
|
||||
if (!fromAddr) {
|
||||
console.warn('[email-service] SMTP_FROM o SMTP_USER debe configurarse para enviar correo');
|
||||
return;
|
||||
}
|
||||
|
||||
const transporter = buildTransporter();
|
||||
if (!transporter) return;
|
||||
|
||||
const from = opts.fromDisplayName?.trim()
|
||||
? `"${opts.fromDisplayName.trim()}" <${fromAddr}>`
|
||||
: fromAddr;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (opts.highImportance) {
|
||||
headers['Importance'] = 'high';
|
||||
headers['X-Priority'] = '1';
|
||||
headers['X-MSMail-Priority'] = 'High';
|
||||
}
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to: opts.toAddrs.join(', '),
|
||||
subject: opts.subject,
|
||||
text: opts.plainBody,
|
||||
...(opts.htmlBody ? { html: opts.htmlBody } : {}),
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
/** Genera HTML de alerta de respaldo compatible con Gmail/Outlook (tablas, sin flex). */
|
||||
export function buildBackupAlertHtml(alerts: BackupAlertRow[]): string {
|
||||
const rows = alerts
|
||||
.map((a) => {
|
||||
const badgeColor =
|
||||
a.daysWithout === null
|
||||
? '#6b7280'
|
||||
: a.daysWithout >= 7
|
||||
? '#b91c1c'
|
||||
: a.daysWithout >= 3
|
||||
? '#b45309'
|
||||
: '#047857';
|
||||
const badgeBg =
|
||||
a.daysWithout === null
|
||||
? '#f3f4f6'
|
||||
: a.daysWithout >= 7
|
||||
? '#fef2f2'
|
||||
: a.daysWithout >= 3
|
||||
? '#fffbeb'
|
||||
: '#ecfdf5';
|
||||
const diasLabel =
|
||||
a.daysWithout === null ? 'Sin datos' : `${a.daysWithout} día${a.daysWithout !== 1 ? 's' : ''}`;
|
||||
const ultimaRest = a.last_restore_date
|
||||
? new Date(a.last_restore_date).toLocaleString('es-MX')
|
||||
: 'Nunca';
|
||||
return `
|
||||
<tr style="border-bottom:1px solid #e2e8f0;">
|
||||
<td style="padding:8px 12px;font-family:sans-serif;font-size:12px;color:#1e293b;">${escHtml(a.visible_name)}</td>
|
||||
<td style="padding:8px 12px;font-family:sans-serif;font-size:12px;color:#475569;">${escHtml(a.clientName ?? 'N/D')}</td>
|
||||
<td style="padding:8px 12px;font-family:sans-serif;font-size:12px;color:#475569;">${ultimaRest}</td>
|
||||
<td style="padding:8px 12px;text-align:center;">
|
||||
<span style="display:inline-block;padding:2px 8px;border-radius:9999px;font-family:sans-serif;font-size:11px;font-weight:600;color:${badgeColor};background:${badgeBg};border:1px solid ${badgeColor}30;">
|
||||
${diasLabel}
|
||||
</span>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||
<body style="margin:0;padding:0;background:#f8fafc;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc;padding:24px 0;">
|
||||
<tr><td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.1);">
|
||||
<!-- Encabezado -->
|
||||
<tr>
|
||||
<td style="background:#b91c1c;padding:20px 24px;">
|
||||
<p style="margin:0;font-family:sans-serif;font-size:18px;font-weight:700;color:#ffffff;">
|
||||
Alerta de respaldo — Aduanasoft
|
||||
</p>
|
||||
<p style="margin:4px 0 0;font-family:sans-serif;font-size:12px;color:#fecaca;">
|
||||
${alerts.length} base${alerts.length !== 1 ? 's' : ''} de datos sin restaurar recientemente
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Tabla de alertas -->
|
||||
<tr>
|
||||
<td style="padding:20px 24px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e2e8f0;border-radius:6px;overflow:hidden;">
|
||||
<thead>
|
||||
<tr style="background:#f1f5f9;">
|
||||
<th style="padding:8px 12px;font-family:sans-serif;font-size:11px;font-weight:600;color:#64748b;text-align:left;text-transform:uppercase;">Base de datos</th>
|
||||
<th style="padding:8px 12px;font-family:sans-serif;font-size:11px;font-weight:600;color:#64748b;text-align:left;text-transform:uppercase;">Cliente</th>
|
||||
<th style="padding:8px 12px;font-family:sans-serif;font-size:11px;font-weight:600;color:#64748b;text-align:left;text-transform:uppercase;">Última restauración</th>
|
||||
<th style="padding:8px 12px;font-family:sans-serif;font-size:11px;font-weight:600;color:#64748b;text-align:center;text-transform:uppercase;">Días sin sync</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Pie -->
|
||||
<tr>
|
||||
<td style="padding:12px 24px 20px;border-top:1px solid #f1f5f9;">
|
||||
<p style="margin:0;font-family:sans-serif;font-size:11px;color:#94a3b8;">
|
||||
Este mensaje fue generado automáticamente por el Panel de Control de Bases de Datos Aduanasoft.
|
||||
Por favor no responda a este correo.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
export interface BackupAlertRow {
|
||||
visible_name: string;
|
||||
clientName: string | null;
|
||||
last_restore_date: string | null;
|
||||
daysWithout: number | null;
|
||||
}
|
||||
|
||||
function escHtml(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
Reference in New Issue
Block a user