primer commit

This commit is contained in:
2026-01-17 21:43:47 -06:00
commit c8450ed5a8
40 changed files with 11170 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Dependencies
node_modules/
# Build outputs
dist/
out/
build/
# Logistics
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.*.local
# IDEs
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Gateway specific
gateway/auth_info/*
!gateway/auth_info/.gitkeep

236
API_n8n.md Normal file
View File

@@ -0,0 +1,236 @@
# WhatsApp Gateway - API Core para n8n
## 🎯 **COMPONENTE 3: API Core (n8n Integration)**
API diseñada específicamente para integración con n8n, completamente desacoplada de Baileys.
---
## 🔐 **Autenticación Bearer Token**
### Generar Token
```http
POST /api/n8n/generate-token
Content-Type: application/json
{
"permissions": ["send", "status", "messages"]
}
```
**Respuesta:**
```json
{
"success": true,
"token": "MTY3Mzk4NzIzNDM2Ny1leGFtcGxlLXRva2Vu",
"permissions": ["send", "status", "messages"],
"expiresAt": "2026-01-18T22:43:27.367Z",
"timestamp": "2026-01-17T22:43:27.367Z"
}
```
### Usar Token
```http
Authorization: Bearer MTY3Mzk4NzIzNDM2Ny1leGFtcGxlLXRva2Vu
```
---
## 📤 **Enviar Mensajes (n8n → WhatsApp)**
### Endpoint Principal
```http
POST /api/messages/send
Authorization: Bearer <token>
Content-Type: application/json
{
"to": "50371234567",
"message": "Hola desde n8n!"
}
```
**Respuesta:**
```json
{
"success": true,
"messageId": "3EB0A7B5A234567890ABCDEF",
"to": "50371234567",
"message": "Hola desde n8n!",
"timestamp": "2026-01-17T22:43:27.367Z"
}
```
### Errores
```json
{
"success": false,
"to": "50371234567",
"message": "Hola desde n8n!",
"timestamp": "2026-01-17T22:43:27.367Z",
"error": "WhatsApp not connected. Please scan QR code first."
}
```
---
## 📥 **Recibir Mensajes (WhatsApp → n8n)**
### Webhook para n8n
```http
POST /webhook/whatsapp
Content-Type: application/json
{
"from": "50371234567@s.whatsapp.net",
"body": "Hola desde WhatsApp",
"timestamp": "2026-01-17T22:43:27.367Z",
"type": "text",
"messageId": "3EB0A7B5A234567890ABCDEF"
}
```
### Configurar Webhook
```http
POST /api/webhook/configure
Authorization: Bearer <token>
Content-Type: application/json
{
"url": "https://tu-n8n-instance.com/webhook/whatsapp",
"enabled": true
}
```
---
## 📊 **Ver Estado de Conexión**
### Estado del Gateway
```http
GET /api/status
Authorization: Bearer <token>
```
**Respuesta:**
```json
{
"success": true,
"status": "connected",
"sessionId": "default",
"uptime": 3600,
"timestamp": "2026-01-17T22:43:27.367Z",
"capabilities": {
"send": true,
"receive": true,
"webhook": true
}
}
```
---
## 🌐 **Configuración CORS**
La API acepta peticiones desde:
- `http://localhost:3002` (Manager Web)
- `http://localhost:5678` (n8n default)
- `http://localhost:5679` (n8n alternative)
- Orígenes personalizados via `N8N_ORIGINS` env var
---
## 📋 **Formato de Teléfonos**
### Formatos Soportados
```json
{
"to": "50371234567", // ✅ Completo con código de país
"to": "71234567", // ✅ Agrega código 503 automáticamente
"to": "+50371234567", // ✅ Formato internacional
"to": "01234567", // ✅ Quita 0 y agrega 503
"to": "123" // ❌ Inválido (demasiado corto)
}
```
---
## 🔧 **Endpoints de Gestión**
### Validar Token
```http
POST /api/n8n/token
Authorization: Bearer <token>
```
### Estado del Webhook
```http
GET /api/webhook/status
Authorization: Bearer <token>
```
### Probar Webhook
```http
POST /api/webhook/test
Authorization: Bearer <token>
```
---
## 🚀 **Integración con n8n**
### Node HTTP Request (Enviar Mensaje)
```
Method: POST
URL: http://localhost:3001/api/messages/send
Headers:
Authorization: Bearer {{token}}
Body (JSON):
{
"to": "50371234567",
"message": "Hola desde n8n!"
}
```
### Webhook Trigger (Recibir Mensaje)
```
Webhook URL: http://localhost:3001/webhook/whatsapp
HTTP Method: POST
Content Type: application/json
```
---
## 🔒 **Consideraciones de Seguridad**
1. **Tokens expiran en 24 horas**
2. **Permisos granulares**: send, status, messages
3. **Validación de formato de teléfono**
4. **CORS configurado para n8n**
5. **Logs estructurados para auditoría**
---
## 📝 **Ejemplo Completo de Flujo n8n**
### 1. Generar Token
```bash
curl -X POST http://localhost:3001/api/n8n/generate-token \
-H "Content-Type: application/json" \
-d '{"permissions": ["send", "status"]}'
```
### 2. Enviar Mensaje desde n8n
```bash
curl -X POST http://localhost:3001/api/messages/send \
-H "Authorization: Bearer MTY3Mzk4NzIzNDM2Ny1leGFtcGxlLXRva2Vu" \
-H "Content-Type: application/json" \
-d '{"to": "50371234567", "message": "Hola mundo!"}'
```
### 3. Recibir Mensaje en n8n
El Gateway enviará automáticamente al webhook configurado cuando llegue un mensaje a WhatsApp.
---
**🎯 Perfecto para n8n: API simple, segura, sin dependencias de Baileys.**

366
README.md Normal file
View File

@@ -0,0 +1,366 @@
# WhatsApp Gateway - Arquitectura Profesional
## 🏗️ Arquitectura del Sistema
```
┌──────────────────────────────┐
│ Manager Web │
│ (React + TypeScript) │
│ - QR Display │
│ - Status Dashboard │
│ - Control Interface │
└──────────────┬───────────────┘
│ HTTP/WebSocket
┌──────────────▼───────────────┐
│ WhatsApp Gateway (Node) │
│ Baileys │
│ - Session Management │
│ - QR Generation │
│ - Message Handling │
│ - WebSocket Events │
└──────────────┬───────────────┘
│ REST API
┌──────────────▼───────────────┐
│ Core API Layer │
│ - Send Messages │
│ - Bulk Operations │
│ - Status Monitoring │
│ - Health Checks │
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ File Storage │
│ - Session Credentials │
│ - Auth Information │
└──────────────────────────────┘
```
## 🚀 Inicio Rápido
### 1. Instalar Dependencias
```bash
# Gateway (Backend)
cd gateway
npm install
# Manager Web (Frontend)
cd ../manager
npm install
```
### 2. Configurar Variables de Entorno
```bash
# gateway/.env
PORT=3001
NODE_ENV=production
SESSION_ID=default
LOG_LEVEL=info
```
### 3. Iniciar Servicios
```bash
# Terminal 1: Gateway
cd gateway
npm run dev
# Terminal 2: Manager Web
cd manager
npm run dev
```
### 4. Acceder a la Interfaz
- **Manager Web**: http://localhost:3002
- **API Gateway**: http://localhost:3001
- **WebSocket**: ws://localhost:3003
## 📡 API Endpoints
### Enviar Mensaje
```http
POST /api/send
Content-Type: application/json
{
"jid": "1234567890@s.whatsapp.net",
"content": "Hola desde el Gateway!",
"type": "text"
}
```
### Enviar Mensajes Masivos
```http
POST /api/send/bulk
Content-Type: application/json
{
"messages": [
{
"jid": "1234567890@s.whatsapp.net",
"content": "Mensaje 1",
"type": "text"
},
{
"jid": "0987654321@s.whatsapp.net",
"content": "Mensaje 2",
"type": "text"
}
]
}
```
### Ver Estado
```http
GET /api/status
```
### Health Check
```http
GET /api/health
```
## 🔧 Configuración para VPS
### Systemd Service (Gateway)
```ini
# /etc/systemd/system/whatsapp-gateway.service
[Unit]
Description=WhatsApp Gateway Service
After=network.target
[Service]
Type=simple
User=whatsapp
WorkingDirectory=/opt/whatsapp-gateway
ExecStart=/usr/bin/node dist/index.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3001
[Install]
WantedBy=multi-user.target
```
### Nginx Reverse Gateway
```nginx
# /etc/nginx/sites-available/whatsapp-gateway
server {
listen 80;
server_name tu-dominio.com;
# Manager Web
location / {
proxy_pass http://localhost:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# API Gateway
location /api/ {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# WebSocket
location /ws {
proxy_pass http://localhost:3003;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
```
### Docker Compose
```yaml
# docker-compose.yml
version: '3.8'
services:
whatsapp-gateway:
build: ./gateway
ports:
- "3001:3001"
- "3003:3003"
volumes:
- ./auth_info:/app/auth_info
environment:
- NODE_ENV=production
- PORT=3001
restart: unless-stopped
whatsapp-manager:
build: ./manager
ports:
- "3002:3002"
depends_on:
- whatsapp-gateway
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- whatsapp-gateway
- whatsapp-manager
restart: unless-stopped
```
## 🔐 Seguridad
### 1. Variables de Entorno
```bash
# Nunca exponer credenciales en el código
SESSION_ID=unique-session-id
API_KEY=your-api-key-here
WEBHOOK_SECRET=webhook-secret
```
### 2. CORS Configurado
```typescript
// Solo dominios confiables
cors({
origin: ['https://tu-dominio.com', 'https://manager.tu-dominio.com'],
credentials: true
})
```
### 3. Rate Limiting
```bash
# Nginx rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
```
## 📈 Monitoreo
### Logs Estructurados
```bash
# Ver logs en tiempo real
tail -f /var/log/whatsapp-gateway.log | jq '.'
```
### Métricas de Salud
```bash
# Health check endpoint
curl http://localhost:3001/api/health
```
### Uso de Recursos
```bash
# Monitorear memoria y CPU
htop
docker stats
```
## 🚀 Despliegue en VPS
### 1. Preparar Servidor
```bash
# Actualizar sistema
sudo apt update && sudo apt upgrade -y
# Instalar Node.js 18+
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Instalar PM2
sudo npm install -g pm2
```
### 2. Clonar y Configurar
```bash
# Clonar repositorio
git clone <tu-repo> whatsapp-gateway
cd whatsapp-gateway
# Instalar dependencias
cd gateway && npm install
cd ../manager && npm install
```
### 3. Iniciar con PM2
```bash
# Gateway
pm2 start gateway/dist/index.js --name whatsapp-gateway
# Manager Web (opcional, puede servir con nginx)
pm2 start "cd manager && npm run build && serve -s dist -l 3002" --name whatsapp-manager
# Guardar configuración
pm2 save
pm2 startup
```
### 4. Configurar Firewall
```bash
# Solo puertos necesarios
sudo ufw allow 22 # SSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
sudo ufw enable
```
## 🔄 Integración con n8n
### Webhook Configuration
```json
{
"method": "POST",
"path": "/webhook",
"webhookId": "whatsapp-events",
"responseMode": "onReceived"
}
```
### n8n Node Example
```javascript
// Enviar mensaje desde n8n
const response = await fetch('http://localhost:3001/api/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jid: $input.first().json.phone + '@s.whatsapp.net',
content: $input.first().json.message,
type: 'text'
})
});
```
## 🛠️ Troubleshooting
### Problemas Comunes
1. **QR no aparece**
- Verificar conexión WebSocket
- Revisar logs del Gateway
2. **Conexión se cae**
- Checar persistencia de sesión
- Verificar archivo de credenciales
3. **Mensajes no se envían**
- Validar formato JID
- Verificar estado de conexión
### Debug Mode
```bash
# Habilitar logs detallados
LOG_LEVEL=debug npm run dev
```
## 📝 Licencia
Este proyecto es para uso personal y educativo.

32
clean.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# WhatsApp Gateway - Cleanup Script
# Limpia sesiones, logs y cache
echo "🧹 Limpiando datos de WhatsApp Gateway..."
# 1. Limpiar sesión de WhatsApp (QR y vinculación)
if [ -d "gateway/auth_info" ]; then
echo "🗑️ Eliminando datos de sesión (gateway/auth_info)..."
rm -rf gateway/auth_info/*
# Mantener el directorio pero vacío
touch gateway/auth_info/.keep
else
mkdir -p gateway/auth_info
fi
# 2. Limpiar logs
if [ -f "gateway/gateway.log" ]; then
echo "🗑️ Eliminando log principal..."
rm -f gateway/gateway.log
fi
# 3. Limpiar logs de PM2 (si existen)
if command -v pm2 &> /dev/null; then
echo "🗑️ Limpiando logs de PM2..."
pm2 flush > /dev/null 2>&1 || true
fi
echo "✅ Limpieza completada!"
echo "👉 Ahora puedes reiniciar los servicios:"
echo " cd gateway && npm run dev"

308
deploy.sh Executable file
View File

@@ -0,0 +1,308 @@
#!/bin/bash
# WhatsApp Gateway - Deployment Script
# Para VPS Ubuntu/Debian
set -e
echo "🚀 WhatsApp Gateway Deployment Script"
echo "======================================"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
if [[ $EUID -eq 0 ]]; then
log_error "This script should not be run as root!"
exit 1
fi
# Update system
log_info "Updating system packages..."
sudo apt update && sudo apt upgrade -y
# Install Node.js
log_info "Installing Node.js 18+..."
if ! command -v node &> /dev/null; then
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
else
log_warn "Node.js already installed: $(node --version)"
fi
# Install PM2
log_info "Installing PM2 process manager..."
if ! command -v pm2 &> /dev/null; then
sudo npm install -g pm2
else
log_warn "PM2 already installed: $(pm2 --version)"
fi
# Install Nginx (optional)
log_info "Installing Nginx..."
if ! command -v nginx &> /dev/null; then
sudo apt install -y nginx
else
log_warn "Nginx already installed: $(nginx -v 2>&1)"
fi
# Create deployment directory
DEPLOY_DIR="/opt/whatsapp-gateway"
log_info "Creating deployment directory: $DEPLOY_DIR"
sudo mkdir -p $DEPLOY_DIR
sudo chown $USER:$USER $DEPLOY_DIR
# Copy files (assuming script is run from project root)
log_info "Copying project files..."
cp -r gateway $DEPLOY_DIR/
cp -r manager $DEPLOY_DIR/
cp README.md $DEPLOY_DIR/
# Install dependencies
log_info "Installing Gateway dependencies..."
cd $DEPLOY_DIR/gateway
npm install
log_info "Installing Manager dependencies..."
cd $DEPLOY_DIR/manager
npm install
# Build Manager
log_info "Building Manager Web interface..."
npm run build
# Create PM2 ecosystem file
log_info "Creating PM2 ecosystem configuration..."
cat > $DEPLOY_DIR/ecosystem.config.js << 'EOF'
module.exports = {
apps: [
{
name: 'whatsapp-gateway',
script: './gateway/dist/index.js',
cwd: '/opt/whatsapp-gateway',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 3001
},
error_file: '/var/log/whatsapp-gateway-error.log',
out_file: '/var/log/whatsapp-gateway-out.log',
log_file: '/var/log/whatsapp-gateway-combined.log',
time: true
},
{
name: 'whatsapp-manager',
script: 'serve',
cwd: '/opt/whatsapp-gateway/manager',
args: '-s dist -l 3002',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '500M',
error_file: '/var/log/whatsapp-manager-error.log',
out_file: '/var/log/whatsapp-manager-out.log',
log_file: '/var/log/whatsapp-manager-combined.log',
time: true
}
]
};
EOF
# Install serve for Manager
cd $DEPLOY_DIR/manager
npm install serve
# Build Gateway
log_info "Building Gateway..."
cd $DEPLOY_DIR/gateway
npm run build
# Create auth_info directory
mkdir -p $DEPLOY_DIR/gateway/auth_info
# Set permissions
chmod +x $DEPLOY_DIR/gateway/auth_info
# Create log directory
sudo mkdir -p /var/log/whatsapp-gateway
sudo chown $USER:$USER /var/log/whatsapp-gateway
# Start services with PM2
log_info "Starting services with PM2..."
cd $DEPLOY_DIR
pm2 start ecosystem.config.js
# Save PM2 configuration
pm2 save
pm2 startup
# Configure Nginx
log_info "Configuring Nginx..."
sudo tee /etc/nginx/sites-available/whatsapp-gateway << 'EOF'
server {
listen 80;
server_name _;
# Manager Web
location / {
proxy_pass http://localhost:3002;
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;
}
# API Gateway
location /api/ {
proxy_pass http://localhost:3001;
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;
}
# WebSocket
location /ws {
proxy_pass http://localhost:3003;
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;
}
}
EOF
# Enable Nginx site
sudo ln -sf /etc/nginx/sites-available/whatsapp-gateway /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
# Test Nginx configuration
sudo nginx -t
# Restart Nginx
sudo systemctl restart nginx
sudo systemctl enable nginx
# Configure firewall
log_info "Configuring firewall..."
sudo ufw --force reset
sudo ufw allow 22 # SSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
sudo ufw --force enable
# Create status script
log_info "Creating status monitoring script..."
cat > $DEPLOY_DIR/status.sh << 'EOF'
#!/bin/bash
echo "🔍 WhatsApp Gateway Status"
echo "=========================="
# Check PM2 processes
echo -e "\n📊 PM2 Processes:"
pm2 status
# Check service health
echo -e "\n🏥 Service Health:"
curl -s http://localhost:3001/api/health | jq '.' 2>/dev/null || echo "❌ Gateway API not responding"
# Check logs
echo -e "\n📝 Recent Logs:"
pm2 logs whatsapp-gateway --lines 10 --nostream
# Check system resources
echo -e "\n💻 System Resources:"
echo "Memory: $(free -h | grep '^Mem:' | awk '{print $3 "/" $2}')"
echo "Disk: $(df -h / | tail -1 | awk '{print $3 "/" $2}')"
echo "Uptime: $(uptime -p)"
EOF
chmod +x $DEPLOY_DIR/status.sh
# Create update script
log_info "Creating update script..."
cat > $DEPLOY_DIR/update.sh << 'EOF'
#!/bin/bash
echo "🔄 Updating WhatsApp Gateway..."
echo "=============================="
# Backup current version
BACKUP_DIR="/opt/whatsapp-gateway-backup-$(date +%Y%m%d-%H%M%S)"
sudo mkdir -p $BACKUP_DIR
sudo cp -r /opt/whatsapp-gateway/* $BACKUP_DIR/
# Pull latest changes (if git repo)
cd /opt/whatsapp-gateway
if [ -d ".git" ]; then
git pull origin main
fi
# Rebuild and restart
cd gateway
npm install
npm run build
cd ../manager
npm install
npm run build
# Restart services
pm2 restart whatsapp-gateway whatsapp-manager
echo "✅ Update completed!"
echo "📁 Backup available at: $BACKUP_DIR"
EOF
chmod +x $DEPLOY_DIR/update.sh
# Final status
log_info "Deployment completed successfully!"
echo ""
echo -e "${GREEN}🎉 WhatsApp Gateway is now running!${NC}"
echo ""
echo "📍 Access URLs:"
echo " Manager Web: http://$(curl -s ifconfig.me)/"
echo " API: http://$(curl -s ifconfig.me)/api/"
echo " WebSocket: ws://$(curl -s ifconfig.me)/ws"
echo ""
echo "🔧 Management Commands:"
echo " Status: $DEPLOY_DIR/status.sh"
echo " Update: $DEPLOY_DIR/update.sh"
echo " PM2: pm2 status | pm2 logs | pm2 restart"
echo ""
echo "📁 Important Paths:"
echo " Project: $DEPLOY_DIR"
echo " Logs: /var/log/whatsapp-gateway/"
echo " Auth: $DEPLOY_DIR/gateway/auth_info/"
echo ""
echo -e "${YELLOW}⚠️ Next Steps:${NC}"
echo " 1. Open Manager Web in browser"
echo " 2. Scan QR code with WhatsApp"
echo " 3. Test API endpoints"
echo " 4. Configure n8n integration"
echo ""
echo -e "${GREEN}✅ All services are running and ready!${NC}"

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
version: "3.8"
services:
gateway:
build:
context: ./gateway
image: whatsapp-gateway
container_name: whatsapp-gateway
restart: always
ports:
- "3001:3001" # API Port
- "3003:3003" # WebSocket Port
environment:
- PORT=3001
- SESSION_ID=default
- WEBHOOK_URL=http://n8n:5678/webhook/whatsapp # Example for n8n container
volumes:
- ./gateway/auth_info:/app/auth_info # Persist WhatsApp Session
- ./gateway/gateway.log:/app/gateway.log
networks:
- whatsapp-network
manager:
build:
context: ./manager
image: whatsapp-manager
container_name: whatsapp-manager
restart: always
ports:
- "3002:80" # Web Interface
depends_on:
- gateway
networks:
- whatsapp-network
networks:
whatsapp-network:
driver: bridge

7
gateway/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
dist/
*.log
.env.local
.env.*.local
auth_info/*.json
!auth_info/.gitkeep

18
gateway/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm install
# Build
COPY . .
RUN npm run build
# Expose ports
EXPOSE 3001
EXPOSE 3003
# Command
CMD ["npm", "start"]

5118
gateway/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
gateway/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "whatsapp-gateway",
"version": "1.0.0",
"description": "Professional WhatsApp Gateway with Baileys",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint src --ext .ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@whiskeysockets/baileys": "^7.0.0-rc.9",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"pino": "^8.16.2",
"pino-pretty": "^10.2.3",
"qrcode": "^1.5.3",
"ws": "^8.14.2"
},
"devDependencies": {
"@types/cors": "^2.8.15",
"@types/express": "^4.17.20",
"@types/node": "^20.8.7",
"@types/qrcode": "^1.5.5",
"@types/ws": "^8.5.8",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"eslint": "^8.52.0",
"tsx": "^4.1.4",
"typescript": "^5.2.2"
}
}

108
gateway/src/api/messages.ts Normal file
View File

@@ -0,0 +1,108 @@
import { Request, Response } from 'express';
import { logger } from '../config/logger';
import { ApiResponse } from './send';
interface MessageData {
id: string;
from: string;
to: string;
content: string;
timestamp: string;
type: 'text' | 'image' | 'document';
}
interface RecentMessages {
messages: MessageData[];
total: number;
}
export class MessagesController {
private recentMessages: MessageData[] = [];
private maxMessages = 50;
constructor() {
// Setup message storage
}
addMessage = (messageData: MessageData): void => {
this.recentMessages.unshift(messageData);
if (this.recentMessages.length > this.maxMessages) {
this.recentMessages = this.recentMessages.slice(0, this.maxMessages);
}
logger.info(`Message added to recent messages: ${messageData.id}`);
};
getRecentMessages = async (req: Request, res: Response): Promise<void> => {
try {
const limit = parseInt(req.query.limit as string) || 10;
const response: ApiResponse<RecentMessages> = {
success: true,
data: {
messages: this.recentMessages.slice(0, limit),
total: this.recentMessages.length
},
timestamp: new Date().toISOString()
};
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Get recent messages failed: ${error}`);
res.status(500).json(response);
}
};
clearMessages = async (req: Request, res: Response): Promise<void> => {
try {
this.recentMessages = [];
const response: ApiResponse = {
success: true,
data: { message: 'Messages cleared successfully' },
timestamp: new Date().toISOString()
};
logger.info('Recent messages cleared');
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Clear messages failed: ${error}`);
res.status(500).json(response);
}
};
getMessageCount = (): number => {
return this.recentMessages.length;
};
// Método para ser llamado desde el Gateway cuando llega un mensaje
handleMessage = (message: any): void => {
try {
const messageData: MessageData = {
id: message.key.id || Date.now().toString(),
from: message.key.remoteJid || 'unknown',
to: 'me',
content: message.message?.conversation || message.message?.extendedTextMessage?.text || '(No text content)',
timestamp: new Date().toISOString(),
type: message.message?.conversation ? 'text' : 'document'
};
this.addMessage(messageData);
} catch (error) {
logger.error(`Failed to process message: ${error}`);
}
};
}

228
gateway/src/api/n8n.ts Normal file
View File

@@ -0,0 +1,228 @@
import { Request, Response } from 'express';
import { logger } from '../config/logger';
import { AuthMiddleware } from '../middleware/auth';
interface n8nSendMessageRequest {
to: string;
message: string;
}
interface n8nMessageResponse {
success: boolean;
messageId?: string;
to: string;
message: string;
timestamp: string;
error?: string;
}
export class n8nController {
private whatsappClient: any;
constructor(whatsappClient: any) {
this.whatsappClient = whatsappClient;
}
validatePhoneNumber(phone: string): { valid: boolean; formatted: string } {
// Allow Group JIDs
if (phone.endsWith('@g.us')) {
return { valid: true, formatted: phone };
}
// Remove non-numeric characters
let cleaned = phone.replace(/[^\d]/g, '');
// Basic validation for different formats
if (cleaned.length < 10) {
return { valid: false, formatted: phone };
}
// Ensure country code (if starting with 0, remove it and add country code if needed)
if (cleaned.startsWith('0')) {
cleaned = cleaned.substring(1);
}
// Add country code if not present (assuming El Salvador by default: 503)
if (cleaned.length === 8 && !cleaned.startsWith('503')) {
cleaned = '503' + cleaned;
}
// Validate reasonable length
if (cleaned.length < 10 || cleaned.length > 15) {
return { valid: false, formatted: phone };
}
return { valid: true, formatted: cleaned + '@s.whatsapp.net' };
}
getGroups = async (req: Request, res: Response): Promise<void> => {
try {
const groups = await this.whatsappClient.getGroups();
const simplifiedGroups = groups.map((g: any) => ({
id: g.id,
subject: g.subject,
description: g.desc,
participants: g.participants?.length || 0,
creation: g.creation
}));
res.json({
success: true,
count: simplifiedGroups.length,
groups: simplifiedGroups,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
});
}
};
sendMessage = async (req: Request, res: Response): Promise<void> => {
try {
const { to, message }: n8nSendMessageRequest = req.body;
if (!to || !message) {
const response: n8nMessageResponse = {
success: false,
to: to || '(missing)',
message: message || '(missing)',
timestamp: new Date().toISOString(),
error: 'Both "to" and "message" fields are required'
};
res.status(400).json(response);
return;
}
// Validate phone number
const phoneValidation = this.validatePhoneNumber(to);
if (!phoneValidation.valid) {
const response: n8nMessageResponse = {
success: false,
to,
message: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
timestamp: new Date().toISOString(),
error: `Invalid phone number format: ${to}`
};
res.status(400).json(response);
return;
}
// Check WhatsApp connection
const connectionState = this.whatsappClient.getConnectionState();
if (connectionState !== 'connected') {
const response: n8nMessageResponse = {
success: false,
to,
message: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
timestamp: new Date().toISOString(),
error: 'WhatsApp not connected. Please scan QR code first.'
};
res.status(503).json(response);
return;
}
// Send message
const result = await this.whatsappClient.sendMessage(phoneValidation.formatted, message, 'text');
const response: n8nMessageResponse = {
success: true,
messageId: result.key?.id || 'unknown',
to: to,
message: message,
timestamp: new Date().toISOString()
};
logger.info(`n8n message sent to ${to}: ${message.substring(0, 50)}...`);
res.json(response);
} catch (error) {
const response: n8nMessageResponse = {
success: false,
to: req.body.to || '(unknown)',
message: req.body.message || '(unknown)',
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error'
};
logger.error(`n8n send message failed: ${error}`);
res.status(500).json(response);
}
};
getStatus = async (req: Request, res: Response): Promise<void> => {
try {
const connectionState = this.whatsappClient.getConnectionState();
const sessionId = this.whatsappClient.sessionStore?.['sessionId'] || 'default';
const response = {
success: true,
status: connectionState,
sessionId,
uptime: process.uptime(),
timestamp: new Date().toISOString(),
capabilities: {
send: connectionState === 'connected',
receive: true,
webhook: true
}
};
res.json(response);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
});
}
};
generateToken = async (req: Request, res: Response): Promise<void> => {
try {
const permissions = ['send', 'status', 'messages'];
const token = AuthMiddleware.generateToken(permissions);
const response = {
success: true,
token: token,
permissions: permissions,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
timestamp: new Date().toISOString()
};
logger.info('n8n token generated');
res.json(response);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
});
}
};
validateToken = async (req: Request, res: Response): Promise<void> => {
try {
const token = (req as any).token;
const tokenInfo = AuthMiddleware.getTokenInfo(token);
res.json({
success: true,
token: token,
valid: tokenInfo ? !tokenInfo.expired : false,
info: tokenInfo,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
});
}
};
}

110
gateway/src/api/send.ts Normal file
View File

@@ -0,0 +1,110 @@
import { Request, Response } from 'express';
import { logger } from '../config/logger';
export interface SendMessageRequest {
jid: string;
content: string;
type?: 'text' | 'image' | 'document';
}
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
timestamp: string;
}
export class SendController {
private whatsappClient: any;
constructor(whatsappClient: any) {
this.whatsappClient = whatsappClient;
}
sendMessage = async (req: Request, res: Response): Promise<void> => {
try {
const { jid, content, type = 'text' }: SendMessageRequest = req.body;
if (!jid || !content) {
const response: ApiResponse = {
success: false,
error: 'jid and content are required',
timestamp: new Date().toISOString()
};
res.status(400).json(response);
return;
}
const result = await this.whatsappClient.sendMessage(jid, content, type);
const response: ApiResponse = {
success: true,
data: { messageId: result.key.id, status: 'sent' },
timestamp: new Date().toISOString()
};
logger.info(`Message sent to ${jid}`);
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Send message failed: ${error}`);
res.status(500).json(response);
}
};
sendBulk = async (req: Request, res: Response): Promise<void> => {
try {
const { messages }: { messages: SendMessageRequest[] } = req.body;
if (!Array.isArray(messages) || messages.length === 0) {
const response: ApiResponse = {
success: false,
error: 'messages array is required',
timestamp: new Date().toISOString()
};
res.status(400).json(response);
return;
}
const results = await Promise.allSettled(
messages.map(msg => this.whatsappClient.sendMessage(msg.jid, msg.content, msg.type))
);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
const response: ApiResponse = {
success: true,
data: {
total: messages.length,
successful,
failed,
results: results.map((r, i) => ({
index: i,
status: r.status,
data: r.status === 'fulfilled' ? r.value.key.id : null,
error: r.status === 'rejected' ? r.reason : null
}))
},
timestamp: new Date().toISOString()
};
logger.info(`Bulk send completed: ${successful}/${messages.length} successful`);
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Bulk send failed: ${error}`);
res.status(500).json(response);
}
};
}

106
gateway/src/api/session.ts Normal file
View File

@@ -0,0 +1,106 @@
import { Request, Response } from 'express';
import { logger } from '../config/logger';
import { ApiResponse } from './send';
export class SessionController {
private whatsappClient: any;
constructor(whatsappClient: any) {
this.whatsappClient = whatsappClient;
}
restartSession = async (req: Request, res: Response): Promise<void> => {
try {
logger.info('Restarting WhatsApp session');
// Disconnect current session
await this.whatsappClient.disconnect();
// Delete session credentials
this.whatsappClient.sessionStore?.delete();
// Reconnect after a short delay
setTimeout(async () => {
try {
await this.whatsappClient.connect();
logger.info('Session restarted successfully');
} catch (error) {
logger.error(`Failed to restart session: ${error}`);
}
}, 1000);
const response: ApiResponse = {
success: true,
data: { message: 'Session restart initiated' },
timestamp: new Date().toISOString()
};
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Session restart failed: ${error}`);
res.status(500).json(response);
}
};
logoutSession = async (req: Request, res: Response): Promise<void> => {
try {
logger.info('Logging out WhatsApp session');
await this.whatsappClient.disconnect();
this.whatsappClient.sessionStore?.delete();
const response: ApiResponse = {
success: true,
data: { message: 'Session logged out successfully' },
timestamp: new Date().toISOString()
};
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Session logout failed: ${error}`);
res.status(500).json(response);
}
};
generateToken = async (req: Request, res: Response): Promise<void> => {
try {
const token = Buffer.from(`${Date.now()}-${Math.random().toString(36).substring(2)}`).toString('base64');
const tokenData = {
token,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
permissions: ['send', 'bulk', 'status', 'messages']
};
const response: ApiResponse = {
success: true,
data: tokenData,
timestamp: new Date().toISOString()
};
logger.info('API token generated');
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Token generation failed: ${error}`);
res.status(500).json(response);
}
};
}

60
gateway/src/api/status.ts Normal file
View File

@@ -0,0 +1,60 @@
import { Request, Response } from 'express';
import { logger } from '../config/logger';
import { ApiResponse } from './send';
export class StatusController {
private whatsappClient: any;
constructor(whatsappClient: any) {
this.whatsappClient = whatsappClient;
}
getStatus = async (req: Request, res: Response): Promise<void> => {
try {
const connectionState = this.whatsappClient.getConnectionState();
const sessionId = this.whatsappClient.sessionStore?.['sessionId'] || 'default';
const response: ApiResponse = {
success: true,
data: {
status: connectionState,
sessionId,
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
},
timestamp: new Date().toISOString()
};
res.json(response);
} catch (error) {
const response: ApiResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
};
logger.error(`Get status failed: ${error}`);
res.status(500).json(response);
}
};
getHealth = async (req: Request, res: Response): Promise<void> => {
try {
const isHealthy = this.whatsappClient.getConnectionState() === 'connected';
res.status(isHealthy ? 200 : 503).json({
healthy: isHealthy,
status: isHealthy ? 'ok' : 'degraded',
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(503).json({
healthy: false,
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
});
}
};
}

151
gateway/src/api/webhook.ts Normal file
View File

@@ -0,0 +1,151 @@
import { Request, Response } from 'express';
import { logger } from '../config/logger';
interface WebhookMessage {
from: string;
body: string;
timestamp: string;
type: 'text' | 'image' | 'document';
messageId: string;
}
interface WebhookResponse {
success: boolean;
received: boolean;
timestamp: string;
error?: string;
}
export class WebhookController {
private webhookUrl?: string;
private enabled: boolean = false;
constructor(webhookUrl?: string) {
this.webhookUrl = webhookUrl;
this.enabled = !!webhookUrl;
}
configureWebhook = async (req: Request, res: Response): Promise<void> => {
try {
const { url, enabled = true } = req.body;
if (url) {
this.webhookUrl = url;
this.enabled = enabled;
// Test webhook
const testPayload = {
event: 'webhook_configured',
timestamp: new Date().toISOString(),
message: 'Webhook configured successfully'
};
await this.sendWebhook(testPayload);
}
const response = {
success: true,
webhookUrl: this.webhookUrl,
enabled: this.enabled,
timestamp: new Date().toISOString()
};
res.json(response);
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
});
}
};
receiveMessage = async (messageData: any): Promise<void> => {
if (!this.enabled || !this.webhookUrl) {
logger.info('Webhook not configured, skipping message forwarding');
return;
}
try {
const webhookMessage: WebhookMessage = {
from: messageData.from || 'unknown',
body: messageData.content || messageData.message || '',
timestamp: messageData.timestamp || new Date().toISOString(),
type: messageData.type || 'text',
messageId: messageData.id || Date.now().toString()
};
await this.sendWebhook(webhookMessage);
logger.info(`Message forwarded to webhook: ${webhookMessage.from}`);
} catch (error) {
logger.error(`Failed to forward message to webhook: ${error}`);
}
};
private async sendWebhook(payload: any): Promise<void> {
const response = await fetch(this.webhookUrl!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'WhatsApp-Gateway-Webhook/1.0'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
}
}
testWebhook = async (req: Request, res: Response): Promise<void> => {
try {
if (!this.enabled || !this.webhookUrl) {
res.status(400).json({
success: false,
error: 'Webhook not configured',
timestamp: new Date().toISOString()
});
return;
}
const testPayload = {
event: 'test',
from: '50312345678@s.whatsapp.net',
body: 'Test message from WhatsApp Gateway',
timestamp: new Date().toISOString(),
type: 'text',
messageId: 'test-' + Date.now()
};
await this.sendWebhook(testPayload);
const response: WebhookResponse = {
success: true,
received: true,
timestamp: new Date().toISOString()
};
res.json(response);
} catch (error) {
const response: WebhookResponse = {
success: false,
received: false,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error'
};
res.status(500).json(response);
}
};
getWebhookStatus = async (req: Request, res: Response): Promise<void> => {
const response = {
success: true,
configured: this.enabled,
webhookUrl: this.webhookUrl || null,
timestamp: new Date().toISOString()
};
res.json(response);
};
}

View File

@@ -0,0 +1,13 @@
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
});

293
gateway/src/index.ts Normal file
View File

@@ -0,0 +1,293 @@
import express from 'express';
//import cors from 'cors';
import dotenv from 'dotenv';
import { WhatsAppClient } from './whatsapp/client';
import { QRSocketServer } from './sockets/qr.socket';
import { SendController } from './api/send';
import { StatusController } from './api/status';
import { SessionController } from './api/session';
import { MessagesController } from './api/messages';
import { n8nController } from './api/n8n';
import { WebhookController } from './api/webhook';
import { AuthMiddleware } from './middleware/auth';
import { logger } from './config/logger';
dotenv.config();
class WhatsAppGateway {
private app: express.Application;
private whatsappClient!: WhatsAppClient;
private socketServer!: QRSocketServer;
private sendController!: SendController;
private statusController!: StatusController;
private sessionController!: SessionController;
private messagesController!: MessagesController;
private n8nController!: n8nController;
private webhookController!: WebhookController;
constructor() {
this.app = express();
this.setupMiddleware();
this.initializeComponents();
this.setupRoutes();
}
private setupMiddleware(): void {
// Configure CORS for n8n and Manager
const corsOrigins = [
...(process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : ['http://localhost:3002']),
'http://localhost:3004', // Add 3004 for current setup
'http://localhost:5678', // n8n default
'http://localhost:5679', // n8n alternative
...(process.env.N8N_ORIGINS ? process.env.N8N_ORIGINS.split(',') : [])
];
// ⚠️ SOLUCIÓN BRUTA — SOLO DEV
this.app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Intercepta preflight
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
/* this.app.use(cors({
origin: corsOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));*/
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true }));
}
private initializeComponents(): void {
const sessionId = process.env.SESSION_ID || 'default';
this.whatsappClient = new WhatsAppClient(sessionId);
this.socketServer = new QRSocketServer(3003);
this.sendController = new SendController(this.whatsappClient);
this.statusController = new StatusController(this.whatsappClient);
this.sessionController = new SessionController(this.whatsappClient);
this.messagesController = new MessagesController();
this.n8nController = new n8nController(this.whatsappClient);
this.webhookController = new WebhookController(process.env.WEBHOOK_URL);
this.setupWhatsAppEvents();
this.connectWhatsApp();
this.setupWebSocketCommands();
// Send current state when new clients connect
this.socketServer.onClientConnect = () => {
const currentState = this.whatsappClient.getConnectionState();
this.socketServer.sendStatus(currentState);
};
}
private setupWebSocketCommands(): void {
// Handle WebSocket commands from Manager
this.socketServer.onCommand = async (command: string, data?: any) => {
logger.info(`Received WebSocket command: ${command}`);
switch (command) {
case 'restart_session':
await this.sessionController.restartSession({} as any, { json: () => {} } as any);
break;
case 'logout_session':
await this.sessionController.logoutSession({} as any, { json: () => {} } as any);
break;
case 'generate_token':
const tokenResponse = await this.sessionController.generateToken({} as any, { json: (data: any) => {
this.socketServer.broadcast({
type: 'token_generated',
data: JSON.stringify(data)
});
} } as any);
break;
case 'get_recent_messages':
await this.messagesController.getRecentMessages({} as any, { json: (data: any) => {
this.socketServer.broadcast({
type: 'status',
data: JSON.stringify(data)
});
} } as any);
break;
default:
logger.warn(`Unknown WebSocket command: ${command}`);
}
};
}
private setupWhatsAppEvents(): void {
this.whatsappClient.onQR((qr: string) => {
this.socketServer.sendQR(qr);
});
this.whatsappClient.onStatus((status: string) => {
this.socketServer.sendStatus(status);
logger.info(`WhatsApp status: ${status}`);
});
this.whatsappClient.onMessage(async (message: any) => {
try {
logger.info('Message received via WhatsApp');
if (!message || !message.key) {
logger.warn('Invalid message structure received');
return;
}
// Store in recent messages
this.messagesController.handleMessage(message);
// Forward to webhook
await this.webhookController.receiveMessage({
id: message.key.id || Date.now().toString(),
from: message.key.remoteJid || 'unknown',
content: message.message?.conversation || message.message?.extendedTextMessage?.text || '',
type: message.message?.conversation ? 'text' : 'document',
timestamp: new Date().toISOString()
});
// Notify Manager
this.socketServer.broadcast({
type: 'status',
data: 'message_received'
});
} catch (error) {
logger.error(`Failed to process message: ${error}`);
}
});
}
private async connectWhatsApp(): Promise<void> {
try {
await this.whatsappClient.connect();
logger.info('WhatsApp client initialized');
} catch (error) {
logger.error(`Failed to initialize WhatsApp client: ${error}`);
}
}
private setupRoutes(): void {
// Legacy API Routes (Manager)
this.app.post('/api/send', this.sendController.sendMessage);
this.app.post('/api/send/bulk', this.sendController.sendBulk);
this.app.get('/api/status', this.statusController.getStatus);
this.app.get('/api/health', this.statusController.getHealth);
// Session management (Manager)
this.app.post('/api/session/restart', this.sessionController.restartSession);
this.app.post('/api/session/logout', this.sessionController.logoutSession);
this.app.post('/api/token', this.sessionController.generateToken);
// Messages (Manager)
this.app.get('/api/messages', this.messagesController.getRecentMessages);
this.app.delete('/api/messages', this.messagesController.clearMessages);
// n8n API Core (Component 3)
this.app.post('/api/messages/send', AuthMiddleware.middleware('send'), this.n8nController.sendMessage);
this.app.get('/api/groups', AuthMiddleware.middleware('messages'), this.n8nController.getGroups);
this.app.get('/api/status', AuthMiddleware.middleware('status'), this.n8nController.getStatus);
this.app.post('/api/n8n/token', AuthMiddleware.middleware('status'), this.n8nController.validateToken);
// Public n8n token generation (no auth required)
this.app.post('/api/n8n/generate-token', this.n8nController.generateToken);
// Webhooks for n8n (receiving messages)
this.app.post('/webhook/whatsapp', (req, res) => {
// This endpoint is for n8n to receive messages FROM WhatsApp
res.json({ success: true, message: 'Webhook endpoint active' });
});
// Webhook configuration
this.app.post('/api/webhook/configure', AuthMiddleware.middleware('messages'), this.webhookController.configureWebhook);
this.app.get('/api/webhook/status', AuthMiddleware.middleware('messages'), this.webhookController.getWebhookStatus);
this.app.post('/api/webhook/test', AuthMiddleware.middleware('messages'), this.webhookController.testWebhook);
// Root endpoint
this.app.get('/', (req, res) => {
res.json({
name: 'WhatsApp Gateway',
version: '1.0.0',
status: 'running',
endpoints: {
// Manager endpoints
send: 'POST /api/send',
sendBulk: 'POST /api/send/bulk',
status: 'GET /api/status',
health: 'GET /api/health',
sessionRestart: 'POST /api/session/restart',
sessionLogout: 'POST /api/session/logout',
token: 'POST /api/token',
messages: 'GET /api/messages',
websocket: 'ws://localhost:3003',
// n8n API Core endpoints
n8nSend: 'POST /api/messages/send (Bearer auth required)',
n8nStatus: 'GET /api/status (Bearer auth required)',
n8nToken: 'POST /api/n8n/generate-token',
webhook: 'POST /webhook/whatsapp',
webhookConfig: 'POST /api/webhook/configure',
webhookTest: 'POST /api/webhook/test'
},
authentication: {
type: 'Bearer Token',
header: 'Authorization: Bearer <token>',
generate: 'POST /api/n8n/generate-token'
}
});
});
// Error handling
this.app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error(`Unhandled error: ${err}`);
res.status(500).json({
success: false,
error: 'Internal server error',
timestamp: new Date().toISOString()
});
});
}
async start(): Promise<void> {
const port = parseInt(process.env.PORT || '3001');
this.app.listen(port, () => {
logger.info(`WhatsApp Gateway API running on port ${port}`);
logger.info(`WebSocket server running on port 3003`);
logger.info(`Manager Web should connect to: http://localhost:3002`);
});
}
async stop(): Promise<void> {
await this.whatsappClient.disconnect();
this.socketServer.close();
logger.info('WhatsApp Gateway stopped');
}
}
// Start the gateway
const gateway = new WhatsAppGateway();
gateway.start().catch(error => {
logger.error(`Failed to start gateway: ${error}`);
process.exit(1);
});
// Graceful shutdown
process.on('SIGINT', async () => {
logger.info('Received SIGINT, shutting down gracefully...');
await gateway.stop();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('Received SIGTERM, shutting down gracefully...');
await gateway.stop();
process.exit(0);
});

