diff --git a/core/admin.py b/core/admin.py index 8c38f3f..20f2bc0 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,35 @@ from django.contrib import admin +from .models import ContactSubmission -# Register your models here. + +@admin.register(ContactSubmission) +class ContactSubmissionAdmin(admin.ModelAdmin): + list_display = ['name', 'email', 'company', 'created_at', 'is_processed', 'ip_address'] + list_filter = ['is_processed', 'created_at'] + search_fields = ['name', 'email', 'company', 'message'] + readonly_fields = ['id', 'created_at', 'ip_address', 'user_agent'] + ordering = ['-created_at'] + + fieldsets = ( + ('Contact Information', { + 'fields': ('name', 'email', 'company') + }), + ('Message', { + 'fields': ('message',) + }), + ('Processing', { + 'fields': ('is_processed', 'processed_at') + }), + ('Technical Details', { + 'fields': ('id', 'ip_address', 'user_agent', 'created_at'), + 'classes': ('collapse',) + }), + ) + + actions = ['mark_as_processed'] + + def mark_as_processed(self, request, queryset): + for submission in queryset: + submission.mark_as_processed() + self.message_user(request, f'{queryset.count()} submissions marked as processed.') + mark_as_processed.short_description = "Mark selected submissions as processed" diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..6448ce0 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 5.2.4 on 2025-07-25 20:53 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ContactSubmission", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(max_length=100)), + ("email", models.EmailField(max_length=254)), + ("company", models.CharField(blank=True, max_length=100)), + ("message", models.TextField(max_length=1000)), + ("ip_address", models.GenericIPAddressField()), + ("user_agent", models.TextField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("is_processed", models.BooleanField(default=False)), + ("processed_at", models.DateTimeField(blank=True, null=True)), + ], + options={ + "ordering": ["-created_at"], + "indexes": [ + models.Index( + fields=["-created_at"], name="core_contac_created_fecead_idx" + ), + models.Index( + fields=["is_processed"], name="core_contac_is_proc_10c42d_idx" + ), + models.Index( + fields=["ip_address"], name="core_contac_ip_addr_88d5d9_idx" + ), + ], + }, + ), + ] diff --git a/core/models.py b/core/models.py index 71a8362..c4da762 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,34 @@ from django.db import models +from django.utils import timezone +import uuid -# Create your models here. + +class ContactSubmission(models.Model): + """Model for storing contact form submissions""" + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=100) + email = models.EmailField() + company = models.CharField(max_length=100, blank=True) + message = models.TextField(max_length=1000) + ip_address = models.GenericIPAddressField() + user_agent = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + is_processed = models.BooleanField(default=False) + processed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ['-created_at'] + indexes = [ + models.Index(fields=['-created_at']), + models.Index(fields=['is_processed']), + models.Index(fields=['ip_address']), + ] + + def __str__(self): + return f"Contact from {self.name} ({self.email}) - {self.created_at.strftime('%Y-%m-%d %H:%M')}" + + def mark_as_processed(self): + """Mark submission as processed""" + self.is_processed = True + self.processed_at = timezone.now() + self.save() diff --git a/core/urls.py b/core/urls.py index 8db89ab..3019d92 100644 --- a/core/urls.py +++ b/core/urls.py @@ -6,4 +6,5 @@ app_name = 'core' urlpatterns = [ path('', views.homepage_view, name='homepage'), path('pricing/', views.pricing_view, name='pricing'), + path('contact/', views.contact_form_view, name='contact_form'), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 8344422..8ed25d2 100644 --- a/core/views.py +++ b/core/views.py @@ -1,28 +1,212 @@ from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.http import JsonResponse +from django.core.mail import send_mail +from django.conf import settings +from django_ratelimit.decorators import ratelimit +from django_ratelimit import UNSAFE from agent_base.models import BaseAgent +from .models import ContactSubmission +import logging +import re + +logger = logging.getLogger(__name__) +@ratelimit(key='ip', rate='60/m', method='GET', block=False) def homepage_view(request): - """Homepage view with agent system""" - # Get featured agents for homepage - featured_agents = BaseAgent.objects.filter(is_active=True).order_by('name')[:6] + """Homepage view with agent system and rate limiting""" + # Check if rate limited + if getattr(request, 'limited', False): + logger.warning(f"Homepage rate limit exceeded for IP {request.META.get('REMOTE_ADDR')}") + messages.warning(request, 'Too many requests. Please wait a moment before refreshing.') - context = { - 'user_balance': request.user.wallet_balance if request.user.is_authenticated else 0, - 'featured_agents': featured_agents, - } - - return render(request, 'core/homepage.html', context) + try: + # Get featured agents for homepage with safe querying + featured_agents = BaseAgent.objects.filter(is_active=True).order_by('name')[:6] + + context = { + 'user_balance': request.user.wallet_balance if request.user.is_authenticated else 0, + 'featured_agents': featured_agents, + } + + return render(request, 'core/homepage.html', context) + + except Exception as e: + logger.error(f"Homepage view error: {e}") + messages.error(request, 'Unable to load homepage. Please try again.') + return render(request, 'core/homepage.html', {'featured_agents': [], 'user_balance': 0}) +@ratelimit(key='ip', rate='60/m', method='GET', block=False) def pricing_view(request): - """Pricing page for non-logged-in users""" + """Pricing page for non-logged-in users with rate limiting""" + # Check if rate limited + if getattr(request, 'limited', False): + logger.warning(f"Pricing page rate limit exceeded for IP {request.META.get('REMOTE_ADDR')}") + messages.warning(request, 'Too many requests. Please wait a moment before refreshing.') + # If user is already logged in, redirect to wallet top-up if request.user.is_authenticated: return redirect('wallet:wallet_topup') - # Get sample agents to show pricing context - sample_agents = BaseAgent.objects.filter(is_active=True).order_by('name')[:4] + try: + # Get sample agents to show pricing context with safe querying + sample_agents = BaseAgent.objects.filter(is_active=True).order_by('name')[:4] + + context = { + 'sample_agents': sample_agents, + } + + return render(request, 'core/pricing.html', context) + + except Exception as e: + logger.error(f"Pricing view error: {e}") + messages.error(request, 'Unable to load pricing page. Please try again.') + return render(request, 'core/pricing.html', {'sample_agents': []}) + + +def validate_contact_input(name, email, message, company=""): + """Validate and sanitize contact form input""" + errors = [] - context = { - 'sample_agents': sample_agents, - } + # Name validation + if not name or len(name.strip()) < 2: + errors.append("Name must be at least 2 characters long") + elif len(name) > 100: + errors.append("Name must be less than 100 characters") + elif not re.match(r'^[a-zA-Z\s\-\.\']+$', name): + errors.append("Name contains invalid characters") - return render(request, 'core/pricing.html', context) + # Email validation (Django handles basic format) + if not email or len(email) > 254: + errors.append("Please provide a valid email address") + + # Message validation + if not message or len(message.strip()) < 10: + errors.append("Message must be at least 10 characters long") + elif len(message) > 1000: + errors.append("Message must be less than 1000 characters") + + # Company validation (optional) + if company and len(company) > 100: + errors.append("Company name must be less than 100 characters") + + # Check for potential spam indicators + spam_keywords = ['viagra', 'casino', 'lottery', 'winner', 'congratulations', 'million dollars'] + message_lower = message.lower() + if any(keyword in message_lower for keyword in spam_keywords): + errors.append("Message contains prohibited content") + + return errors + + +def send_contact_notification(submission): + """Send notification email for new contact submission""" + try: + subject = f'New Contact Form Submission from {submission.name}' + message = f''' +New contact form submission received: + +Name: {submission.name} +Email: {submission.email} +Company: {submission.company or 'Not provided'} +IP Address: {submission.ip_address} +Submitted: {submission.created_at.strftime('%Y-%m-%d %H:%M:%S UTC')} + +Message: +{submission.message} + +--- +This is an automated notification from Quantum Tasks AI contact form. + ''' + + # Send to admin email + admin_email = getattr(settings, 'ADMIN_EMAIL', 'abhay@quantumtaskai.com') + + send_mail( + subject=subject, + message=message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[admin_email], + fail_silently=False, + ) + + logger.info(f"Contact notification sent for submission from {submission.email}") + return True + + except Exception as e: + logger.error(f"Failed to send contact notification: {e}") + return False + + +@ratelimit(key='ip', rate='3/m', method='POST', block=False) +def contact_form_view(request): + """Handle contact form submission with security and rate limiting""" + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Method not allowed'}, status=405) + + # Check if rate limited + if getattr(request, 'limited', False): + logger.warning(f"Contact form rate limit exceeded for IP {request.META.get('REMOTE_ADDR')}") + return JsonResponse({ + 'success': False, + 'error': 'Too many contact form submissions. Please try again in a few minutes.' + }, status=429) + + try: + # Get form data + name = request.POST.get('name', '').strip() + email = request.POST.get('email', '').strip() + company = request.POST.get('company', '').strip() + message = request.POST.get('message', '').strip() + + # Validate input + validation_errors = validate_contact_input(name, email, message, company) + if validation_errors: + logger.warning(f"Contact form validation failed from IP {request.META.get('REMOTE_ADDR')}: {validation_errors}") + return JsonResponse({ + 'success': False, + 'error': 'Please correct the following errors: ' + ', '.join(validation_errors) + }, status=400) + + # Check for duplicate submissions (same email/IP in last hour) + from django.utils import timezone + from datetime import timedelta + + recent_submission = ContactSubmission.objects.filter( + ip_address=request.META.get('REMOTE_ADDR'), + created_at__gte=timezone.now() - timedelta(hours=1) + ).first() + + if recent_submission: + logger.warning(f"Duplicate contact submission attempted from IP {request.META.get('REMOTE_ADDR')}") + return JsonResponse({ + 'success': False, + 'error': 'You have already submitted a contact form recently. Please wait before submitting again.' + }, status=429) + + # Create submission + submission = ContactSubmission.objects.create( + name=name, + email=email, + company=company, + message=message, + ip_address=request.META.get('REMOTE_ADDR', ''), + user_agent=request.META.get('HTTP_USER_AGENT', '')[:500] # Truncate user agent + ) + + # Send notification email + email_sent = send_contact_notification(submission) + + logger.info(f"Contact form submitted successfully from {email} (IP: {request.META.get('REMOTE_ADDR')})") + + return JsonResponse({ + 'success': True, + 'message': 'Thank you for your message! We will get back to you within 24 hours.', + 'email_sent': email_sent + }) + + except Exception as e: + logger.error(f"Contact form processing error: {e}") + return JsonResponse({ + 'success': False, + 'error': 'Unable to process your message at this time. Please try again later.' + }, status=500) diff --git a/templates/core/homepage.html b/templates/core/homepage.html index 4f18840..849e8c0 100644 --- a/templates/core/homepage.html +++ b/templates/core/homepage.html @@ -341,28 +341,38 @@ With over 45 years of entrepreneurial and leadership experience, Mr. J. P. Goenk
-
+ {% csrf_token %}

Send us a Message

+ + + +
- + +
- + +
@@ -371,19 +381,31 @@ With over 45 years of entrepreneurial and leadership experience, Mr. J. P. Goenk id="company" name="company" class="form-input" + maxlength="100" /> +
- + +
+ 0/1000 characters +
+
-
@@ -435,17 +457,295 @@ With over 45 years of entrepreneurial and leadership experience, Mr. J. P. Goenk {% endblock %} {% block extra_js %} + + {% endblock %} \ No newline at end of file