- Add Dockerfile.whatsapp-bridge with Baileys + env var support - Modify whatsapp-bridge-server.js to accept PORT, TENANT_ID, WEBHOOK_BASE - Add internal_bp.py with endpoints to provision/destroy bridges via Docker - Register internal_bp in app.py - Each tenant gets isolated container, port, and volume
153 lines
5.0 KiB
Python
153 lines
5.0 KiB
Python
"""Internal API endpoints for infrastructure orchestration.
|
|
|
|
These endpoints are meant to be called by the Nexus Manager or other
|
|
internal services. They require INTERNAL_API_KEY.
|
|
"""
|
|
import subprocess
|
|
import socket
|
|
from flask import Blueprint, request, jsonify
|
|
from config import INTERNAL_API_KEY
|
|
from tenant_db import get_master_conn, get_tenant_conn
|
|
|
|
internal_bp = Blueprint('internal', __name__, url_prefix='/pos/api/internal')
|
|
|
|
|
|
def _check_internal_key():
|
|
key = request.headers.get('X-Internal-Key', '')
|
|
if not INTERNAL_API_KEY:
|
|
return jsonify({'error': 'INTERNAL_API_KEY not configured on server'}), 500
|
|
if key != INTERNAL_API_KEY:
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
return None
|
|
|
|
|
|
def _find_free_port(start=21465, end=21565):
|
|
"""Find first free TCP port in range."""
|
|
for port in range(start, end + 1):
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
if s.connect_ex(('127.0.0.1', port)) != 0:
|
|
return port
|
|
return None
|
|
|
|
|
|
@internal_bp.route('/whatsapp-bridge', methods=['POST'])
|
|
def provision_whatsapp_bridge():
|
|
"""Provision a new WhatsApp Bridge Docker container for a tenant."""
|
|
auth_error = _check_internal_key()
|
|
if auth_error:
|
|
return auth_error
|
|
|
|
data = request.get_json() or {}
|
|
tenant_id = data.get('tenant_id')
|
|
subdomain = data.get('subdomain', f'tenant-{tenant_id}')
|
|
|
|
if not tenant_id:
|
|
return jsonify({'error': 'tenant_id required'}), 400
|
|
|
|
# Check if container already exists
|
|
container_name = f"wpp-{subdomain}"
|
|
check = subprocess.run(
|
|
['docker', 'ps', '-a', '-q', '-f', f'name={container_name}'],
|
|
capture_output=True, text=True
|
|
)
|
|
if check.stdout.strip():
|
|
return jsonify({'error': f'Container {container_name} already exists'}), 409
|
|
|
|
# Find free port
|
|
port = _find_free_port()
|
|
if not port:
|
|
return jsonify({'error': 'No free ports available in range 21465-21565'}), 503
|
|
|
|
# Build image if not exists
|
|
image_check = subprocess.run(
|
|
['docker', 'images', '-q', 'nexus-whatsapp-bridge'],
|
|
capture_output=True, text=True
|
|
)
|
|
if not image_check.stdout.strip():
|
|
build = subprocess.run(
|
|
['docker', 'build', '-f', '/home/Autopartes/pos/Dockerfile.whatsapp-bridge',
|
|
'-t', 'nexus-whatsapp-bridge', '/home/Autopartes/pos'],
|
|
capture_output=True, text=True
|
|
)
|
|
if build.returncode != 0:
|
|
return jsonify({'error': 'Failed to build bridge image', 'details': build.stderr}), 500
|
|
|
|
# Run container
|
|
bridge_url = f"http://127.0.0.1:{port}"
|
|
run = subprocess.run([
|
|
'docker', 'run', '-d',
|
|
'--name', container_name,
|
|
'--restart', 'unless-stopped',
|
|
'-p', f'{port}:21465',
|
|
'-e', f'PORT=21465',
|
|
'-e', f'TENANT_ID={tenant_id}',
|
|
'-e', f'WEBHOOK_BASE=http://127.0.0.1:5001/pos/api/whatsapp/webhook',
|
|
'-e', f'API_KEY=nexus-wpp-secret-2026',
|
|
'-e', f'LOG_LEVEL=info',
|
|
'-v', f'wpp-{subdomain}:/app/auth',
|
|
'nexus-whatsapp-bridge'
|
|
], capture_output=True, text=True)
|
|
|
|
if run.returncode != 0:
|
|
return jsonify({'error': 'Failed to start container', 'details': run.stderr}), 500
|
|
|
|
container_id = run.stdout.strip()
|
|
|
|
# Save config to tenant_config
|
|
conn = get_tenant_conn_by_dbname(data.get('db_name'))
|
|
if not conn:
|
|
# Fallback: get db_name from master
|
|
mconn = get_master_conn()
|
|
mcur = mconn.cursor()
|
|
mcur.execute("SELECT db_name FROM tenants WHERE id = %s", (tenant_id,))
|
|
row = mcur.fetchone()
|
|
mcur.close()
|
|
mconn.close()
|
|
if row:
|
|
conn = get_tenant_conn_by_dbname(row[0])
|
|
|
|
if conn:
|
|
cur = conn.cursor()
|
|
cur.execute("""
|
|
INSERT INTO tenant_config (key, value) VALUES
|
|
('whatsapp_bridge_url', %s),
|
|
('whatsapp_enabled', 'true')
|
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
|
""", (bridge_url,))
|
|
conn.commit()
|
|
cur.close()
|
|
conn.close()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'tenant_id': tenant_id,
|
|
'container_id': container_id,
|
|
'container_name': container_name,
|
|
'port': port,
|
|
'bridge_url': bridge_url
|
|
}), 201
|
|
|
|
|
|
@internal_bp.route('/whatsapp-bridge', methods=['DELETE'])
|
|
def destroy_whatsapp_bridge():
|
|
"""Destroy a tenant's WhatsApp Bridge container."""
|
|
auth_error = _check_internal_key()
|
|
if auth_error:
|
|
return auth_error
|
|
|
|
data = request.get_json() or {}
|
|
subdomain = data.get('subdomain')
|
|
if not subdomain:
|
|
return jsonify({'error': 'subdomain required'}), 400
|
|
|
|
container_name = f"wpp-{subdomain}"
|
|
|
|
# Stop and remove container
|
|
subprocess.run(['docker', 'stop', container_name], capture_output=True)
|
|
subprocess.run(['docker', 'rm', container_name], capture_output=True)
|
|
|
|
# Remove volume
|
|
subprocess.run(['docker', 'volume', 'rm', f'wpp-{subdomain}'], capture_output=True)
|
|
|
|
return jsonify({'success': True, 'message': f'Bridge {container_name} destroyed'})
|