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:
68
apps/api/src/services/email/templates/new-client-admin.ts
Normal file
68
apps/api/src/services/email/templates/new-client-admin.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { baseTemplate } from './base.js';
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
export function newClientAdminEmail(data: {
|
||||
clienteNombre: string;
|
||||
clienteRfc: string;
|
||||
adminEmail: string;
|
||||
adminNombre: string;
|
||||
tempPassword: string;
|
||||
databaseName: string;
|
||||
plan: string;
|
||||
}): string {
|
||||
return baseTemplate(`
|
||||
<h2 style="color:#1e293b;margin:0 0 16px;">Nuevo Cliente Registrado</h2>
|
||||
<p style="color:#475569;line-height:1.6;margin:0 0 24px;">
|
||||
Se ha dado de alta un nuevo cliente en Horux360. A continuación los detalles:
|
||||
</p>
|
||||
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;">
|
||||
<tr>
|
||||
<td colspan="2" style="background-color:#1e293b;color:#ffffff;padding:12px 16px;font-weight:bold;border-radius:6px 6px 0 0;">
|
||||
Datos del Cliente
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;width:40%;">Empresa</td>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.clienteNombre)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">RFC</td>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.clienteRfc)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">Plan</td>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.plan)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;">
|
||||
<tr>
|
||||
<td colspan="2" style="background-color:#3b82f6;color:#ffffff;padding:12px 16px;font-weight:bold;border-radius:6px 6px 0 0;">
|
||||
Credenciales del Usuario
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;width:40%;">Nombre</td>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.adminNombre)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">Email</td>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.adminEmail)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">Contraseña temporal</td>
|
||||
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;">
|
||||
<code style="background-color:#f1f5f9;padding:4px 8px;border-radius:4px;font-size:14px;color:#dc2626;">${escapeHtml(data.tempPassword)}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p style="color:#94a3b8;font-size:12px;margin:0;">
|
||||
Este correo contiene información confidencial. No lo reenvíes ni lo compartas.
|
||||
</p>
|
||||
`);
|
||||
}
|
||||
Reference in New Issue
Block a user