security: comprehensive security audit and remediation (20 fixes)
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>
This commit is contained in:
96
apps/api/scripts/test-emails.ts
Normal file
96
apps/api/scripts/test-emails.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { emailService } from '../src/services/email/email.service.js';
|
||||
|
||||
const recipients = ['ivan@horuxfin.com', 'carlos@horuxfin.com'];
|
||||
|
||||
async function sendAllSamples() {
|
||||
for (const to of recipients) {
|
||||
console.log(`\n=== Enviando a ${to} ===`);
|
||||
|
||||
// 1. Welcome
|
||||
console.log('1/6 Bienvenida...');
|
||||
await emailService.sendWelcome(to, {
|
||||
nombre: 'Ivan Alcaraz',
|
||||
email: 'ivan@horuxfin.com',
|
||||
tempPassword: 'TempPass123!',
|
||||
});
|
||||
|
||||
// 2. FIEL notification (goes to ADMIN_EMAIL, but we override for test)
|
||||
console.log('2/6 Notificación FIEL...');
|
||||
// Send directly since sendFielNotification goes to admin
|
||||
const { fielNotificationEmail } = await import('../src/services/email/templates/fiel-notification.js');
|
||||
const { createTransport } = await import('nodemailer');
|
||||
const { env } = await import('../src/config/env.js');
|
||||
const transport = createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: parseInt(env.SMTP_PORT),
|
||||
secure: false,
|
||||
auth: { user: env.SMTP_USER, pass: env.SMTP_PASS },
|
||||
});
|
||||
const fielHtml = fielNotificationEmail({
|
||||
clienteNombre: 'Consultoria Alcaraz Salazar',
|
||||
clienteRfc: 'CAS200101XXX',
|
||||
});
|
||||
await transport.sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to,
|
||||
subject: '[Consultoria Alcaraz Salazar] subió su FIEL (MUESTRA)',
|
||||
html: fielHtml,
|
||||
});
|
||||
|
||||
// 3. Payment confirmed
|
||||
console.log('3/6 Pago confirmado...');
|
||||
await emailService.sendPaymentConfirmed(to, {
|
||||
nombre: 'Ivan Alcaraz',
|
||||
amount: 1499,
|
||||
plan: 'Enterprise',
|
||||
date: '16 de marzo de 2026',
|
||||
});
|
||||
|
||||
// 4. Payment failed
|
||||
console.log('4/6 Pago fallido...');
|
||||
const { paymentFailedEmail } = await import('../src/services/email/templates/payment-failed.js');
|
||||
const failedHtml = paymentFailedEmail({
|
||||
nombre: 'Ivan Alcaraz',
|
||||
amount: 1499,
|
||||
plan: 'Enterprise',
|
||||
});
|
||||
await transport.sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to,
|
||||
subject: 'Problema con tu pago - Horux360 (MUESTRA)',
|
||||
html: failedHtml,
|
||||
});
|
||||
|
||||
// 5. Subscription expiring
|
||||
console.log('5/6 Suscripción por vencer...');
|
||||
await emailService.sendSubscriptionExpiring(to, {
|
||||
nombre: 'Ivan Alcaraz',
|
||||
plan: 'Enterprise',
|
||||
expiresAt: '21 de marzo de 2026',
|
||||
});
|
||||
|
||||
// 6. Subscription cancelled
|
||||
console.log('6/6 Suscripción cancelada...');
|
||||
const { subscriptionCancelledEmail } = await import('../src/services/email/templates/subscription-cancelled.js');
|
||||
const cancelledHtml = subscriptionCancelledEmail({
|
||||
nombre: 'Ivan Alcaraz',
|
||||
plan: 'Enterprise',
|
||||
});
|
||||
await transport.sendMail({
|
||||
from: env.SMTP_FROM,
|
||||
to,
|
||||
subject: 'Suscripción cancelada - Horux360 (MUESTRA)',
|
||||
html: cancelledHtml,
|
||||
});
|
||||
|
||||
console.log(`Listo: 6 correos enviados a ${to}`);
|
||||
}
|
||||
|
||||
console.log('\n=== Todos los correos enviados ===');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
sendAllSamples().catch((err) => {
|
||||
console.error('Error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user