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:
323
docs/architecture/api-reference.md
Normal file
323
docs/architecture/api-reference.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# API Reference - Horux360
|
||||
|
||||
**Base URL:** `https://horuxfin.com/api`
|
||||
|
||||
---
|
||||
|
||||
## Autenticación
|
||||
|
||||
Todos los endpoints (excepto auth) requieren header:
|
||||
```
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
|
||||
### Rate Limits (por IP)
|
||||
| Endpoint | Límite | Ventana |
|
||||
|----------|--------|---------|
|
||||
| `POST /auth/login` | 10 requests | 15 minutos |
|
||||
| `POST /auth/register` | 3 requests | 1 hora |
|
||||
| `POST /auth/refresh` | 20 requests | 15 minutos |
|
||||
| General `/api/*` | 30 requests/s | burst 50 |
|
||||
|
||||
---
|
||||
|
||||
## Auth (`/api/auth`)
|
||||
|
||||
### `POST /auth/register`
|
||||
Registra nueva empresa y usuario admin. Provisiona base de datos dedicada.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"empresa": { "nombre": "Mi Empresa", "rfc": "ABC123456789" },
|
||||
"usuario": { "nombre": "Juan", "email": "juan@empresa.com", "password": "min8chars" }
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `{ accessToken, refreshToken, user: UserInfo }`
|
||||
|
||||
### `POST /auth/login`
|
||||
```json
|
||||
{ "email": "usuario@empresa.com", "password": "..." }
|
||||
```
|
||||
**Response:** `{ accessToken, refreshToken, user: UserInfo }`
|
||||
|
||||
### `POST /auth/refresh`
|
||||
```json
|
||||
{ "refreshToken": "..." }
|
||||
```
|
||||
**Response:** `{ accessToken, refreshToken }`
|
||||
|
||||
### `POST /auth/logout` *(requiere auth)*
|
||||
```json
|
||||
{ "refreshToken": "..." }
|
||||
```
|
||||
|
||||
### `GET /auth/me` *(requiere auth)*
|
||||
**Response:** `UserInfo`
|
||||
|
||||
---
|
||||
|
||||
## Dashboard (`/api/dashboard`)
|
||||
|
||||
### `GET /dashboard/kpis?año=2026&mes=3`
|
||||
KPIs principales: ingresos, egresos, utilidad, margen, IVA balance, conteo de CFDIs.
|
||||
|
||||
### `GET /dashboard/ingresos-egresos?año=2026`
|
||||
Datos mensuales de ingresos/egresos para gráfica anual.
|
||||
|
||||
### `GET /dashboard/resumen-fiscal?año=2026&mes=3`
|
||||
IVA por pagar, IVA a favor, ISR, declaraciones pendientes, próxima obligación.
|
||||
|
||||
### `GET /dashboard/alertas?limit=5`
|
||||
Alertas activas no resueltas, ordenadas por prioridad.
|
||||
|
||||
---
|
||||
|
||||
## CFDI (`/api/cfdi`)
|
||||
|
||||
### `GET /cfdi?page=1&limit=20&tipo=ingreso&search=...`
|
||||
Lista paginada de CFDIs con filtros.
|
||||
|
||||
### `GET /cfdi/resumen`
|
||||
Resumen de conteo por tipo y estado.
|
||||
|
||||
### `GET /cfdi/emisores`
|
||||
Lista de emisores únicos.
|
||||
|
||||
### `GET /cfdi/receptores`
|
||||
Lista de receptores únicos.
|
||||
|
||||
### `GET /cfdi/:id`
|
||||
Detalle de un CFDI.
|
||||
|
||||
### `GET /cfdi/:id/xml`
|
||||
XML original del CFDI.
|
||||
|
||||
### `POST /cfdi`
|
||||
Crear un CFDI individual. Sujeto a límite de plan.
|
||||
|
||||
### `POST /cfdi/bulk`
|
||||
Carga masiva de CFDIs. Body limit: 50MB. Sujeto a límite de plan.
|
||||
|
||||
### `DELETE /cfdi/:id`
|
||||
Eliminar un CFDI.
|
||||
|
||||
---
|
||||
|
||||
## Impuestos (`/api/impuestos`)
|
||||
|
||||
### `GET /impuestos/iva?año=2026`
|
||||
Datos mensuales de IVA (trasladado, acreditable, resultado, acumulado).
|
||||
|
||||
---
|
||||
|
||||
## Alertas (`/api/alertas`)
|
||||
|
||||
### `GET /alertas`
|
||||
### `POST /alertas`
|
||||
### `PUT /alertas/:id`
|
||||
### `DELETE /alertas/:id`
|
||||
### `PATCH /alertas/:id/read`
|
||||
### `PATCH /alertas/:id/resolve`
|
||||
|
||||
---
|
||||
|
||||
## Calendario (`/api/calendario`)
|
||||
|
||||
### `GET /calendario?año=2026&mes=3`
|
||||
### `POST /calendario`
|
||||
### `PUT /calendario/:id`
|
||||
### `DELETE /calendario/:id`
|
||||
|
||||
---
|
||||
|
||||
## Reportes (`/api/reportes`)
|
||||
|
||||
### `GET /reportes/flujo-efectivo?año=2026`
|
||||
### `GET /reportes/impuestos?año=2026`
|
||||
### `GET /reportes/forecasting?año=2026`
|
||||
### `GET /reportes/concentrado?año=2026`
|
||||
|
||||
---
|
||||
|
||||
## Export (`/api/export`)
|
||||
|
||||
### `GET /export/cfdis?format=excel&tipo=ingreso`
|
||||
Exporta CFDIs a Excel o CSV.
|
||||
|
||||
---
|
||||
|
||||
## FIEL (`/api/fiel`)
|
||||
|
||||
### `POST /fiel/upload`
|
||||
```json
|
||||
{
|
||||
"cerFile": "<base64>",
|
||||
"keyFile": "<base64>",
|
||||
"password": "..."
|
||||
}
|
||||
```
|
||||
- Archivos max 50KB cada uno
|
||||
- Password max 256 caracteres
|
||||
|
||||
### `GET /fiel/status`
|
||||
Estado actual de la FIEL configurada.
|
||||
|
||||
### `DELETE /fiel`
|
||||
Eliminar credenciales FIEL.
|
||||
|
||||
---
|
||||
|
||||
## SAT Sync (`/api/sat`)
|
||||
|
||||
### `POST /sat/sync`
|
||||
Iniciar sincronización manual.
|
||||
```json
|
||||
{ "type": "daily", "dateFrom": "2026-01-01", "dateTo": "2026-01-31" }
|
||||
```
|
||||
|
||||
### `GET /sat/sync/status`
|
||||
Estado actual de sincronización.
|
||||
|
||||
### `GET /sat/sync/history?page=1&limit=10`
|
||||
Historial de sincronizaciones.
|
||||
|
||||
### `GET /sat/sync/:id`
|
||||
Detalle de un job de sincronización.
|
||||
|
||||
### `POST /sat/sync/:id/retry`
|
||||
Reintentar un job fallido.
|
||||
|
||||
### `GET /sat/cron` *(admin global)*
|
||||
Info del job programado.
|
||||
|
||||
### `POST /sat/cron/run` *(admin global)*
|
||||
Ejecutar sincronización global manualmente.
|
||||
|
||||
---
|
||||
|
||||
## Usuarios (`/api/usuarios`)
|
||||
|
||||
### `GET /usuarios`
|
||||
Usuarios del tenant actual.
|
||||
|
||||
### `GET /usuarios/all` *(admin global)*
|
||||
Todos los usuarios de todas las empresas.
|
||||
|
||||
### `POST /usuarios`
|
||||
Invitar usuario (genera password temporal con `crypto.randomBytes`).
|
||||
```json
|
||||
{ "email": "nuevo@empresa.com", "nombre": "María", "role": "contador" }
|
||||
```
|
||||
|
||||
### `PUT /usuarios/:id`
|
||||
Actualizar usuario (nombre, role, active).
|
||||
|
||||
### `DELETE /usuarios/:id`
|
||||
|
||||
### `PUT /usuarios/:id/global` *(admin global)*
|
||||
Actualizar usuario de cualquier empresa.
|
||||
|
||||
### `DELETE /usuarios/:id/global` *(admin global)*
|
||||
|
||||
---
|
||||
|
||||
## Tenants / Clientes (`/api/tenants`) *(admin global)*
|
||||
|
||||
### `GET /tenants`
|
||||
Lista de todos los tenants/clientes.
|
||||
|
||||
### `POST /tenants`
|
||||
Crear nuevo tenant. Provisiona base de datos. Envía email al admin.
|
||||
```json
|
||||
{
|
||||
"nombre": "Empresa Nueva",
|
||||
"rfc": "ENE123456789",
|
||||
"plan": "business",
|
||||
"adminNombre": "Pedro",
|
||||
"adminEmail": "pedro@nueva.com"
|
||||
}
|
||||
```
|
||||
|
||||
### `PUT /tenants/:id`
|
||||
Actualizar tenant (plan, limits, active).
|
||||
|
||||
### `DELETE /tenants/:id`
|
||||
Soft delete — renombra la base de datos a `*_deleted_*`.
|
||||
|
||||
---
|
||||
|
||||
## Suscripciones (`/api/subscriptions`) *(admin global)*
|
||||
|
||||
### `GET /subscriptions/:tenantId`
|
||||
Suscripción activa del tenant.
|
||||
|
||||
### `POST /subscriptions/:tenantId/generate-link`
|
||||
Generar link de pago MercadoPago.
|
||||
|
||||
### `POST /subscriptions/:tenantId/mark-paid`
|
||||
Marcar como pagado manualmente.
|
||||
```json
|
||||
{ "amount": 999 }
|
||||
```
|
||||
|
||||
### `GET /subscriptions/:tenantId/payments`
|
||||
Historial de pagos.
|
||||
|
||||
---
|
||||
|
||||
## Webhooks (`/api/webhooks`)
|
||||
|
||||
### `POST /webhooks/mercadopago`
|
||||
Webhook de MercadoPago. Requiere headers:
|
||||
- `x-signature`: Firma HMAC-SHA256
|
||||
- `x-request-id`: ID del request
|
||||
|
||||
---
|
||||
|
||||
## Roles y Permisos
|
||||
|
||||
| Rol | Descripción | Acceso |
|
||||
|-----|-------------|--------|
|
||||
| `admin` | Administrador del tenant | Todo dentro de su tenant + invitar usuarios |
|
||||
| `contador` | Contador | CFDI, impuestos, reportes, dashboard |
|
||||
| `visor` | Solo lectura | Dashboard, CFDI (solo ver), reportes |
|
||||
|
||||
### Admin Global
|
||||
El admin del tenant con RFC `CAS2408138W2` tiene acceso adicional:
|
||||
- Gestión de todos los tenants
|
||||
- Suscripciones
|
||||
- SAT cron
|
||||
- Impersonación via `X-View-Tenant` header
|
||||
- Bypass de plan limits al impersonar
|
||||
|
||||
---
|
||||
|
||||
## Tipos Compartidos (`@horux/shared`)
|
||||
|
||||
### UserInfo
|
||||
```typescript
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
nombre: string;
|
||||
role: 'admin' | 'contador' | 'visor';
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
tenantRfc: string;
|
||||
plan: string;
|
||||
}
|
||||
```
|
||||
|
||||
### JWTPayload
|
||||
```typescript
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: Role;
|
||||
tenantId: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
```
|
||||
250
docs/architecture/deployment.md
Normal file
250
docs/architecture/deployment.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Guía de Despliegue en Producción - Horux360
|
||||
|
||||
## Infraestructura
|
||||
|
||||
### Servidor
|
||||
- **OS:** Ubuntu 24.04 LTS
|
||||
- **RAM:** 22GB
|
||||
- **CPU:** 8 cores
|
||||
- **Dominio:** horuxfin.com (DNS en AWS Route 53)
|
||||
- **SSL:** Let's Encrypt (certificado real via DNS challenge)
|
||||
- **IP Interna:** 192.168.10.212
|
||||
|
||||
### Stack
|
||||
| Componente | Tecnología | Puerto |
|
||||
|-----------|-----------|--------|
|
||||
| Reverse Proxy | Nginx 1.24 | 80/443 |
|
||||
| API | Node.js + Express + tsx | 4000 |
|
||||
| Frontend | Next.js 14 | 3000 |
|
||||
| Base de datos | PostgreSQL 16 | 5432 |
|
||||
| Process Manager | PM2 | — |
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura de Red
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
▼
|
||||
Nginx (443/SSL)
|
||||
├── /api/* → 127.0.0.1:4000 (horux-api)
|
||||
├── /api/auth/* → 127.0.0.1:4000 (rate limit: 5r/s)
|
||||
├── /api/webhooks/* → 127.0.0.1:4000 (rate limit: 10r/s)
|
||||
├── /health → 127.0.0.1:4000
|
||||
└── /* → 127.0.0.1:3000 (horux-web)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PM2 - Gestión de Procesos
|
||||
|
||||
### Configuración (`ecosystem.config.js`)
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'horux-api',
|
||||
interpreter: 'node',
|
||||
script: '/root/Horux/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/dist/cli.mjs',
|
||||
args: 'src/index.ts',
|
||||
cwd: '/root/Horux/apps/api',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
max_memory_restart: '1G',
|
||||
kill_timeout: 5000,
|
||||
listen_timeout: 10000,
|
||||
env: { NODE_ENV: 'production', PORT: 4000 },
|
||||
},
|
||||
{
|
||||
name: 'horux-web',
|
||||
script: 'node_modules/next/dist/bin/next',
|
||||
args: 'start',
|
||||
cwd: '/root/Horux/apps/web',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
autorestart: true,
|
||||
max_memory_restart: '512M',
|
||||
kill_timeout: 5000,
|
||||
env: { NODE_ENV: 'production', PORT: 3000 },
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Notas
|
||||
- La API usa `tsx` en lugar de `tsc` compilado porque `@horux/shared` exporta TypeScript raw (ESM) que `dist/` no puede resolver.
|
||||
- Next.js usa la ruta directa `node_modules/next/dist/bin/next` porque `node_modules/.bin/next` es un shell script que PM2 no puede ejecutar como script Node.js.
|
||||
|
||||
### Comandos Útiles
|
||||
```bash
|
||||
pm2 restart all # Reiniciar todo
|
||||
pm2 logs horux-api # Ver logs del API
|
||||
pm2 logs horux-web # Ver logs del frontend
|
||||
pm2 monit # Monitor en tiempo real
|
||||
pm2 save # Guardar estado actual
|
||||
pm2 startup # Configurar inicio automático
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nginx
|
||||
|
||||
### Archivo: `/etc/nginx/sites-available/horux360.conf`
|
||||
|
||||
#### Rate Limiting
|
||||
| Zona | Límite | Burst | Uso |
|
||||
|------|--------|-------|-----|
|
||||
| `auth` | 5r/s | 10 | `/api/auth/*` |
|
||||
| `webhook` | 10r/s | 20 | `/api/webhooks/*` |
|
||||
| `api` | 30r/s | 50 | `/api/*` (general) |
|
||||
|
||||
#### Security Headers
|
||||
- `Content-Security-Policy`: Restrictivo (`default-src 'self'`)
|
||||
- `Strict-Transport-Security`: 1 año con includeSubDomains
|
||||
- `X-Frame-Options`: SAMEORIGIN
|
||||
- `X-Content-Type-Options`: nosniff
|
||||
- `Permissions-Policy`: camera, microphone, geolocation deshabilitados
|
||||
- `Referrer-Policy`: strict-origin-when-cross-origin
|
||||
|
||||
#### Body Limits
|
||||
- Global: `50M` (Nginx)
|
||||
- API default: `10mb` (Express)
|
||||
- `/api/cfdi/bulk`: `50mb` (Express route-specific)
|
||||
|
||||
### Renovar SSL
|
||||
```bash
|
||||
certbot renew --dry-run # Verificar
|
||||
certbot renew # Renovar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
### Configuración de Rendimiento (`postgresql.conf`)
|
||||
| Parámetro | Valor | Descripción |
|
||||
|-----------|-------|-------------|
|
||||
| `max_connections` | 300 | Para multi-tenant con pools por tenant |
|
||||
| `shared_buffers` | 4GB | ~18% de 22GB RAM |
|
||||
| `work_mem` | 16MB | Memoria por operación de sort/hash |
|
||||
| `effective_cache_size` | 16GB | ~72% de RAM |
|
||||
| `maintenance_work_mem` | 512MB | Para VACUUM, CREATE INDEX |
|
||||
| `wal_buffers` | 64MB | Write-ahead log buffers |
|
||||
|
||||
### Arquitectura Multi-Tenant
|
||||
Cada cliente tiene su propia base de datos PostgreSQL:
|
||||
```
|
||||
horux360 ← Base central (tenants, users, subscriptions)
|
||||
horux_cas2408138w2 ← Base del admin global
|
||||
horux_<rfc> ← Base de cada cliente
|
||||
```
|
||||
|
||||
### Backups
|
||||
```bash
|
||||
# Cron job: 0 1 * * * /root/Horux/scripts/backup.sh
|
||||
# Ubicación: /var/horux/backups/
|
||||
# Retención: 7 diarios + 4 semanales
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
### API (`apps/api/.env`)
|
||||
```env
|
||||
NODE_ENV=production
|
||||
PORT=4000
|
||||
DATABASE_URL="postgresql://postgres:<password>@localhost:5432/horux360?schema=public"
|
||||
JWT_SECRET=<min 32 chars>
|
||||
JWT_EXPIRES_IN=15m
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
CORS_ORIGIN=https://horuxfin.com
|
||||
FRONTEND_URL=https://horuxfin.com
|
||||
FIEL_ENCRYPTION_KEY=<min 32 chars, REQUERIDO>
|
||||
FIEL_STORAGE_PATH=/var/horux/fiel
|
||||
|
||||
# MercadoPago
|
||||
MP_ACCESS_TOKEN=<token>
|
||||
MP_WEBHOOK_SECRET=<secret, REQUERIDO para producción>
|
||||
|
||||
# SMTP (Google Workspace)
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=ivan@horuxfin.com
|
||||
SMTP_PASS=<app-password>
|
||||
SMTP_FROM=Horux360 <ivan@horuxfin.com>
|
||||
|
||||
# Admin
|
||||
ADMIN_EMAIL=carlos@horuxfin.com
|
||||
```
|
||||
|
||||
### Web (`apps/web/.env.local`)
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=https://horuxfin.com/api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directorios Importantes
|
||||
|
||||
```
|
||||
/root/Horux/ ← Código fuente
|
||||
/var/horux/fiel/ ← Archivos FIEL encriptados (0700)
|
||||
/var/horux/backups/ ← Backups de PostgreSQL
|
||||
/etc/nginx/sites-available/ ← Config de Nginx
|
||||
/etc/letsencrypt/live/ ← Certificados SSL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Despliegue de Cambios
|
||||
|
||||
```bash
|
||||
# 1. Pull cambios
|
||||
cd /root/Horux
|
||||
git pull origin main
|
||||
|
||||
# 2. Instalar dependencias
|
||||
pnpm install
|
||||
|
||||
# 3. Build
|
||||
pnpm build
|
||||
|
||||
# 4. Reiniciar servicios
|
||||
pm2 restart all
|
||||
|
||||
# 5. Si hay cambios en nginx:
|
||||
cp deploy/nginx/horux360.conf /etc/nginx/sites-available/horux360.conf
|
||||
nginx -t && systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### API no inicia
|
||||
```bash
|
||||
pm2 logs horux-api --lines 50 # Ver logs de error
|
||||
pm2 restart horux-api # Reiniciar
|
||||
```
|
||||
|
||||
### Puerto en uso
|
||||
```bash
|
||||
lsof -i :4000 # Ver quién usa el puerto
|
||||
kill <PID> # Matar proceso
|
||||
pm2 restart horux-api
|
||||
```
|
||||
|
||||
### Certificado SSL expirado
|
||||
```bash
|
||||
certbot renew
|
||||
systemctl reload nginx
|
||||
```
|
||||
|
||||
### Base de datos lenta
|
||||
```bash
|
||||
sudo -u postgres psql -c "SELECT * FROM pg_stat_activity WHERE state = 'active';"
|
||||
```
|
||||
Reference in New Issue
Block a user