primer commit
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal 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
236
API_n8n.md
Normal 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
366
README.md
Normal 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
32
clean.sh
Executable 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
308
deploy.sh
Executable 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
38
docker-compose.yml
Normal 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
7
gateway/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.env.local
|
||||
.env.*.local
|
||||
auth_info/*.json
|
||||
!auth_info/.gitkeep
|
||||
18
gateway/Dockerfile
Normal file
18
gateway/Dockerfile
Normal 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
5118
gateway/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
gateway/package.json
Normal file
35
gateway/package.json
Normal 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
108
gateway/src/api/messages.ts
Normal 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
228
gateway/src/api/n8n.ts
Normal 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
110
gateway/src/api/send.ts
Normal 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
106
gateway/src/api/session.ts
Normal 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
60
gateway/src/api/status.ts
Normal 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
151
gateway/src/api/webhook.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
13
gateway/src/config/logger.ts
Normal file
13
gateway/src/config/logger.ts
Normal 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
293
gateway/src/index.ts
Normal 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);
|
||||
});
|
||||
130
gateway/src/middleware/auth.ts
Normal file
130
gateway/src/middleware/auth.ts
Normal 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()
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
115
gateway/src/sockets/qr.socket.ts
Normal file
115
gateway/src/sockets/qr.socket.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
184
gateway/src/whatsapp/client.ts
Normal file
184
gateway/src/whatsapp/client.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
gateway/src/whatsapp/session.store.ts
Normal file
35
gateway/src/whatsapp/session.store.ts
Normal 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
19
gateway/tsconfig.json
Normal 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
13
manager/Dockerfile
Normal 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
12
manager/index.html
Normal 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
2590
manager/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
manager/package.json
Normal file
28
manager/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
manager/postcss.config.js
Normal file
6
manager/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
309
manager/src/components/ConnectionManager.tsx
Normal file
309
manager/src/components/ConnectionManager.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
146
manager/src/components/Dashboard.tsx
Normal file
146
manager/src/components/Dashboard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
71
manager/src/components/Modal.tsx
Normal file
71
manager/src/components/Modal.tsx
Normal 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
16
manager/src/index.css
Normal 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
10
manager/src/main.tsx
Normal 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>
|
||||
);
|
||||
126
manager/src/services/websocket.ts
Normal file
126
manager/src/services/websocket.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
44
manager/src/types/gateway.ts
Normal file
44
manager/src/types/gateway.ts
Normal 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
1
manager/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
11
manager/tailwind.config.js
Normal file
11
manager/tailwind.config.js
Normal 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
21
manager/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
10
manager/tsconfig.node.json
Normal file
10
manager/tsconfig.node.json
Normal 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
14
manager/vite.config.ts
Normal 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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user