fix(cors): Resolve duplicate CORS headers in production

## Problem
CORS error: "Access-Control-Allow-Origin header contains multiple values"
caused by both Nginx and NestJS sending CORS headers.

## Solution
- NestJS handles CORS exclusively (main.ts)
- Nginx acts as SSL proxy only (no CORS headers)
- Updated .env.production.example with HTTPS origins
- Created GUIA-CORS-PRODUCCION.md with complete documentation

## Files Changed
- .gitignore: Allow .env.*.example files
- apps/backend/.gitignore: Allow .env.*.example files
- apps/backend/.env.production.example: HTTPS CORS config
- apps/frontend/.env.production.example: HTTPS/WSS config
- docs/95-guias-desarrollo/GUIA-CORS-PRODUCCION.md: Full guide

## Production Steps
1. Update .env.production files with HTTPS origins
2. Remove CORS headers from Nginx config
3. Rebuild frontend, restart backend

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2025-12-18 10:24:01 -06:00
parent d0d5699cd5
commit 8b12d7f231
5 changed files with 461 additions and 245 deletions

View File

@ -1,148 +1,106 @@
# GAMILIT Monorepo - .gitignore
# Generado: 2025-11-01 (RFC-0001)
# Actualizado: 2025-12-05
# ============================================
# NODE.JS - DEPENDENCIAS (GLOBAL)
# ============================================
# Ignorar node_modules en CUALQUIER nivel de anidación
**/node_modules/
# Lock files de otros package managers (mantener solo package-lock.json)
yarn.lock
pnpm-lock.yaml
bun.lockb
# Logs de npm/yarn/pnpm
# === NODE.JS ===
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Directorios de dependencias alternativas
# Dependency directories
jspm_packages/
bower_components/
# Cache de npm
.npm/
.npmrc.local
# Optional npm cache directory
.npm
# Cache de eslint/stylelint
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# ============================================
# TYPESCRIPT / BUILD ARTIFACTS (GLOBAL)
# ============================================
# Ignorar dist/build en CUALQUIER nivel
**/dist/
**/build/
**/out/
**/.next/
**/.nuxt/
**/.turbo/
# TypeScript
# === TYPESCRIPT ===
*.tsbuildinfo
**/*.tsbuildinfo
dist/
build/
*.js.map
# ============================================
# FRAMEWORKS ESPECÍFICOS
# ============================================
# Angular / NX
# === ANGULAR / NX ===
.angular/
.nx/
**/.nx/
.nx/cache/
.nx/workspace-data/
# Vite
**/.vite/
# === NESTJS ===
/apps/backend/dist/
/apps/backend/build/
# Webpack
.webpack/
**/.webpack/
# ============================================
# ENVIRONMENT FILES - SECRETS
# ============================================
# CRÍTICO: Nunca commitear secrets
# === ENVIRONMENT FILES ===
# IMPORTANTE: Nunca commitear secrets reales
.env
.env.local
.env.*.local
.env.production
.env.development
.env.test
.env.staging
# Permitir archivos de ejemplo (sin secrets)
!.env.*.example
!.env.example
# Archivos con secrets
**/secrets.json
**/credentials.json
# Archivos de configuración con secrets
config/secrets.json
config/credentials.json
**/*secrets*.json
**/*credentials*.json
**/*.secret
**/*.secrets
# Configuración de base de datos con credenciales
**/database.config.ts
!**/database.config.example.ts
**/ormconfig.json
!**/ormconfig.example.json
# ============================================
# DATABASES
# ============================================
# Backups y dumps
# === DATABASES ===
# PostgreSQL
*.sql.backup
*.dump
*.pgdata
*.sql.gz
# Bases de datos locales
# Local database files
*.sqlite
*.sqlite3
*.db
*.db-journal
# Redis
dump.rdb
# Database connection strings (excepción: ejemplos con .example)
database.config.ts
!database.config.example.ts
# ============================================
# LOGS (GLOBAL)
# ============================================
**/logs/
**/*.log
**/pm2-logs/
**/*.pm2.log
# === LOGS ===
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# ============================================
# TESTING (GLOBAL)
# ============================================
**/coverage/
**/.nyc_output/
# PM2 logs
pm2-logs/
*.pm2.log
# === TESTING ===
coverage/
.nyc_output/
*.lcov
# Jest
**/jest-cache/
.jest/
jest-cache/
# Cypress
**/cypress/screenshots/
**/cypress/videos/
**/cypress/downloads/
# Playwright
**/playwright-report/
**/playwright/.cache/
**/test-results/
cypress/screenshots/
cypress/videos/
cypress/downloads/
# E2E reports
**/e2e-reports/
e2e-reports/
test-results/
# ============================================
# IDEs and EDITORS
# ============================================
# VSCode - mantener configuración compartida
# === IDEs and EDITORS ===
# VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
@ -161,117 +119,92 @@ dump.rdb
*.sublime-workspace
*.sublime-project
# Vim/Neovim
# Vim
*.swp
*.swo
*~
.netrwhist
Session.vim
# Emacs
*~
\#*\#
.\#*
*.elc
auto-save-list/
# ============================================
# OS FILES
# ============================================
# === OS FILES ===
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
# Linux
*~
.directory
.Trash-*
# Windows
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
$RECYCLE.BIN/
*.lnk
# ============================================
# DOCKER
# ============================================
# === DOCKER ===
# No ignorar Dockerfiles, solo archivos temporales
docker-compose.override.yml
docker-compose.local.yml
.dockerignore.local
**/*.dockerignore.local
# ============================================
# DEPLOYMENT & SECURITY
# ============================================
# === BUILD ARTIFACTS ===
/apps/*/dist/
/apps/*/build/
/libs/*/dist/
# Webpack
.webpack/
# === DEPLOYMENT ===
# PM2
ecosystem.config.js.local
pm2.config.js.local
# Claves y certificados
# Deploy keys (excepción: .example files)
*.pem
*.key
*.p12
*.pfx
!*.example.pem
!*.example.key
# SSL certificates
# SSL certificates (excepción: self-signed para dev)
*.crt
*.cer
!dev-cert.crt
!*.example.crt
# SSH keys
id_rsa*
id_ed25519*
*.pub
!*.example.pub
# === TEMP FILES ===
tmp/
temp/
*.tmp
*.temp
*.cache
# ============================================
# TEMP FILES (GLOBAL)
# ============================================
**/tmp/
**/temp/
**/.tmp/
**/.temp/
**/*.tmp
**/*.temp
**/*.cache
**/.cache/
# ============================================
# ARTIFACTS
# ============================================
# === ARTIFACTS (parcial) ===
# Mantener reportes importantes, ignorar temporales
/artifacts/temp/
/artifacts/cache/
/artifacts/**/*.tmp
/artifacts/**/*.cache
# ============================================
# CLAUDE CODE / AI
# ============================================
# Configuración local de Claude Code
# === CLAUDE CODE ===
# Excluir toda la carpeta .claude (configuración local de IA)
.claude/
# Pero NO ignorar orchestration (necesario para Claude Code cloud)
# Solo ignorar temporales dentro de orchestration
# === ORCHESTRATION ===
# IMPORTANTE: orchestration/ DEBE estar en el repo para Claude Code cloud
# Contiene: prompts, directivas, trazas, inventarios, templates
# Solo ignorar subcarpetas temporales específicas y archivos comprimidos
orchestration/.archive/
orchestration/.tmp/
orchestration/**/*.tmp
orchestration/**/*.cache
# ============================================
# REFERENCE (Código de Referencia)
# ============================================
# reference/ DEBE estar en el repo
# Solo ignorar build/dependencias dentro
# === REFERENCE (Código de Referencia) ===
# IMPORTANTE: reference/ DEBE estar en el repo para Claude Code cloud
# Contiene: proyectos de referencia para análisis y desarrollo
# Ignorar solo carpetas de build/dependencias dentro de reference/
reference/**/node_modules/
reference/**/dist/
reference/**/build/
@ -286,9 +219,16 @@ reference/**/*.tmp
reference/**/*.cache
reference/**/.DS_Store
# ============================================
# PACKAGE MANAGERS
# ============================================
# === MIGRATION (temporal) ===
# Durante migración, mantener docs de análisis
# Descomentar después de migración completa:
# /docs-analysis/
# === ARCHIVOS ESPECÍFICOS GRANDES ===
# Si hay archivos markdown extremadamente grandes (>50MB), considerarlos para LFS
# *.large.md
# === PACKAGE MANAGERS ===
# Yarn
.yarn/*
!.yarn/patches
@ -301,102 +241,33 @@ reference/**/.DS_Store
# PNPM
.pnpm-store/
# ============================================
# MONITORING & OBSERVABILITY
# ============================================
# === MONITORING ===
# Application monitoring
newrelic_agent.log
.monitors/
**/.sentry/
**/sentry-debug.log
# ============================================
# BACKUPS (GLOBAL)
# ============================================
# Archivos
# === MISC ===
# Backups - Archivos
*.backup
*.bak
*.old
*.orig
# Carpetas
**/*_old/
**/*_bckp/
**/*_bkp/
**/*_backup/
**/*.old/
**/*.bak/
**/*.backup/
# Backups - Carpetas
*_old/
*_bckp/
*_bkp/
*_backup/
*.old/
*.bak/
*.backup/
# Específicos del proyecto
# Backups específicos (carpetas identificadas en workspace)
orchestration_old/
orchestration_bckp/
docs_bkp/
# ============================================
# COMPRESSED FILES
# ============================================
# Compressed files (si no son assets del proyecto)
*.zip
*.tar.gz
*.tar
*.rar
*.7z
# Excepto assets
!assets/**/*.zip
!public/**/*.zip
# ============================================
# MISCELÁNEOS
# ============================================
# Archivos de debug
**/*.debug
**/debug.log
# Archivos de error
**/*.error
**/error.log
# Storybook
**/storybook-static/
# Parcel
.parcel-cache/
# Serverless
.serverless/
# FuseBox
.fusebox/
# DynamoDB Local
.dynamodb/
# TernJS
.tern-port
# Archivos generados por IDEs
*.code-workspace
# Archivos de licencia generados
LICENSE.txt.bak
# ============================================
# APPS ESPECÍFICAS DEL MONOREPO
# ============================================
# Backend NestJS
apps/backend/dist/
apps/backend/build/
apps/backend/.nest/
# Frontend React/Vite
apps/frontend/dist/
apps/frontend/build/
apps/frontend/.vite/
# Database - mantener DDL, ignorar generados
apps/database/*.dump
apps/database/*.backup
apps/database/data/
# DevOps - ignorar logs y temp
apps/devops/logs/
apps/devops/tmp/

View File

@ -0,0 +1,137 @@
# ============================================================================
# GAMILIT Backend - Production Environment Variables (EXAMPLE)
# ============================================================================
# INSTRUCCIONES:
# 1. Copiar este archivo a .env.production
# 2. Reemplazar todos los valores <...> con valores reales
# 3. Configurar como variables de entorno del sistema (recomendado)
# O usar archivo .env.production (asegurar permisos restrictivos)
# ==================== SERVER ====================
NODE_ENV=production
PORT=3006
API_PREFIX=api
# ==================== DATABASE ====================
# IMPORTANTE: Usar credenciales seguras en producción
DB_HOST=localhost
DB_PORT=5432
DB_NAME=gamilit_platform
DB_USER=gamilit_user
DB_PASSWORD=<PASSWORD_SEGURO_AQUI>
DB_SYNCHRONIZE=false
DB_LOGGING=false
# ==================== JWT ====================
# IMPORTANTE: NUNCA usar valores de ejemplo en producción
# Generar secret seguro con: openssl rand -base64 32
JWT_SECRET=<GENERAR_SECRET_SEGURO_AQUI>
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# ==================== CORS ====================
# CRÍTICO: Configurar orígenes permitidos correctamente
#
# ============================================================================
# ADVERTENCIA: HEADERS CORS DUPLICADOS
# ============================================================================
# Si usas Nginx como proxy SSL, NO agregar headers CORS en Nginx.
# NestJS maneja CORS internamente en main.ts.
# Headers duplicados causan error:
# "The 'Access-Control-Allow-Origin' header contains multiple values"
#
# SOLUCION: Solo NestJS maneja CORS, Nginx solo hace proxy sin headers CORS.
# Ver: docs/95-guias-desarrollo/GUIA-CORS-PRODUCCION.md
# ============================================================================
#
# Servidor actual: 74.208.126.102
# Frontend puerto: 3005
#
# CONFIGURACION RECOMENDADA CON SSL:
# Incluye HTTPS y HTTP para compatibilidad durante transición
CORS_ORIGIN=https://74.208.126.102:3005,https://74.208.126.102,http://74.208.126.102:3005,http://74.208.126.102
ENABLE_CORS=true
ENABLE_SWAGGER=false
# ==================== LOGGING ====================
LOG_LEVEL=warn
LOG_TO_FILE=true
# ==================== RATE LIMITING ====================
RATE_LIMIT_TTL=60
RATE_LIMIT_MAX=100
# ==================== SESSION ====================
# Generar secret con: openssl rand -base64 32
SESSION_SECRET=<GENERAR_SECRET_SEGURO_AQUI>
SESSION_MAX_AGE=86400000
# ==================== EMAIL ====================
# Configurar si se usa envío de emails
EMAIL_FROM=noreply@gamilit.com
EMAIL_REPLY_TO=support@gamilit.com
# ==================== FRONTEND URL ====================
# URL del frontend para redirects, links en emails, etc.
# IMPORTANTE: Usar HTTPS si el servidor tiene SSL configurado
FRONTEND_URL=https://74.208.126.102:3005
# ==================== WEB PUSH NOTIFICATIONS (VAPID) ====================
# Web Push API nativo - No requiere servicios externos (Firebase, etc.)
# IMPORTANTE: Generar claves VAPID con el comando:
# npx web-push generate-vapid-keys
#
# VAPID (Voluntary Application Server Identification):
# - Public Key: Se comparte con el frontend para crear subscripciones
# - Private Key: Se mantiene secreta en backend para firmar notificaciones
# - Subject: Email de contacto o URL del sitio (mailto: o https:)
#
# Ejemplo de generación:
# $ npx web-push generate-vapid-keys
# Public Key: BN4GvZtEZiZuqaaObWga7lEP-S1WCv7L1c...
# Private Key: aB3cDefGh4IjKlM5nOpQr6StUvWxYz...
#
# Compatible con: Chrome, Firefox, Edge, Safari 16.4+
# NOTA: Si no se configuran, push notifications quedarán deshabilitadas (graceful degradation)
#
VAPID_PUBLIC_KEY=<GENERAR_CON_WEB_PUSH_GENERATE_VAPID_KEYS>
VAPID_PRIVATE_KEY=<GENERAR_CON_WEB_PUSH_GENERATE_VAPID_KEYS>
VAPID_SUBJECT=mailto:admin@gamilit.com
# ==================== OPCIONALES ====================
# Descomentar y configurar si se usan
# Uploads
# MAX_FILE_SIZE=5242880
# UPLOAD_DESTINATION=./uploads
# ALLOWED_MIME_TYPES=image/jpeg,image/png,image/gif,application/pdf
# Mantenimiento
# MAINTENANCE_MODE=false
# ============================================================================
# NOTAS DE SEGURIDAD
# ============================================================================
#
# 1. JWT_SECRET: DEBE ser diferente al de desarrollo, usar 32+ caracteres aleatorios
# 2. DB_PASSWORD: NUNCA commitear credenciales reales al repositorio
# 3. SESSION_SECRET: Generar valor único y seguro
# 4. CORS_ORIGIN: Solo incluir orígenes confiables, NUNCA usar "*"
# 5. ENABLE_SWAGGER: DEBE estar en false en producción
# 6. Permisos archivo: chmod 600 .env.production (solo owner puede leer)
# 7. Logs: Verificar que LOG_TO_FILE no exponga datos sensibles
#
# ============================================================================
# VALIDACIÓN PRE-DEPLOY
# ============================================================================
#
# Antes de deploy, verificar:
# [ ] JWT_SECRET cambiado de valor de ejemplo
# [ ] DB_PASSWORD configurado correctamente
# [ ] CORS_ORIGIN incluye origen del frontend en producción
# [ ] ENABLE_SWAGGER=false
# [ ] FRONTEND_URL apunta a URL correcta
# [ ] Todas las variables <...> reemplazadas
# [ ] VAPID_* configuradas si se requieren push notifications (generar con npx web-push generate-vapid-keys)
#
# ============================================================================

View File

@ -33,13 +33,11 @@ coverage/
logs/
*.log
# Uploads (keep directory structure but ignore files)
uploads/exercises/*
!uploads/exercises/.gitkeep
.env*
.flaskenv*
!.env.project
!.env.vault
!.env.*.example
!.env.example
# Backups de scripts automáticos
**/*.backup-*