View File

@@ -0,0 +1,130 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from '../config/logger';
interface TokenStore {
[token: string]: {
createdAt: string;
expiresAt: string;
permissions: string[];
};
}
// In-memory token store (en producción usar Redis/DB)
const tokenStore: TokenStore = {};
export class AuthMiddleware {
static generateToken(permissions: string[] = ['send', 'status', 'messages']): string {
const token = Buffer.from(`${Date.now()}-${Math.random().toString(36).substring(2)}`).toString('base64');
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24 horas
tokenStore[token] = {
createdAt: new Date().toISOString(),
expiresAt,
permissions
};
logger.info(`Token generated: ${token.substring(0, 20)}...`);
return token;
}
static validateToken(token: string): { valid: boolean; permissions?: string[] } {
const tokenData = tokenStore[token];
if (!tokenData) {
logger.warn(`Invalid token: ${token.substring(0, 20)}...`);
return { valid: false };
}
if (new Date() > new Date(tokenData.expiresAt)) {
logger.warn(`Expired token: ${token.substring(0, 20)}...`);
delete tokenStore[token];
return { valid: false };
}
return { valid: true, permissions: tokenData.permissions };
}
static revokeToken(token: string): boolean {
if (tokenStore[token]) {
delete tokenStore[token];
logger.info(`Token revoked: ${token.substring(0, 20)}...`);
return true;
}
return false;
}
static getTokenInfo(token: string): any {
const tokenData = tokenStore[token];
if (!tokenData) return null;
return {
...tokenData,
expired: new Date() > new Date(tokenData.expiresAt)
};
}
static listTokens(): Array<{ token: string; info: any }> {
return Object.entries(tokenStore).map(([token, info]) => ({
token: token.substring(0, 20) + '...',
info
}));
}
static middleware(requiredPermission?: string) {
return (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
success: false,
error: 'Authorization header required',
timestamp: new Date().toISOString()
});
}
// Bearer token format: "Bearer xxxxxx"
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return res.status(401).json({
success: false,
error: 'Invalid authorization format. Expected: Bearer <token>',
timestamp: new Date().toISOString()
});
}
const token = parts[1];
const validation = AuthMiddleware.validateToken(token);
if (!validation.valid) {
return res.status(401).json({
success: false,
error: 'Invalid or expired token',
timestamp: new Date().toISOString()
});
}
if (requiredPermission && !validation.permissions?.includes(requiredPermission)) {
return res.status(403).json({
success: false,
error: `Insufficient permissions. Required: ${requiredPermission}`,
timestamp: new Date().toISOString()
});
}
// Add token info to request
(req as any).token = token;
(req as any).permissions = validation.permissions;
next();
} catch (error) {
logger.error(`Auth middleware error: ${error}`);
return res.status(500).json({
success: false,
error: 'Authentication error',
timestamp: new Date().toISOString()
});
}
};
}
}

