Files
Autoparts-DB/pos/blueprints/internal_bp.py
consultoria-as d725ed2e0c feat(whatsapp): auto-provision Docker bridge per tenant
- 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
2026-05-18 04:52:56 +00:00

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'})