View File

@ -0,0 +1,53 @@
# ============================================================================
# GAMILIT Frontend - Production Environment EXAMPLE
# ============================================================================
# COPIAR A .env.production
# Fecha: 2025-12-18
# Server: 74.208.126.102
# ============================================================================
# ==================== APPLICATION ====================
VITE_APP_NAME=GAMILIT Platform
VITE_APP_VERSION=1.0.0
VITE_APP_ENV=production
VITE_ENV=production
# ==================== API CONFIGURATION ====================
# IMPORTANTE: Usar HTTPS/WSS si el servidor tiene SSL configurado
# ============================================================================
# SSL CONFIGURADO - Usar HTTPS/WSS
# ============================================================================
VITE_API_HOST=74.208.126.102:3006
VITE_API_PROTOCOL=https
VITE_API_VERSION=v1
VITE_API_TIMEOUT=30000
# WebSocket configuration (WSS para conexiones seguras)
VITE_WS_HOST=74.208.126.102:3006
VITE_WS_PROTOCOL=wss
# ==================== AUTHENTICATION ====================
VITE_JWT_EXPIRATION=7d
# ==================== FEATURE FLAGS ====================
VITE_ENABLE_GAMIFICATION=true
VITE_ENABLE_SOCIAL_FEATURES=true
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_DEBUG=false
VITE_ENABLE_STORYBOOK=false
VITE_MOCK_API=false
# ==================== EXTERNAL SERVICES ====================
# Configurar en produccion si se usan
VITE_GOOGLE_ANALYTICS_ID=
VITE_SENTRY_DSN=
VITE_AI_SERVICE_URL=
# ==================== PRODUCTION ====================
VITE_ENABLE_DEBUG=false
VITE_LOG_LEVEL=error
# ==================== TESTING ====================
# NO usar credenciales en produccion
VITE_TEST_USER_EMAIL=
VITE_TEST_USER_PASSWORD=