View File

@@ -0,0 +1,115 @@
import { WebSocketServer } from 'ws';
import * as WebSocket from 'ws';
import { logger } from '../config/logger';
export interface QRSocketMessage {
type: 'qr' | 'status' | 'error' | 'token_generated';
data: string;
}
export class QRSocketServer {
private wss: WebSocketServer;
private clients: Set<WebSocket> = new Set();
public onCommand?: (command: string, data?: any) => void;
public onClientConnect?: () => void;
constructor(port: number) {
this.wss = new WebSocketServer({ port });
this.setupServer();
}
private setupServer(): void {
this.wss.on('connection', (ws: WebSocket) => {
logger.info('WebSocket client connected');
this.clients.add(ws);
ws.on('close', () => {
logger.info('WebSocket client disconnected');
this.clients.delete(ws);
});
ws.on('error', (error) => {
logger.error(`WebSocket error: ${error}`);
this.clients.delete(ws);
});
ws.on('message', (data: WebSocket.Data) => {
try {
const message = JSON.parse(data.toString());
if (message.type === 'command' && this.onCommand) {
this.onCommand(message.action, message.data);
}
} catch (error) {
logger.error(`Failed to parse WebSocket command: ${error}`);
}
});
// Send initial status
// Note: We send 'waiting' as default, the actual state will be sent by WhatsApp client events
this.sendToClient(ws, {
type: 'status',
data: 'waiting'
});
// Notify that a new client connected
if (this.onClientConnect) {
this.onClientConnect();
}
});
logger.info(`WebSocket server running on port ${this.wss.options.port}`);
}
broadcast(message: QRSocketMessage): void {
const messageStr = JSON.stringify(message);
this.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(messageStr);
} catch (error) {
logger.error(`Failed to send message to client: ${error}`);
this.clients.delete(client);
}
}
});
}
private sendToClient(client: WebSocket, message: QRSocketMessage): void {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(JSON.stringify(message));
} catch (error) {
logger.error(`Failed to send message to client: ${error}`);
}
}
}
sendQR(qr: string): void {
logger.info(`Broadcasting QR code to ${this.clients.size} connected clients`);
console.log('QR DATA LENGTH:', qr.length);
this.broadcast({
type: 'qr',
data: qr
});
}
sendStatus(status: string): void {
this.broadcast({
type: 'status',
data: status
});
}
sendError(error: string): void {
this.broadcast({
type: 'error',
data: error
});
}
close(): void {
this.wss.close();
logger.info('WebSocket server closed');
}
}

