from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.pagination import PageNumberPagination from django.shortcuts import get_object_or_404, render from django.utils import timezone from django.contrib.auth.decorators import login_required from django.db import models from .models import Agent, AgentExecution, AgentCategory from .serializers import AgentSerializer, AgentExecutionSerializer import requests import json import time import uuid import ipaddress from urllib.parse import urlparse def validate_webhook_url(url): """ Validate webhook URL to prevent SSRF attacks. Only allows HTTPS URLs to external, non-private networks. """ try: parsed = urlparse(url) # Only allow HTTP/HTTPS protocols if parsed.scheme not in ['http', 'https']: raise ValueError("Only HTTP/HTTPS URLs are allowed") # Get hostname hostname = parsed.hostname if not hostname: raise ValueError("Invalid hostname in URL") # For localhost development, allow localhost URLs first if hostname in ['localhost', '127.0.0.1'] and parsed.port in [5678, 8000, 8080]: return True # Allow N8N development server # Check if hostname is an IP address try: ip = ipaddress.ip_address(hostname) # Block private, loopback, and reserved IP ranges if (ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local or ip.is_multicast): raise ValueError("Internal/private IP addresses are not allowed") except ValueError as e: if "does not appear to be an IPv4 or IPv6 address" not in str(e): raise # Re-raise if it's not just a "not an IP" error # If it's not an IP, it's a domain name - that's fine return True except Exception as e: raise ValueError(f"Invalid webhook URL: {str(e)}") @api_view(['GET']) @permission_classes([IsAuthenticated]) def agent_list(request): """List all active agents with optional category filtering""" agents = Agent.objects.filter(is_active=True) category = request.GET.get('category') if category: agents = agents.filter(category__slug=category) search = request.GET.get('search') if search: agents = agents.filter(name__icontains=search) paginator = PageNumberPagination() paginator.page_size = 20 result_page = paginator.paginate_queryset(agents, request) serializer = AgentSerializer(result_page, many=True) return paginator.get_paginated_response(serializer.data) @api_view(['GET']) @permission_classes([IsAuthenticated]) def agent_detail(request, slug): """Get detailed agent information""" agent = get_object_or_404(Agent, slug=slug, is_active=True) serializer = AgentSerializer(agent) return Response(serializer.data) @api_view(['POST']) @permission_classes([IsAuthenticated]) def execute_agent(request): """Execute an agent with provided input data""" agent_slug = request.data.get('agent_slug') input_data = request.data.get('input_data', {}) if not agent_slug: return Response({'error': 'agent_slug is required'}, status=status.HTTP_400_BAD_REQUEST) agent = get_object_or_404(Agent, slug=agent_slug, is_active=True) # Check if user has sufficient balance (using existing wallet system) if hasattr(request.user, 'has_sufficient_balance') and not request.user.has_sufficient_balance(agent.price): return Response({'error': 'Insufficient wallet balance'}, status=status.HTTP_400_BAD_REQUEST) # Create execution record execution = AgentExecution.objects.create( agent=agent, user=request.user, input_data=input_data, fee_charged=agent.price, status='pending' ) try: # Deduct fee from user wallet (using existing wallet system) if hasattr(request.user, 'deduct_balance'): success = request.user.deduct_balance( agent.price, f'{agent.name} - Execution {str(execution.id)[:8]}', agent.slug ) if not success: execution.status = 'failed' execution.error_message = 'Failed to deduct wallet balance' execution.save() return Response({'error': 'Failed to deduct wallet balance'}, status=status.HTTP_400_BAD_REQUEST) # Validate webhook URL to prevent SSRF attacks try: validate_webhook_url(agent.webhook_url) except ValueError as e: execution.status = 'failed' execution.error_message = f'Invalid webhook URL: {str(e)}' execution.save() return Response({'error': f'Invalid webhook URL: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) # Call n8n webhook with proper payload format execution.status = 'running' execution.save() # Generate session ID session_id = f"session_{int(time.time() * 1000)}_{str(uuid.uuid4())[:8]}" # Format message text for N8N based on agent type message_text = format_agent_message(agent.slug, input_data) webhook_payload = { 'sessionId': session_id, 'message': {'text': message_text}, 'webhookUrl': agent.webhook_url, 'executionMode': 'production', 'agentId': str(agent.id), 'executionId': str(execution.id), 'userId': str(request.user.id) } response = requests.post( agent.webhook_url, json=webhook_payload, timeout=90, # Increased timeout for complex processing headers={'Content-Type': 'application/json'} ) # Store webhook response execution.webhook_response = response.json() if response.headers.get('content-type', '').startswith('application/json') else {'raw': response.text} if response.status_code == 200: execution.status = 'completed' execution.output_data = execution.webhook_response else: execution.status = 'failed' execution.error_message = f"Webhook returned {response.status_code}: {response.text[:500]}" execution.completed_at = timezone.now() execution.save() serializer = AgentExecutionSerializer(execution) return Response(serializer.data, status=status.HTTP_201_CREATED) except requests.RequestException as e: execution.status = 'failed' execution.error_message = str(e) execution.completed_at = timezone.now() execution.save() return Response({ 'error': 'Failed to execute agent', 'execution_id': str(execution.id) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) @api_view(['GET']) @permission_classes([IsAuthenticated]) def execution_list(request): """List user's agent executions""" executions = AgentExecution.objects.filter(user=request.user) paginator = PageNumberPagination() paginator.page_size = 20 result_page = paginator.paginate_queryset(executions, request) serializer = AgentExecutionSerializer(result_page, many=True) return paginator.get_paginated_response(serializer.data) @api_view(['GET']) @permission_classes([IsAuthenticated]) def execution_detail(request, execution_id): """Get detailed execution information""" execution = get_object_or_404(AgentExecution, id=execution_id, user=request.user) serializer = AgentExecutionSerializer(execution) return Response(serializer.data) def format_agent_message(agent_slug, input_data): """Format input data into a message for N8N webhook based on agent type""" if agent_slug == 'social-ads-generator': description = input_data.get('description', '') platform = input_data.get('social_platform', '') emoji = input_data.get('include_emoji', 'yes') language = input_data.get('language', 'English') return f"Execute Social Media Ad Creator with the following parameters:. Describe what you'd like to generate: {description}. Include Emoji: {emoji.title()}. For Social Media Platform: {platform.title()}. Language: {language}." elif agent_slug == 'job-posting-generator': job_title = input_data.get('job_title', '') company_name = input_data.get('company_name', '') description = input_data.get('job_description', '') seniority = input_data.get('seniority_level', '') contract = input_data.get('contract_type', '') location = input_data.get('location', '') language = input_data.get('language', 'English') return f"Create a professional job posting for: {job_title} at {company_name}. Description: {description}. Seniority: {seniority}. Contract: {contract}. Location: {location}. Language: {language}. Make it comprehensive and attractive to candidates." # Default formatting for other agents params = [f"{key}: {value}" for key, value in input_data.items() if value] return f"Execute {agent_slug.replace('-', ' ').title()} with parameters: {'. '.join(params)}." # Web interface views @login_required def agent_detail_view(request, slug): """Render agent detail page with dynamic form""" agent = get_object_or_404(Agent, slug=slug, is_active=True) context = { 'agent': agent, 'timestamp': int(time.time()) # For cache busting } return render(request, 'agents/agent_detail.html', context) def agents_marketplace(request): """Agent marketplace view""" agents = Agent.objects.filter(is_active=True).select_related('category') categories = AgentCategory.objects.filter(is_active=True) # Filter by category category_slug = request.GET.get('category') if category_slug: agents = agents.filter(category__slug=category_slug) # Search functionality search_query = request.GET.get('search', '').strip() if search_query: agents = agents.filter( models.Q(name__icontains=search_query) | models.Q(short_description__icontains=search_query) | models.Q(description__icontains=search_query) ) context = { 'agents': agents, 'categories': categories, 'selected_category': category_slug, 'search_query': search_query, 'timestamp': int(time.time()) } return render(request, 'agents/marketplace.html', context)