CRITICAL fixes: - Restrict X-View-Tenant impersonation to global admin only (was any admin) - Add authorization to subscription endpoints (was open to any user) - Make webhook signature verification mandatory (was skippable) - Remove databaseName from JWT payload (resolve server-side with cache) - Reduce body size limit from 1GB to 10MB (50MB for bulk CFDI) - Restrict .env file permissions to 600 HIGH fixes: - Add authorization to SAT cron endpoints (global admin only) - Add Content-Security-Policy and Permissions-Policy headers - Centralize isGlobalAdmin() utility with caching - Add rate limiting on auth endpoints (express-rate-limit) - Require authentication on logout endpoint MEDIUM fixes: - Replace Math.random() with crypto.randomBytes for temp passwords - Remove console.log of temporary passwords in production - Remove DB credentials from admin notification email - Add escapeHtml() to email templates (prevent HTML injection) - Add file size validation on FIEL upload (50KB max) - Require TLS for SMTP connections - Normalize email to lowercase before uniqueness check - Remove hardcoded default for FIEL_ENCRYPTION_KEY Also includes: - Complete production deployment documentation - API reference documentation - Security audit report with remediation details - Updated README with v0.5.0 changelog - New client admin email template - Utility scripts (create-carlos, test-emails) - PM2 ecosystem config updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
import { createTransport, type Transporter } from 'nodemailer';
|
|
import { env } from '../../config/env.js';
|
|
|
|
let transporter: Transporter | null = null;
|
|
|
|
function getTransporter(): Transporter {
|
|
if (!transporter) {
|
|
if (!env.SMTP_USER || !env.SMTP_PASS) {
|
|
console.warn('[EMAIL] SMTP not configured. Emails will be logged to console.');
|
|
return {
|
|
sendMail: async (opts: any) => {
|
|
console.log('[EMAIL] Would send:', { to: opts.to, subject: opts.subject });
|
|
return { messageId: 'mock' };
|
|
},
|
|
} as any;
|
|
}
|
|
|
|
transporter = createTransport({
|
|
host: env.SMTP_HOST,
|
|
port: parseInt(env.SMTP_PORT),
|
|
secure: false, // Upgrade to TLS via STARTTLS
|
|
requireTLS: true, // Reject if STARTTLS is not available
|
|
auth: {
|
|
user: env.SMTP_USER,
|
|
pass: env.SMTP_PASS,
|
|
},
|
|
});
|
|
}
|
|
return transporter;
|
|
}
|
|
|
|
async function sendEmail(to: string, subject: string, html: string) {
|
|
const transport = getTransporter();
|
|
try {
|
|
await transport.sendMail({
|
|
from: env.SMTP_FROM,
|
|
to,
|
|
subject,
|
|
html,
|
|
text: html.replace(/<[^>]*>/g, ''),
|
|
});
|
|
} catch (error) {
|
|
console.error('[EMAIL] Error sending email:', error);
|
|
// Don't throw — email failure shouldn't break the main flow
|
|
}
|
|
}
|
|
|
|
export const emailService = {
|
|
sendWelcome: async (to: string, data: { nombre: string; email: string; tempPassword: string }) => {
|
|
const { welcomeEmail } = await import('./templates/welcome.js');
|
|
await sendEmail(to, 'Bienvenido a Horux360', welcomeEmail(data));
|
|
},
|
|
|
|
sendFielNotification: async (data: { clienteNombre: string; clienteRfc: string }) => {
|
|
const { fielNotificationEmail } = await import('./templates/fiel-notification.js');
|
|
await sendEmail(env.ADMIN_EMAIL, `[${data.clienteNombre}] subió su FIEL`, fielNotificationEmail(data));
|
|
},
|
|
|
|
sendPaymentConfirmed: async (to: string, data: { nombre: string; amount: number; plan: string; date: string }) => {
|
|
const { paymentConfirmedEmail } = await import('./templates/payment-confirmed.js');
|
|
await sendEmail(to, 'Confirmación de pago - Horux360', paymentConfirmedEmail(data));
|
|
},
|
|
|
|
sendPaymentFailed: async (to: string, data: { nombre: string; amount: number; plan: string }) => {
|
|
const { paymentFailedEmail } = await import('./templates/payment-failed.js');
|
|
await sendEmail(to, 'Problema con tu pago - Horux360', paymentFailedEmail(data));
|
|
await sendEmail(env.ADMIN_EMAIL, `Pago fallido: ${data.nombre}`, paymentFailedEmail(data));
|
|
},
|
|
|
|
sendSubscriptionExpiring: async (to: string, data: { nombre: string; plan: string; expiresAt: string }) => {
|
|
const { subscriptionExpiringEmail } = await import('./templates/subscription-expiring.js');
|
|
await sendEmail(to, 'Tu suscripción vence en 5 días', subscriptionExpiringEmail(data));
|
|
},
|
|
|
|
sendSubscriptionCancelled: async (to: string, data: { nombre: string; plan: string }) => {
|
|
const { subscriptionCancelledEmail } = await import('./templates/subscription-cancelled.js');
|
|
await sendEmail(to, 'Suscripción cancelada - Horux360', subscriptionCancelledEmail(data));
|
|
await sendEmail(env.ADMIN_EMAIL, `Suscripción cancelada: ${data.nombre}`, subscriptionCancelledEmail(data));
|
|
},
|
|
|
|
sendNewClientAdmin: async (data: {
|
|
clienteNombre: string;
|
|
clienteRfc: string;
|
|
adminEmail: string;
|
|
adminNombre: string;
|
|
tempPassword: string;
|
|
databaseName: string;
|
|
plan: string;
|
|
}) => {
|
|
const { newClientAdminEmail } = await import('./templates/new-client-admin.js');
|
|
await sendEmail(env.ADMIN_EMAIL, `Nuevo cliente: ${data.clienteNombre} (${data.clienteRfc})`, newClientAdminEmail(data));
|
|
},
|
|
};
|