View File

@@ -0,0 +1,184 @@
import { makeWASocket, DisconnectReason, useMultiFileAuthState, AuthenticationState } from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import { SessionStore, SessionData } from './session.store';
import { logger } from '../config/logger';
import { join } from 'path';
export class WhatsAppClient {
private client: any;
public sessionStore: SessionStore;
private qrCallback?: (qr: string) => void;
private statusCallback?: (status: string) => void;
private messageCallback?: (message: any) => void;
private saveCreds?: () => Promise<void>;
private isConnected: boolean = false;
constructor(sessionId: string) {
this.sessionStore = new SessionStore(sessionId);
}
async connect(): Promise<void> {
try {
this.isConnected = false;
const sessionId = this.sessionStore['sessionId'] || 'default';
const authPath = join(process.cwd(), 'auth_info', sessionId);
// Use proper Baileys authentication
const { state, saveCreds } = await useMultiFileAuthState(authPath);
const hasCredentials = state.creds?.registered;
if (!hasCredentials) {
logger.info('No credentials found, initiating new session - QR will be generated');
this.statusCallback?.('waiting');
} else {
logger.info('Existing credentials found, attempting to connect');
this.statusCallback?.('connecting');
}
this.client = makeWASocket({
auth: state,
printQRInTerminal: false,
logger: logger.child({ module: 'baileys' }),
browser: ['WhatsApp Gateway', 'Chrome', '120.0.0'],
mobile: false,
syncFullHistory: false,
markOnlineOnConnect: false,
generateHighQualityLinkPreview: false,
});
// Store saveCreds for later use
this.saveCreds = saveCreds;
this.setupEventHandlers();
} catch (error) {
logger.error(`Failed to connect: ${error}`);
this.statusCallback?.('error');
throw error;
}
}
private setupEventHandlers(): void {
this.client.ev.on('connection.update', (update: any) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
logger.info('QR code received, broadcasting to clients');
console.log('QR CODE GENERATED - Length:', qr.length);
if (this.qrCallback) {
this.qrCallback(qr);
}
if (this.statusCallback) {
this.statusCallback('qr');
}
}
if (connection === 'close') {
this.isConnected = false;
const shouldReconnect = (lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut;
logger.info(`Connection closed. Reconnect: ${shouldReconnect}`);
if (shouldReconnect) {
this.statusCallback?.('reconnecting');
setTimeout(() => this.connect(), 5000);
} else {
this.statusCallback?.('logout');
}
}
if (connection === 'open') {
logger.info('WhatsApp connection opened');
this.isConnected = true;
this.statusCallback?.('connected');
}
if (connection === 'connecting') {
logger.info('WhatsApp connecting...');
this.statusCallback?.('connecting');
}
});
this.client.ev.on('creds.update', () => {
logger.info('Credentials updated');
if (this.saveCreds) {
this.saveCreds();
}
});
this.client.ev.on('messages.upsert', (event: any) => {
const { messages } = event;
if (messages && messages.length > 0) {
messages.forEach((msg: any) => {
logger.info('Message received');
this.messageCallback?.(msg);
});
}
});
}
onQR(callback: (qr: string) => void): void {
this.qrCallback = callback;
}
onStatus(callback: (status: string) => void): void {
this.statusCallback = callback;
}
onMessage(callback: (message: any) => void): void {
this.messageCallback = callback;
}
async sendMessage(jid: string, content: string, type: 'text' | 'image' | 'document' = 'text'): Promise<any> {
try {
const message = type === 'text'
? { text: content }
: { caption: content };
const result = await this.client.sendMessage(jid, message);
logger.info(`Message sent to ${jid}, result: ${JSON.stringify(result)}`);
// Ensure we always return a valid result structure
if (!result) {
return {
key: { id: `unknown-${Date.now()}` },
status: 'sent'
};
}
return result;
} catch (error) {
logger.error(`Failed to send message: ${error}`);
throw error;
}
}
async getGroups(): Promise<any[]> {
try {
if (!this.client) {
throw new Error('WhatsApp client not initialized');
}
const groups = await this.client.groupFetchAllParticipating();
return Object.values(groups);
} catch (error) {
logger.error(`Failed to fetch groups: ${error}`);
throw error;
}
}
getConnectionState(): string {
return this.isConnected ? 'connected' : 'disconnected';
}
async disconnect(): Promise<void> {
try {
this.isConnected = false;
if (this.client) {
this.client.ws?.close();
this.client.end?.();
}
logger.info('WhatsApp client disconnected');
} catch (error) {
logger.error(`Failed to disconnect: ${error}`);
}
}
}

