Files
Horux360/apps/api/src/controllers/sat.controller.ts
Consultoria AS 351b14a78c 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>
2026-03-18 22:32:04 +00:00

161 lines
4.6 KiB
TypeScript

import type { Request, Response } from 'express';
import {
startSync,
getSyncStatus,
getSyncHistory,
retryJob,
} from '../services/sat/sat.service.js';
import { getJobInfo, runSatSyncJobManually } from '../jobs/sat-sync.job.js';
import type { StartSyncRequest } from '@horux/shared';
import { isGlobalAdmin } from '../utils/global-admin.js';
/**
* Inicia una sincronización manual
*/
export async function start(req: Request, res: Response): Promise<void> {
try {
const tenantId = req.user!.tenantId;
const { type, dateFrom, dateTo } = req.body as StartSyncRequest;
const jobId = await startSync(
tenantId,
type || 'daily',
dateFrom ? new Date(dateFrom) : undefined,
dateTo ? new Date(dateTo) : undefined
);
res.json({
jobId,
message: 'Sincronización iniciada',
});
} catch (error: any) {
console.error('[SAT Controller] Error en start:', error);
if (error.message.includes('FIEL') || error.message.includes('sincronización en curso')) {
res.status(400).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Error interno del servidor' });
}
}
/**
* Obtiene el estado actual de sincronización
*/
export async function status(req: Request, res: Response): Promise<void> {
try {
const tenantId = req.user!.tenantId;
const syncStatus = await getSyncStatus(tenantId);
res.json(syncStatus);
} catch (error: any) {
console.error('[SAT Controller] Error en status:', error);
res.status(500).json({ error: 'Error interno del servidor' });
}
}
/**
* Obtiene el historial de sincronizaciones
*/
export async function history(req: Request, res: Response): Promise<void> {
try {
const tenantId = req.user!.tenantId;
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const result = await getSyncHistory(tenantId, page, limit);
res.json({
...result,
page,
limit,
});
} catch (error: any) {
console.error('[SAT Controller] Error en history:', error);
res.status(500).json({ error: 'Error interno del servidor' });
}
}
/**
* Obtiene detalle de un job específico
*/
export async function jobDetail(req: Request, res: Response): Promise<void> {
try {
const tenantId = req.user!.tenantId;
const { id } = req.params;
const { jobs } = await getSyncHistory(tenantId, 1, 100);
const job = jobs.find(j => j.id === id);
if (!job) {
res.status(404).json({ error: 'Job no encontrado' });
return;
}
res.json(job);
} catch (error: any) {
console.error('[SAT Controller] Error en jobDetail:', error);
res.status(500).json({ error: 'Error interno del servidor' });
}
}
/**
* Reintenta un job fallido
*/
export async function retry(req: Request, res: Response): Promise<void> {
try {
const id = req.params.id as string;
const newJobId = await retryJob(id);
res.json({
jobId: newJobId,
message: 'Job reintentado',
});
} catch (error: any) {
console.error('[SAT Controller] Error en retry:', error);
if (error.message.includes('no encontrado') || error.message.includes('Solo se pueden')) {
res.status(400).json({ error: error.message });
return;
}
res.status(500).json({ error: 'Error interno del servidor' });
}
}
/**
* Obtiene información del job programado (solo admin global)
*/
export async function cronInfo(req: Request, res: Response): Promise<void> {
try {
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role))) {
res.status(403).json({ error: 'Solo el administrador global puede ver info del cron' });
return;
}
const info = getJobInfo();
res.json(info);
} catch (error: any) {
console.error('[SAT Controller] Error en cronInfo:', error);
res.status(500).json({ error: 'Error interno del servidor' });
}
}
/**
* Ejecuta el job de sincronización manualmente (solo admin global)
*/
export async function runCron(req: Request, res: Response): Promise<void> {
try {
if (!(await isGlobalAdmin(req.user!.tenantId, req.user!.role))) {
res.status(403).json({ error: 'Solo el administrador global puede ejecutar el cron' });
return;
}
// Ejecutar en background
runSatSyncJobManually().catch(err =>
console.error('[SAT Controller] Error ejecutando cron manual:', err)
);
res.json({ message: 'Job de sincronización iniciado' });
} catch (error: any) {
console.error('[SAT Controller] Error en runCron:', error);
res.status(500).json({ error: 'Error interno del servidor' });
}
}