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