View File

@@ -0,0 +1,35 @@
import { existsSync, rmSync } from 'fs';
import { join } from 'path';
import { logger } from '../config/logger';
export interface SessionData {
id: string;
creds?: any;
keys?: any;
}
export class SessionStore {
private sessionId: string;
constructor(sessionId: string) {
this.sessionId = sessionId;
}
// Baileys now handles session persistence automatically
// We only need this for session metadata
getAuthPath(): string {
return join(process.cwd(), 'auth_info', this.sessionId);
}
delete(): void {
try {
const authPath = this.getAuthPath();
if (existsSync(authPath)) {
rmSync(authPath, { recursive: true, force: true });
logger.info(`Session deleted for ${this.sessionId}`);
}
} catch (error) {
logger.error(`Failed to delete session: ${error}`);
}
}
}

19
gateway/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

13
manager/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
# Build stage
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

12
manager/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WhatsApp Gateway Manager</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2590
manager/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
manager/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "whatsapp-manager",
"version": "1.0.0",
"description": "WhatsApp Gateway Manager Web Interface",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"qrcode": "^1.5.3"
},
"devDependencies": {
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-react": "^4.1.1",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"tailwindcss": "^3.3.5",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,309 @@
import React, { useState, useEffect } from 'react';
import QRCode from 'qrcode';
import { Modal } from './Modal';
import { WebSocketService } from '../services/websocket';
import { ConnectionStatus, MessageData, APIToken, RecentMessages } from '../types/gateway';
interface ConnectionManagerProps {
wsService: WebSocketService;
}
export const ConnectionManager: React.FC<ConnectionManagerProps> = ({ wsService }) => {
const [qrCode, setQrCode] = useState<string>('');
const [status, setStatus] = useState<ConnectionStatus['status']>('waiting');
const [error, setError] = useState<string>('');
const [recentMessages, setRecentMessages] = useState<MessageData[]>([]);
const [apiToken, setApiToken] = useState<APIToken | null>(null);
const [showTokenModal, setShowTokenModal] = useState(false);
const [loading, setLoading] = useState<string>('');
useEffect(() => {
// Listen for QR codes
wsService.on('qr', async (qrData: string) => {
try {
const qrCodeUrl = await QRCode.toDataURL(qrData, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
});
setQrCode(qrCodeUrl);
setStatus('qr');
setError('');
} catch (error) {
console.error('Failed to generate QR code:', error);
setError('Failed to generate QR code');
}
});
// Listen for status updates
wsService.on('status', (statusData: string) => {
setStatus(statusData as ConnectionStatus['status']);
if (statusData === 'connected') {
setQrCode('');
setError('');
}
});
// Listen for errors
wsService.on('error', (errorData: string) => {
setError(errorData);
setStatus('logout');
});
// Listen for expired sessions
wsService.on('expired', () => {
setStatus('expired');
setQrCode('');
setError('La sesión ha expirado. Por favor, vuelve a escanear el código QR.');
});
// Listen for new messages
wsService.on('message', (messageData: MessageData) => {
setRecentMessages(prev => [messageData, ...prev.slice(0, 9)]);
});
// Listen for recent messages
wsService.on('recent_messages', (messagesData: RecentMessages) => {
if (messagesData.success && messagesData.data) {
setRecentMessages(messagesData.data.messages);
}
});
// Listen for token generation
wsService.on('token_generated', (tokenData: any) => {
if (tokenData.success && tokenData.data) {
setApiToken(tokenData.data);
setShowTokenModal(true);
}
});
return () => {
wsService.off('qr', () => {});
wsService.off('status', () => {});
wsService.off('error', () => {});
wsService.off('expired', () => {});
wsService.off('message', () => {});
wsService.off('recent_messages', () => {});
wsService.off('token_generated', () => {});
};
}, [wsService]);
const handleRestartSession = () => {
setLoading('restart');
wsService.restartSession();
setTimeout(() => setLoading(''), 2000);
};
const handleLogout = () => {
setLoading('logout');
wsService.logoutSession();
setRecentMessages([]);
setApiToken(null);
setTimeout(() => setLoading(''), 2000);
};
const handleGenerateToken = () => {
setLoading('token');
wsService.generateToken();
setTimeout(() => setLoading(''), 2000);
};
const handleCopyToken = async () => {
if (apiToken?.token) {
try {
await navigator.clipboard.writeText(apiToken.token);
alert('Token copiado al portapapeles');
} catch (error) {
console.error('Failed to copy token:', error);
}
}
};
const getStatusColor = () => {
switch (status) {
case 'connected': return 'text-green-600';
case 'connecting':
case 'reconnecting': return 'text-yellow-600';
case 'qr': return 'text-blue-600';
case 'expired': return 'text-orange-600';
case 'logout': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getStatusText = () => {
switch (status) {
case 'waiting': return 'Esperando conexión...';
case 'qr': return 'Escanea el código QR';
case 'connecting': return 'Conectando...';
case 'connected': return 'Conectado ✅';
case 'reconnecting': return 'Reconectando...';
case 'expired': return 'Sesión Expirada ⚠️';
case 'logout': return 'Desconectado';
default: return 'Estado desconocido';
}
};
const getStatusIcon = () => {
switch (status) {
case 'connected': return '✅';
case 'expired': return '⚠️';
case 'logout': return '❌';
case 'qr': return '📱';
case 'connecting':
case 'reconnecting': return '🔄';
default: return '⏳';
}
};
return (
<div className="flex flex-col lg:flex-row gap-8">
{/* Panel Principal: QR y Estado */}
<div className="flex-1">
<div className="bg-white rounded-lg shadow p-6">
<div className="text-center mb-6">
<div className="text-4xl mb-2">{getStatusIcon()}</div>
<h2 className="text-2xl font-bold mb-2">WhatsApp Gateway</h2>
<div className={`text-lg font-semibold ${getStatusColor()}`}>
{getStatusText()}
</div>
</div>
{error && (
<div className="w-full p-4 bg-red-50 border border-red-200 rounded-lg mb-6">
<div className="text-red-800">{error}</div>
</div>
)}
{qrCode && (
<div className="flex flex-col items-center space-y-4 mb-6">
<div className="p-4 bg-white rounded-lg shadow-lg border">
<img src={qrCode} alt="WhatsApp QR Code" className="w-64 h-64" />
</div>
<div className="text-center text-sm text-gray-600 max-w-md">
<p>1. Abre WhatsApp en tu teléfono</p>
<p>2. Ve a Configuración Dispositivos vinculados</p>
<p>3. Escanea este código QR</p>
</div>
</div>
)}
{status === 'connected' && (
<div className="text-center space-y-4 mb-6">
<div className="text-green-600 text-6xl"></div>
<p className="text-gray-700">Tu WhatsApp está conectado y listo para usar</p>
</div>
)}
{/* Botones de Acción */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-6">
<button
onClick={handleRestartSession}
disabled={loading === 'restart'}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-blue-300 transition-colors"
>
{loading === 'restart' ? 'Reiniciando...' : 'Reiniciar Sesión'}
</button>
<button
onClick={handleLogout}
disabled={loading === 'logout' || status !== 'connected'}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:bg-gray-300 transition-colors"
>
{loading === 'logout' ? 'Cerrando...' : 'Logout'}
</button>
<button
onClick={handleGenerateToken}
disabled={loading === 'token' || status !== 'connected'}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-300 transition-colors"
>
{loading === 'token' ? 'Generando...' : 'Generar Token'}
</button>
</div>
{/* Token Modal */}
<Modal
isOpen={showTokenModal}
onClose={() => setShowTokenModal(false)}
title="Token de API Generado"
>
{apiToken && (
<div className="space-y-4">
<p className="text-sm text-gray-600">
Este token te permitirá autenticarte en la API. Guárdalo en un lugar seguro ya que no podrás volver a verlo completo.
</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Token de Acceso</label>
<div className="flex gap-2">
<div className="flex-1 font-mono text-xs bg-gray-50 p-2 rounded border break-all">
{apiToken.token}
</div>
<button
onClick={handleCopyToken}
className="px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700 transition-colors shrink-0"
>
Copiar
</button>
</div>
</div>
<div className="text-xs text-gray-500">
Expira: {new Date(apiToken.expiresAt).toLocaleString()}
</div>
<div className="flex justify-end pt-2">
<button
onClick={() => setShowTokenModal(false)}
className="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
>
Cerrar
</button>
</div>
</div>
)}
</Modal>
</div>
</div>
{/* Panel Derecho: Mensajes Recientes */}
<div className="lg:w-96">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Mensajes Recientes</h3>
<button
onClick={() => wsService.getRecentMessages()}
className="text-sm text-blue-600 hover:text-blue-800"
>
Actualizar
</button>
</div>
{recentMessages.length === 0 ? (
<div className="text-center text-gray-500 py-8">
<div className="text-4xl mb-2">💬</div>
<p>No hay mensajes recientes</p>
</div>
) : (
<div className="space-y-3 max-h-96 overflow-y-auto">
{recentMessages.map((message, index) => (
<div key={`${message.id}-${index}`} className="bg-gray-50 rounded-lg p-3">
<div className="flex justify-between items-start mb-1">
<div className="font-mono text-sm text-gray-600">{message.from}</div>
<div className="text-xs text-gray-500">
{new Date(message.timestamp).toLocaleTimeString()}
</div>
</div>
<div className="text-sm text-gray-800">{message.content}</div>
<div className="text-xs text-gray-500 mt-1 capitalize">{message.type}</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,146 @@
import React, { useState, useEffect } from 'react';
import { WebSocketService } from '../services/websocket';
import { ConnectionManager } from './ConnectionManager';
import { GatewayStatus } from '../types/gateway';
export const Dashboard: React.FC = () => {
const [wsService, setWsService] = useState<WebSocketService | null>(null);
const [gatewayStatus, setGatewayStatus] = useState<GatewayStatus | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:3003';
const service = new WebSocketService(wsUrl);
setWsService(service);
service.connect()
.then(() => {
setIsConnected(true);
fetchGatewayStatus();
})
.catch(error => {
console.error('Failed to connect to WebSocket:', error);
setIsConnected(false);
});
// Fetch gateway status every 30 seconds (less frequent)
const statusInterval = setInterval(fetchGatewayStatus, 30000);
return () => {
service.disconnect();
clearInterval(statusInterval);
};
}, []);
const fetchGatewayStatus = async () => {
try {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001';
const response = await fetch(`${apiUrl}/api/status`);
if (response.ok) {
const data = await response.json();
if (data.success) {
setGatewayStatus(data.data);
}
}
} catch (error) {
console.error('Failed to fetch gateway status:', error);
}
};
const formatUptime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours}h ${minutes}m ${secs}s`;
};
if (!wsService) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Iniciando Manager...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100">
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<h1 className="text-2xl font-bold text-gray-900">WhatsApp Gateway Manager</h1>
<div className="flex items-center space-x-4">
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
isConnected ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{isConnected ? 'Conectado' : 'Desconectado'}
</div>
<button
onClick={fetchGatewayStatus}
className="px-3 py-1 bg-blue-600 text-white rounded-full text-sm font-medium hover:bg-blue-700 transition-colors"
>
Actualizar
</button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="space-y-8">
{/* Connection Manager - Panel Principal */}
<ConnectionManager wsService={wsService} />
{/* Status Info - Panel Inferior Simplificado */}
{gatewayStatus && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Estado del Sistema</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<div className="text-sm text-gray-500">Estado Gateway</div>
<div className="font-medium capitalize">{gatewayStatus.status}</div>
</div>
<div>
<div className="text-sm text-gray-500">Sesión</div>
<div className="font-medium">{gatewayStatus.sessionId}</div>
</div>
<div>
<div className="text-sm text-gray-500">Tiempo Activo</div>
<div className="font-medium">{formatUptime(gatewayStatus.uptime)}</div>
</div>
<div>
<div className="text-sm text-gray-500">Memoria</div>
<div className="font-medium">{(gatewayStatus.memory.heapUsed / 1024 / 1024).toFixed(2)} MB</div>
</div>
</div>
</div>
)}
{/* Quick Reference - Panel API Simplificado */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Referencia Rápida API</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium text-gray-700 mb-2">Envío de Mensajes</h4>
<div className="space-y-1 text-sm">
<div className="font-mono bg-gray-50 p-2 rounded">POST /api/send</div>
<div className="font-mono bg-gray-50 p-2 rounded">POST /api/send/bulk</div>
</div>
</div>
<div>
<h4 className="font-medium text-gray-700 mb-2">Control de Sesión</h4>
<div className="space-y-1 text-sm">
<div className="font-mono bg-gray-50 p-2 rounded">POST /api/session/restart</div>
<div className="font-mono bg-gray-50 p-2 rounded">POST /api/session/logout</div>
<div className="font-mono bg-gray-50 p-2 rounded">POST /api/token</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
);
};

View File

@@ -0,0 +1,71 @@
import React, { useEffect } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
}
export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }) => {
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-50 transition-opacity backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal Container */}
<div className="flex min-h-full items-center justify-center p-4">
<div
className="relative w-full max-w-lg transform rounded-lg bg-white p-6 text-left shadow-xl transition-all"
role="dialog"
aria-modal="true"
>
{/* Header */}
<div className="flex items-center justify-between mb-4">
{title && (
<h3 className="text-lg font-medium leading-6 text-gray-900">
{title}
</h3>
)}
<button
onClick={onClose}
className="rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 transition-colors"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="mt-2">
{children}
</div>
</div>
</div>
</div>
);
};

16
manager/src/index.css Normal file
View File

@@ -0,0 +1,16 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
min-height: 100vh;
}

10
manager/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Dashboard } from './components/Dashboard';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Dashboard />
</React.StrictMode>
);

View File

@@ -0,0 +1,126 @@
import { WebSocketMessage } from '../types/gateway';
export class WebSocketService {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 3000;
private listeners: Map<string, ((data: any) => void)[]> = new Map();
constructor(private url: string) {}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
resolve();
};
this.ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
console.log('WebSocket message received:', message.type, message.data?.length || message.data);
this.emit(message.type, message.data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.handleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
} catch (error) {
reject(error);
}
});
}
private handleReconnect(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.connect().catch(error => {
console.error('Reconnect failed:', error);
});
}, this.reconnectDelay);
} else {
console.error('Max reconnect attempts reached');
}
}
on(event: string, callback: (data: any) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
off(event: string, callback: (data: any) => void): void {
const eventListeners = this.listeners.get(event);
if (eventListeners) {
const index = eventListeners.indexOf(callback);
if (index > -1) {
eventListeners.splice(index, 1);
}
}
}
private emit(event: string, data: any): void {
const eventListeners = this.listeners.get(event);
if (eventListeners) {
eventListeners.forEach(callback => callback(data));
}
}
// Send command to Gateway
sendCommand(command: string, data?: any): void {
if (this.isConnected()) {
const message = {
type: 'command',
action: command,
data: data || {}
};
this.ws!.send(JSON.stringify(message));
}
}
// Gateway specific commands
restartSession(): void {
this.sendCommand('restart_session');
}
logoutSession(): void {
this.sendCommand('logout_session');
}
getRecentMessages(): void {
this.sendCommand('get_recent_messages');
}
generateToken(): void {
this.sendCommand('generate_token');
}
disconnect(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
}

View File

@@ -0,0 +1,44 @@
export interface ConnectionStatus {
status: 'waiting' | 'qr' | 'connecting' | 'connected' | 'reconnecting' | 'logout' | 'expired';
timestamp: string;
}
export interface QRData {
qr: string;
timestamp: string;
}
export interface GatewayStatus {
status: string;
sessionId: string;
uptime: number;
memory: NodeJS.MemoryUsage;
timestamp: string;
}
export interface WebSocketMessage {
type: 'qr' | 'status' | 'error' | 'message' | 'expired';
data: string | MessageData | any;
}
export interface MessageData {
id: string;
from: string;
to: string;
content: string;
timestamp: string;
type: 'text' | 'image' | 'document';
}
export interface APIToken {
token: string;
expiresAt: string;
permissions: string[];
}
export interface RecentMessages {
success: any;
data: any;
messages: MessageData[];
total: number;
}

1
manager/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

21
manager/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

14
manager/vite.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3002,
host: true
},
build: {
outDir: 'dist',
sourcemap: true
}
})