mirror of
https://github.com/thecyberlearn/quantumtaskai-caprover.git
synced 2026-08-18 07:32:55 +00:00
- Comment out @cache_user_data decorator that was not imported/defined - This resolves NameError preventing Django startup - Temporary fix - caching can be re-enabled later with proper decorator 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
13 KiB
Python
327 lines
13 KiB
Python
from django.shortcuts import render, redirect
|
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
from django.contrib import messages
|
|
from django.http import JsonResponse
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.views.decorators.http import require_http_methods
|
|
# from django_ratelimit.decorators import ratelimit
|
|
# from django_ratelimit import UNSAFE
|
|
from .stripe_handler import StripePaymentHandler
|
|
import datetime
|
|
import stripe
|
|
from django.conf import settings
|
|
import logging
|
|
import ipaddress
|
|
import json
|
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
|
from decimal import Decimal
|
|
# Simplified version - no caching
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Stripe webhook IP ranges for security validation
|
|
STRIPE_WEBHOOK_IPS = [
|
|
'3.18.12.0/24', '3.130.192.0/24', '13.235.14.0/24', '13.235.122.0/24',
|
|
'18.211.135.0/24', '35.154.171.0/24', '52.15.183.0/24', '54.187.174.0/24',
|
|
'54.187.205.0/24', '54.187.216.0/24', '54.241.31.0/24', '54.241.31.99/32',
|
|
'54.241.31.102/32'
|
|
]
|
|
|
|
|
|
@login_required
|
|
# @cache_user_data('wallet_data', timeout=300) # Cache for 5 minutes - decorator not defined
|
|
def wallet_view(request):
|
|
"""Wallet management page with optimized queries"""
|
|
from django.db.models import Sum, Q
|
|
|
|
# Get recent transactions with optimized query
|
|
transactions = (request.user.wallet_transactions
|
|
.select_related('user')
|
|
.order_by('-created_at')[:50])
|
|
|
|
# Calculate statistics with database aggregation (much faster)
|
|
stats = request.user.wallet_transactions.aggregate(
|
|
total_spent=Sum('amount', filter=Q(type='agent_usage')),
|
|
total_topped_up=Sum('amount', filter=Q(type='top_up'))
|
|
)
|
|
|
|
# Handle None values from aggregation
|
|
total_spent = abs(stats['total_spent'] or 0)
|
|
total_topped_up = stats['total_topped_up'] or 0
|
|
|
|
context = {
|
|
'transactions': transactions,
|
|
'total_spent': total_spent,
|
|
'total_topped_up': total_topped_up,
|
|
'current_balance': request.user.wallet_balance,
|
|
}
|
|
|
|
return render(request, 'wallet/wallet.html', context)
|
|
|
|
|
|
@login_required
|
|
# @ratelimit(key='user', rate='5/m', method=UNSAFE, block=False)
|
|
def wallet_topup_view(request):
|
|
"""Wallet top-up page with rate limiting (5 attempts per minute per user)"""
|
|
# Check if rate limited
|
|
if getattr(request, 'limited', False):
|
|
logger.warning(f"Wallet top-up rate limit exceeded for user {request.user.id}")
|
|
messages.error(request, 'Too many top-up attempts. Please try again in a few minutes.')
|
|
return render(request, 'wallet/wallet_topup.html')
|
|
|
|
if request.method == 'POST':
|
|
amount = request.POST.get('amount')
|
|
|
|
try:
|
|
amount = float(amount)
|
|
if amount not in [10, 50, 100, 500]:
|
|
logger.warning(f"Invalid amount attempted by user {request.user.id}: {amount}")
|
|
messages.error(request, 'Invalid amount selected')
|
|
return redirect('wallet:wallet_topup')
|
|
|
|
# Create Stripe checkout session
|
|
stripe_handler = StripePaymentHandler()
|
|
session_data = stripe_handler.create_checkout_session(request.user, amount, request)
|
|
|
|
logger.info(f"Checkout session created for user {request.user.id}, amount: {amount} AED")
|
|
return redirect(session_data['payment_url'])
|
|
|
|
except (ValueError, TypeError) as e:
|
|
logger.error(f"Invalid amount format from user {request.user.id}: {e}")
|
|
messages.error(request, 'Invalid amount')
|
|
return redirect('wallet:wallet_topup')
|
|
except Exception as e:
|
|
logger.error(f"Checkout session creation failed for user {request.user.id}: {e}")
|
|
messages.error(request, 'Payment failed. Try again.')
|
|
return redirect('wallet:wallet_topup')
|
|
|
|
return render(request, 'wallet/wallet_topup.html')
|
|
|
|
|
|
# @ratelimit(key='user', rate='10/m', method='GET', block=False)
|
|
def wallet_topup_success_view(request):
|
|
"""Payment success page with automatic payment verification"""
|
|
# Check authentication first and clear any messages if redirecting to login
|
|
if not request.user.is_authenticated:
|
|
# Clear any existing messages to prevent them from showing on login page
|
|
storage = messages.get_messages(request)
|
|
storage.used = True
|
|
return redirect('authentication:login')
|
|
|
|
# Check if rate limited
|
|
if getattr(request, 'limited', False):
|
|
logger.warning(f"Payment success page rate limit exceeded for user {request.user.id}")
|
|
messages.error(request, 'Too many verification attempts. Please wait a moment.')
|
|
return redirect('wallet:wallet')
|
|
|
|
session_id = request.GET.get('session_id')
|
|
|
|
if not session_id:
|
|
logger.warning(f"No session ID provided for user {request.user.id}")
|
|
messages.error(request, 'Payment session not found.')
|
|
return redirect('wallet:wallet')
|
|
|
|
# Verify payment directly with Stripe API
|
|
try:
|
|
stripe_handler = StripePaymentHandler()
|
|
|
|
logger.info(f"Verifying payment for user {request.user.id}, session: {session_id}")
|
|
result = stripe_handler.verify_payment(session_id)
|
|
|
|
if result['success']:
|
|
if result['processed']:
|
|
messages.success(request, f'{result["amount"]} AED added to wallet.')
|
|
logger.info(f"Payment verified and wallet updated for user {request.user.id}")
|
|
else:
|
|
messages.info(request, 'Payment already processed.')
|
|
logger.info(f"Payment already processed for session {session_id}")
|
|
else:
|
|
messages.warning(request, 'Payment verification failed. Please contact support.')
|
|
logger.error(f"Payment verification failed for user {request.user.id}: {result.get('error', 'Unknown error')}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error verifying payment for user {request.user.id}: {e}")
|
|
messages.error(request, 'Payment verification failed.')
|
|
|
|
return redirect('wallet:wallet')
|
|
|
|
|
|
def wallet_topup_cancel_view(request):
|
|
"""Payment cancel page"""
|
|
# Check authentication first and clear any messages if redirecting to login
|
|
if not request.user.is_authenticated:
|
|
# Clear any existing messages to prevent them from showing on login page
|
|
storage = messages.get_messages(request)
|
|
storage.used = True
|
|
return redirect('authentication:login')
|
|
|
|
messages.info(request, 'Payment cancelled.')
|
|
return redirect('wallet:wallet_topup')
|
|
|
|
|
|
@login_required
|
|
@user_passes_test(lambda u: u.is_superuser)
|
|
# @ratelimit(key='user', rate='3/m', method='GET', block=True)
|
|
def stripe_debug_view(request):
|
|
"""Secure debug endpoint for superusers only"""
|
|
if not request.user.is_superuser:
|
|
logger.warning(f"Unauthorized debug access attempt by user {request.user.id}")
|
|
return JsonResponse({'error': 'Unauthorized access'}, status=403)
|
|
|
|
debug_info = {
|
|
'timestamp': datetime.datetime.now().isoformat(),
|
|
'user_id': request.user.id,
|
|
'environment': 'production' if not settings.DEBUG else 'development',
|
|
}
|
|
|
|
try:
|
|
# Basic connectivity test without exposing sensitive data
|
|
api_key = settings.STRIPE_SECRET_KEY
|
|
debug_info['stripe_api_configured'] = bool(api_key)
|
|
debug_info['stripe_api_version'] = stripe.api_version
|
|
|
|
# Test account connectivity (minimal info)
|
|
try:
|
|
account = stripe.Account.retrieve()
|
|
debug_info['stripe_account'] = {
|
|
'id': account.id[:8] + '...', # Partial ID only
|
|
'charges_enabled': account.charges_enabled,
|
|
'payouts_enabled': account.payouts_enabled,
|
|
}
|
|
logger.info(f"Stripe debug accessed by superuser {request.user.id}")
|
|
except Exception as account_error:
|
|
debug_info['stripe_account_error'] = 'Connection failed'
|
|
logger.error(f"Stripe account error in debug: {account_error}")
|
|
|
|
debug_info['status'] = 'success'
|
|
|
|
except Exception as e:
|
|
debug_info['error'] = 'Configuration error'
|
|
debug_info['status'] = 'error'
|
|
logger.error(f"Stripe debug error: {e}")
|
|
|
|
return JsonResponse(debug_info, indent=2)
|
|
|
|
|
|
def is_stripe_ip(ip_address):
|
|
"""Check if IP address is from Stripe's webhook IP ranges"""
|
|
try:
|
|
ip = ipaddress.ip_address(ip_address)
|
|
for ip_range in STRIPE_WEBHOOK_IPS:
|
|
if ip in ipaddress.ip_network(ip_range):
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
return False
|
|
|
|
@csrf_exempt
|
|
@require_http_methods(["POST"])
|
|
# @ratelimit(key='ip', rate='50/m', method='POST', block=True)
|
|
def stripe_webhook_view(request):
|
|
"""Secure Stripe webhook handler with IP validation and rate limiting"""
|
|
timestamp = datetime.datetime.now().isoformat()
|
|
remote_ip = request.META.get('REMOTE_ADDR', 'unknown')
|
|
|
|
# Security: Validate request comes from Stripe
|
|
if not is_stripe_ip(remote_ip) and not settings.DEBUG:
|
|
logger.warning(f"Webhook from unauthorized IP: {remote_ip}")
|
|
return JsonResponse({'status': 'error', 'message': 'Unauthorized'}, status=401)
|
|
|
|
# Security: Validate content type
|
|
if request.content_type != 'application/json':
|
|
logger.warning(f"Invalid content type from {remote_ip}: {request.content_type}")
|
|
return JsonResponse({'status': 'error', 'message': 'Invalid content type'}, status=400)
|
|
|
|
# Security: Validate payload size (max 64KB)
|
|
if len(request.body) > 65536:
|
|
logger.warning(f"Oversized payload from {remote_ip}: {len(request.body)} bytes")
|
|
return JsonResponse({'status': 'error', 'message': 'Payload too large'}, status=413)
|
|
|
|
payload = request.body
|
|
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
|
|
|
logger.info(f"Webhook received from {remote_ip}, payload size: {len(payload)} bytes")
|
|
|
|
if not sig_header:
|
|
logger.warning(f"No Stripe signature from {remote_ip}")
|
|
return JsonResponse({'status': 'error', 'message': 'No signature'}, status=400)
|
|
|
|
try:
|
|
stripe_handler = StripePaymentHandler()
|
|
result = stripe_handler.handle_webhook(payload, sig_header)
|
|
|
|
if result['success']:
|
|
logger.info(f"Webhook processed successfully from {remote_ip}")
|
|
return JsonResponse({'status': 'success'})
|
|
else:
|
|
logger.error(f"Webhook processing failed from {remote_ip}: {result['error']}")
|
|
return JsonResponse({'status': 'error', 'message': 'Processing failed'}, status=400)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Webhook error from {remote_ip}: {e}")
|
|
return JsonResponse({'status': 'error', 'message': 'Internal error'}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
# @ratelimit(key='user', rate='10/m', method='POST', block=False)
|
|
def wallet_deduct_api(request):
|
|
"""API endpoint for wallet balance deduction (for direct N8N integration)"""
|
|
# Check if rate limited
|
|
if getattr(request, 'limited', False):
|
|
logger.warning(f"Wallet deduction rate limit exceeded for user {request.user.id}")
|
|
return JsonResponse({'error': 'Too many requests. Please wait a moment.'}, status=429)
|
|
|
|
try:
|
|
# Parse JSON request body
|
|
data = json.loads(request.body)
|
|
|
|
# Validate required fields
|
|
amount = data.get('amount')
|
|
description = data.get('description', 'Agent usage')
|
|
agent = data.get('agent', 'unknown')
|
|
|
|
if not amount:
|
|
return JsonResponse({'error': 'Amount is required'}, status=400)
|
|
|
|
# Validate amount
|
|
try:
|
|
amount = Decimal(str(amount))
|
|
if amount <= 0:
|
|
return JsonResponse({'error': 'Amount must be positive'}, status=400)
|
|
if amount > 1000: # Maximum deduction limit
|
|
return JsonResponse({'error': 'Amount exceeds maximum limit'}, status=400)
|
|
except (ValueError, TypeError):
|
|
return JsonResponse({'error': 'Invalid amount format'}, status=400)
|
|
|
|
# Check sufficient balance
|
|
if not request.user.has_sufficient_balance(amount):
|
|
return JsonResponse({
|
|
'error': 'Insufficient wallet balance',
|
|
'current_balance': float(request.user.wallet_balance)
|
|
}, status=400)
|
|
|
|
# Sanitize description
|
|
description = str(description)[:200] # Limit length
|
|
agent = str(agent)[:50] # Limit length
|
|
|
|
# Deduct balance
|
|
old_balance = request.user.wallet_balance
|
|
request.user.deduct_balance(amount, description, agent)
|
|
new_balance = request.user.wallet_balance
|
|
|
|
logger.info(f"Wallet deduction successful for user {request.user.id}: {amount} AED for {agent}")
|
|
|
|
return JsonResponse({
|
|
'success': True,
|
|
'message': 'Balance deducted successfully',
|
|
'old_balance': float(old_balance),
|
|
'new_balance': float(new_balance),
|
|
'deducted_amount': float(amount)
|
|
})
|
|
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({'error': 'Invalid JSON payload'}, status=400)
|
|
except Exception as e:
|
|
logger.error(f"Wallet deduction error for user {request.user.id}: {e}", exc_info=True)
|
|
return JsonResponse({'error': 'Internal error'}, status=500) |