diff --git a/CLAUDE.md b/CLAUDE.md index b99ad89..8177b87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,10 +137,22 @@ gunicorn netcop_hub.wsgi:application - `DATABASE_URL`: PostgreSQL connection string (Railway) **N8N Webhook URLs:** -Agent-specific webhook URLs are stored in the database with each agent. Current working agents: -- Social Ads Generator: Creates compelling social media advertisements -- Job Posting Generator: Creates professional job postings -- PDF Summarizer: Analyzes and summarizes PDF documents with file upload +Agent-specific webhook URLs are stored in the database with each agent. Current working agents (all tested and confirmed working): + +1. **Social Ads Generator** (social-ads-generator) - 6.00 AED + - Creates compelling social media advertisements + - Form fields: description, social_platform, include_emoji, language + - Webhook: N8N endpoint for social media ad generation + +2. **Job Posting Generator** (job-posting-generator) - 10.00 AED + - Creates professional job postings + - Form fields: job_title, company_name, job_description, seniority_level, contract_type, location, language + - Webhook: N8N endpoint for job posting generation + +3. **PDF Summarizer** (pdf-summarizer) - 8.00 AED + - Analyzes and summarizes PDF documents with file upload + - Form fields: pdf_file (file upload with drag-and-drop), summary_type + - Webhook: N8N endpoint for PDF processing with multipart file support ### URL Structure ``` @@ -256,5 +268,25 @@ class Command(BaseCommand): 3. Test agent execution flow 4. Check AgentExecution records and results display +## System Status + +**Current Status: ✅ STABLE WORKING SYSTEM** +- All 3 agents confirmed working and tested +- Clean agents-only architecture (workflows app completely removed) +- Emergency recovery completed from optimization failures +- System restored to stable commit 657712f + +**Latest Changes:** +- Removed workflows app completely for simplified architecture +- Enhanced agent marketplace with modern responsive design +- Fixed all authentication-aware UI components +- Implemented file upload support for PDF Summarizer +- Real-time wallet balance updates after agent execution + +**Future Development:** +- Optimization work available in feature/optimization-backup branch +- Safe to add new agents via database-driven approach +- Performance optimizations should be applied incrementally with testing + --- -Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: Last updated: 2025-07-31 23:12:15 +Last updated: 2025-07-31 23:35:00 diff --git a/agents/views.py b/agents/views.py index ee2bd4d..809cc6b 100644 --- a/agents/views.py +++ b/agents/views.py @@ -13,6 +13,46 @@ 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]) @@ -81,6 +121,15 @@ def execute_agent(request): 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() diff --git a/authentication/models.py b/authentication/models.py index 31b4fb1..c41df9d 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -1,5 +1,5 @@ from django.contrib.auth.models import AbstractUser -from django.db import models +from django.db import models, transaction from decimal import Decimal import uuid from django.utils import timezone @@ -29,15 +29,23 @@ class User(AbstractUser): def has_sufficient_balance(self, amount): return self.wallet_balance >= Decimal(str(amount)) + @transaction.atomic def deduct_balance(self, amount, description="", agent_slug=""): - if self.has_sufficient_balance(amount): - self.wallet_balance -= Decimal(str(amount)) - self.save() + """ + Deduct balance from user wallet with atomic transaction to prevent race conditions. + Uses select_for_update to lock the user record during the transaction. + """ + # Lock the user record for the duration of this transaction + user = User.objects.select_for_update().get(id=self.id) + + if user.wallet_balance >= Decimal(str(amount)): + user.wallet_balance -= Decimal(str(amount)) + user.save() # Create transaction record from wallet.models import WalletTransaction transaction_data = { - 'user': self, + 'user': user, 'amount': -Decimal(str(amount)), 'type': 'agent_usage', 'description': description, @@ -54,6 +62,9 @@ class User(AbstractUser): WalletTransaction.objects.create(**transaction_data) else: raise e + + # Update the current instance's balance to reflect the change + self.wallet_balance = user.wallet_balance return True return False