View File

@ -0,0 +1,157 @@
# GUIA-CORS-PRODUCCION.md
## Configuración CORS para Producción - GAMILIT
**Fecha**: 2025-12-18
**Problema resuelto**: Error `Access-Control-Allow-Origin contains multiple values`
---
## 1. Descripción del Problema
Al hacer requests desde el frontend (puerto 3005) al backend (puerto 3006) en producción con HTTPS, se recibe el error:
```
Access to XMLHttpRequest at 'https://74.208.126.102:3006/api/v1/auth/register'
from origin 'https://74.208.126.102:3005' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header contains multiple values
'https://74.208.126.102:3005, https://74.208.126.102:3005', but only one is allowed.
```
---
## 2. Causa Raíz
El header `Access-Control-Allow-Origin` se está enviando **DOS VECES**:
1. **Nginx** (proxy SSL) agrega headers CORS
2. **NestJS** (backend) también agrega headers CORS via `app.enableCors()`
Cuando ambos envían el header, el navegador ve valores duplicados y rechaza la respuesta.
---
## 3. Solución Definitiva
### Regla de Oro: **Solo NestJS maneja CORS**
Nginx debe actuar únicamente como proxy SSL sin agregar headers CORS.
### 3.1 Configuración Backend (.env.production)
```bash
# CORS - CONFIGURACIÓN CRÍTICA
# ============================================================================
# ADVERTENCIA: HEADERS CORS DUPLICADOS
# ============================================================================
# Si usas Nginx como proxy SSL, NO agregar headers CORS en Nginx.
# NestJS maneja CORS internamente en main.ts.
# Headers duplicados causan error:
# "The 'Access-Control-Allow-Origin' header contains multiple values"
# ============================================================================
# Incluye HTTPS y HTTP para compatibilidad durante transición
CORS_ORIGIN=https://74.208.126.102:3005,https://74.208.126.102,http://74.208.126.102:3005,http://74.208.126.102
ENABLE_CORS=true
```
### 3.2 Configuración Frontend (.env.production)
```bash
# SSL CONFIGURADO - Usar HTTPS/WSS
VITE_API_HOST=74.208.126.102:3006
VITE_API_PROTOCOL=https
VITE_WS_HOST=74.208.126.102:3006
VITE_WS_PROTOCOL=wss
```
### 3.3 Configuración Nginx (SIN CORS)
```nginx
# /etc/nginx/sites-available/gamilit-backend
server {
listen 3006 ssl;
server_name 74.208.126.102;
ssl_certificate /etc/ssl/certs/gamilit.crt;
ssl_certificate_key /etc/ssl/private/gamilit.key;
location / {
proxy_pass http://127.0.0.1:3007; # PM2 cluster interno
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# ⚠️ NO AGREGAR headers CORS aquí
# NestJS los maneja internamente
}
}
```
---
## 4. Verificación
### 4.1 Verificar headers con curl
```bash
# Verificar que solo hay UN header Access-Control-Allow-Origin
curl -I -X OPTIONS \
-H "Origin: https://74.208.126.102:3005" \
-H "Access-Control-Request-Method: POST" \
https://74.208.126.102:3006/api/v1/auth/register
# Salida esperada (UN solo header):
# Access-Control-Allow-Origin: https://74.208.126.102:3005
# Access-Control-Allow-Credentials: true
# Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS
```
### 4.2 Verificar configuración Nginx
```bash
# Buscar headers CORS duplicados en configuración
grep -r "Access-Control" /etc/nginx/
```
Si encuentra líneas como `add_header Access-Control-Allow-Origin`, elimínelas.
---
## 5. Pasos de Deploy
1. **Backend**: Actualizar `.env.production` con HTTPS origins
2. **Frontend**: Actualizar `.env.production` con HTTPS/WSS
3. **Nginx**: Remover cualquier header CORS
4. **Reiniciar servicios**:
```bash
sudo systemctl reload nginx
pm2 restart gamilit-backend
cd /path/to/frontend && npm run build
```
---
## 6. Troubleshooting
### Error persiste después de la configuración
1. Verificar que Nginx no tenga headers CORS en ningún include
2. Revisar si hay otro proxy (CloudFlare, etc.) agregando headers
3. Limpiar cache del navegador (F12 > Network > Disable cache)
### Error "CORS blocked request" en logs
- El origen que hace la petición no está en CORS_ORIGIN
- Verificar que el protocolo coincide (https vs http)
---
## 7. Referencias
- **Backend CORS**: `apps/backend/src/main.ts` líneas 27-46
- **Config CORS**: `apps/backend/src/config/app.config.ts`
- **Ejemplo .env**: `apps/backend/.env.production.example`