mirror of
https://github.com/thecyberlearn/quantum-ai.git
synced 2026-08-18 09:53:00 +00:00
🔒 Implement comprehensive wallet security improvements
Critical Security Fixes: - Fix CSRF webhook vulnerability with IP validation and rate limiting - Secure debug endpoint with superuser-only access and data masking - Add rate limiting to all payment operations (5/min for top-up, 10/min for success) - Replace debug print statements with secure logging throughout Security Enhancements: - Stripe webhook IP whitelist validation with current IP ranges - Content type and payload size validation for webhooks - Comprehensive error handling with sanitized error messages - Proper logging for all payment operations and security events Payment System Improvements: - Enhanced duplicate payment prevention - Improved error handling and user feedback - Secure session verification and balance updates - Comprehensive audit trail for all payment operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3fc4783e64
commit
b145749ab2
@ -5,6 +5,7 @@ from django.http import JsonResponse
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||||
@ -12,6 +13,8 @@ stripe.api_key = settings.STRIPE_SECRET_KEY
|
|||||||
# ✅ CRITICAL: Set API version to match webhook configuration
|
# ✅ CRITICAL: Set API version to match webhook configuration
|
||||||
stripe.api_version = "2025-05-28.basil"
|
stripe.api_version = "2025-05-28.basil"
|
||||||
|
|
||||||
|
logger = logging.getLogger('wallet.payments')
|
||||||
|
|
||||||
|
|
||||||
class StripePaymentHandler:
|
class StripePaymentHandler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -32,15 +35,9 @@ class StripePaymentHandler:
|
|||||||
cancel_url = 'https://quantumtaskai.com/wallet/top-up/cancel/'
|
cancel_url = 'https://quantumtaskai.com/wallet/top-up/cancel/'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"🚀 [STRIPE DEBUG] Starting checkout session creation...")
|
logger.info(f"Creating checkout session for user {user.id}, amount: {amount} AED")
|
||||||
print(f"👤 User: {user.id} ({user.email})")
|
environment = 'production' if 'railway.app' in (request.get_host() if request else '') else 'development'
|
||||||
print(f"💰 Amount: {amount} AED")
|
logger.debug(f"Environment: {environment}, API version: {stripe.api_version}")
|
||||||
print(f"🔑 Stripe API Key configured: {bool(settings.STRIPE_SECRET_KEY)}")
|
|
||||||
print(f"🔑 API Version: {stripe.api_version}")
|
|
||||||
print(f"📍 Success URL: {success_url}")
|
|
||||||
print(f"📍 Cancel URL: {cancel_url}")
|
|
||||||
print(f"📍 Expected Webhook URL: https://quantumtaskai.com/stripe/webhook/")
|
|
||||||
print(f"🌍 Environment: {'production' if 'railway.app' in (request.get_host() if request else '') else 'development'}")
|
|
||||||
|
|
||||||
# Create session with modern Stripe practices
|
# Create session with modern Stripe practices
|
||||||
session = stripe.checkout.Session.create(
|
session = stripe.checkout.Session.create(
|
||||||
@ -104,32 +101,16 @@ class StripePaymentHandler:
|
|||||||
expires_at=int(time.time()) + (30 * 60), # 30 minutes from now
|
expires_at=int(time.time()) + (30 * 60), # 30 minutes from now
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"✅ [STRIPE DEBUG] Session created successfully!")
|
logger.info(f"Checkout session created successfully: {session.id}")
|
||||||
print(f" 💳 Session ID: {session.id}")
|
logger.debug(f"Session details - Amount: {session.amount_total / 100} AED, Status: {session.status}")
|
||||||
print(f" 👤 Client Reference: {session.client_reference_id}")
|
|
||||||
print(f" 👤 Customer Email: {session.customer_email}")
|
|
||||||
print(f" 💰 Amount Total: {session.amount_total} fils ({session.amount_total / 100} AED)")
|
|
||||||
print(f" 💱 Currency: {session.currency}")
|
|
||||||
print(f" 🔗 Payment URL: {session.url}")
|
|
||||||
print(f" 📊 Status: {session.status}")
|
|
||||||
print(f" 💳 Payment Status: {session.payment_status}")
|
|
||||||
print(f" ⏰ Created: {session.created}")
|
|
||||||
print(f" ⏰ Expires: {session.expires_at}")
|
|
||||||
print(f" 🏷️ Mode: {session.mode}")
|
|
||||||
print(f" 🆔 Object Type: {session.object}")
|
|
||||||
print(f" 📝 Metadata: {session.metadata}")
|
|
||||||
|
|
||||||
# CRITICAL: Verify session was created in correct Stripe account
|
# Verify session was created in correct Stripe account
|
||||||
print(f"🔍 [STRIPE DEBUG] Verifying session exists immediately...")
|
|
||||||
try:
|
try:
|
||||||
verification_session = stripe.checkout.Session.retrieve(session.id)
|
verification_session = stripe.checkout.Session.retrieve(session.id)
|
||||||
print(f"✅ [STRIPE DEBUG] Session verification successful!")
|
logger.debug(f"Session verification successful: {verification_session.id}")
|
||||||
print(f" 🔗 Retrieved Session ID: {verification_session.id}")
|
|
||||||
print(f" 📊 Retrieved Status: {verification_session.status}")
|
|
||||||
print(f" 👤 Retrieved Customer Email: {verification_session.customer_email}")
|
|
||||||
except Exception as verify_error:
|
except Exception as verify_error:
|
||||||
print(f"❌ [STRIPE DEBUG] Session verification FAILED: {verify_error}")
|
logger.error(f"Session verification failed: {verify_error}")
|
||||||
print(f"❌ This means the session was NOT created in the expected Stripe account!")
|
raise ValueError("Session creation verification failed")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'payment_url': session.url,
|
'payment_url': session.url,
|
||||||
@ -140,60 +121,36 @@ class StripePaymentHandler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
except stripe.error.StripeError as e:
|
except stripe.error.StripeError as e:
|
||||||
print(f"❌ [MODERN] Stripe error: {str(e)}")
|
logger.error(f"Stripe error creating checkout session: {e}")
|
||||||
raise ValueError(f"Failed to create checkout session: {str(e)}")
|
raise ValueError(f"Failed to create checkout session")
|
||||||
|
|
||||||
def verify_payment(self, session_id):
|
def verify_payment(self, session_id):
|
||||||
"""Verify payment directly from Stripe (bypasses webhook issues)"""
|
"""Verify payment directly from Stripe (bypasses webhook issues)"""
|
||||||
try:
|
try:
|
||||||
print(f"🔍 [STRIPE DEBUG] Starting payment verification...")
|
logger.info(f"Starting payment verification for session: {session_id}")
|
||||||
print(f"🔑 Stripe API Key configured: {bool(settings.STRIPE_SECRET_KEY)}")
|
|
||||||
print(f"🔑 API Version: {stripe.api_version}")
|
|
||||||
print(f"💳 Session ID to verify: {session_id}")
|
|
||||||
|
|
||||||
session = stripe.checkout.Session.retrieve(session_id)
|
session = stripe.checkout.Session.retrieve(session_id)
|
||||||
|
|
||||||
print(f"✅ [STRIPE DEBUG] Session retrieved successfully!")
|
logger.debug(f"Session retrieved - Status: {session.status}, Payment Status: {session.payment_status}")
|
||||||
print(f" 💳 Session ID: {session.id}")
|
logger.debug(f"Amount: {session.amount_total / 100} AED, User: {session.client_reference_id}")
|
||||||
print(f" 📊 Session Status: {session.status}")
|
|
||||||
print(f" 💳 Payment Status: {session.payment_status}")
|
|
||||||
print(f" 👤 Client Reference ID: {session.client_reference_id}")
|
|
||||||
print(f" 👤 Customer Email: {session.customer_email}")
|
|
||||||
print(f" 💰 Amount Total: {session.amount_total} fils ({session.amount_total / 100} AED)")
|
|
||||||
print(f" 💱 Currency: {session.currency}")
|
|
||||||
print(f" ⏰ Created: {session.created}")
|
|
||||||
print(f" ⏰ Expires At: {session.expires_at}")
|
|
||||||
print(f" 🏷️ Mode: {session.mode}")
|
|
||||||
print(f" 📝 Metadata: {session.metadata}")
|
|
||||||
print(f" 💳 Payment Intent: {getattr(session, 'payment_intent', 'None')}")
|
|
||||||
print(f" 🧾 Invoice: {getattr(session, 'invoice', 'None')}")
|
|
||||||
print(f" 🎯 Success URL: {getattr(session, 'success_url', 'None')}")
|
|
||||||
|
|
||||||
# Check if payment was actually completed
|
# Check if payment was actually completed
|
||||||
if hasattr(session, 'payment_intent') and session.payment_intent:
|
if hasattr(session, 'payment_intent') and session.payment_intent:
|
||||||
try:
|
try:
|
||||||
payment_intent = stripe.PaymentIntent.retrieve(session.payment_intent)
|
payment_intent = stripe.PaymentIntent.retrieve(session.payment_intent)
|
||||||
print(f"💳 [STRIPE DEBUG] Payment Intent Details:")
|
logger.debug(f"Payment intent status: {payment_intent.status}")
|
||||||
print(f" 🆔 Payment Intent ID: {payment_intent.id}")
|
|
||||||
print(f" 📊 Status: {payment_intent.status}")
|
|
||||||
print(f" 💰 Amount: {payment_intent.amount} fils ({payment_intent.amount / 100} AED)")
|
|
||||||
print(f" 💱 Currency: {payment_intent.currency}")
|
|
||||||
print(f" ⏰ Created: {payment_intent.created}")
|
|
||||||
print(f" 📝 Description: {payment_intent.description}")
|
|
||||||
except Exception as pi_error:
|
except Exception as pi_error:
|
||||||
print(f"❌ [STRIPE DEBUG] Could not retrieve Payment Intent: {pi_error}")
|
logger.warning(f"Could not retrieve payment intent: {pi_error}")
|
||||||
|
|
||||||
if session.payment_status == 'paid' and session.status == 'complete':
|
if session.payment_status == 'paid' and session.status == 'complete':
|
||||||
user_id = session.client_reference_id
|
user_id = session.client_reference_id
|
||||||
amount = session.amount_total / 100 # Convert from cents
|
amount = session.amount_total / 100 # Convert from cents
|
||||||
|
|
||||||
print(f"✅ VERIFY: Payment successful - User: {user_id}, Amount: {amount}")
|
logger.info(f"Payment successful - User: {user_id}, Amount: {amount} AED")
|
||||||
|
|
||||||
# Process the payment manually (bypass webhook)
|
# Process the payment manually (bypass webhook)
|
||||||
if user_id:
|
if user_id:
|
||||||
try:
|
try:
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
User = get_user_model()
|
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
|
|
||||||
# Check if already processed to avoid double-charging
|
# Check if already processed to avoid double-charging
|
||||||
@ -201,13 +158,13 @@ class StripePaymentHandler:
|
|||||||
existing = WalletTransaction.objects.filter(stripe_session_id=session_id).first()
|
existing = WalletTransaction.objects.filter(stripe_session_id=session_id).first()
|
||||||
|
|
||||||
if not existing:
|
if not existing:
|
||||||
print(f"💰 VERIFY: Processing payment for {user.email}")
|
logger.info(f"Processing payment for user {user_id}")
|
||||||
user.add_balance(
|
user.add_balance(
|
||||||
amount=amount,
|
amount=amount,
|
||||||
description=f"Wallet top-up via Stripe",
|
description=f"Wallet top-up via Stripe",
|
||||||
stripe_session_id=session_id
|
stripe_session_id=session_id
|
||||||
)
|
)
|
||||||
print(f"✅ VERIFY: Balance updated successfully")
|
logger.info(f"Balance updated successfully for user {user_id}")
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
@ -216,7 +173,7 @@ class StripePaymentHandler:
|
|||||||
'message': 'Payment processed via manual verification'
|
'message': 'Payment processed via manual verification'
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
print(f"⚠️ VERIFY: Payment already processed")
|
logger.info(f"Payment already processed for session {session_id}")
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
@ -226,8 +183,8 @@ class StripePaymentHandler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ VERIFY: Error processing payment: {e}")
|
logger.error(f"Error processing payment for user {user_id}: {e}")
|
||||||
return {'success': False, 'error': f'Processing error: {e}'}
|
return {'success': False, 'error': 'Processing error'}
|
||||||
else:
|
else:
|
||||||
return {'success': False, 'error': 'No user ID in session'}
|
return {'success': False, 'error': 'No user ID in session'}
|
||||||
|
|
||||||
@ -237,59 +194,67 @@ class StripePaymentHandler:
|
|||||||
return {'success': False, 'error': f'Payment status: {session.payment_status}'}
|
return {'success': False, 'error': f'Payment status: {session.payment_status}'}
|
||||||
|
|
||||||
except stripe.error.StripeError as e:
|
except stripe.error.StripeError as e:
|
||||||
print(f"❌ VERIFY: Stripe error: {e}")
|
logger.error(f"Stripe error during payment verification: {e}")
|
||||||
return {'success': False, 'error': str(e)}
|
return {'success': False, 'error': 'Payment verification failed'}
|
||||||
|
|
||||||
def handle_webhook(self, payload, signature):
|
def handle_webhook(self, payload, signature):
|
||||||
"""Handle Stripe webhook events"""
|
"""Handle Stripe webhook events"""
|
||||||
print(f"🔍 Processing webhook with signature: {bool(signature)}")
|
logger.info("Processing webhook event")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event = stripe.Webhook.construct_event(
|
event = stripe.Webhook.construct_event(
|
||||||
payload, signature, settings.STRIPE_WEBHOOK_SECRET
|
payload, signature, settings.STRIPE_WEBHOOK_SECRET
|
||||||
)
|
)
|
||||||
print(f"📋 Event type: {event['type']}")
|
logger.info(f"Webhook event type: {event['type']}")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"❌ Invalid payload: {e}")
|
logger.error(f"Invalid webhook payload: {e}")
|
||||||
return {'success': False, 'error': 'Invalid payload'}
|
return {'success': False, 'error': 'Invalid payload'}
|
||||||
except stripe.error.SignatureVerificationError as e:
|
except stripe.error.SignatureVerificationError as e:
|
||||||
print(f"❌ Invalid signature: {e}")
|
logger.error(f"Invalid webhook signature: {e}")
|
||||||
return {'success': False, 'error': 'Invalid signature'}
|
return {'success': False, 'error': 'Invalid signature'}
|
||||||
|
|
||||||
if event['type'] == 'checkout.session.completed':
|
if event['type'] == 'checkout.session.completed':
|
||||||
session = event['data']['object']
|
session = event['data']['object']
|
||||||
print(f"💳 Processing checkout session: {session['id']}")
|
session_id = session['id']
|
||||||
|
logger.info(f"Processing checkout session completion: {session_id}")
|
||||||
|
|
||||||
# Process successful payment
|
# Process successful payment
|
||||||
user_id = session.get('client_reference_id')
|
user_id = session.get('client_reference_id')
|
||||||
amount = session['amount_total'] / 100 # Convert from cents
|
amount = session['amount_total'] / 100 # Convert from cents
|
||||||
|
|
||||||
print(f"👤 User ID: {user_id}, Amount: {amount} AED")
|
logger.info(f"Webhook payment - User: {user_id}, Amount: {amount} AED")
|
||||||
print(f"📋 Session data: client_reference_id={session.get('client_reference_id')}")
|
|
||||||
print(f"📋 Session metadata: {session.get('metadata', {})}")
|
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
print(f"✅ Found user: {user.email}, Current balance: {user.wallet_balance}")
|
logger.info(f"Found user for webhook payment: {user_id}")
|
||||||
|
|
||||||
user.add_balance(
|
# Check if already processed to avoid double-charging
|
||||||
amount=amount,
|
from wallet.models import WalletTransaction
|
||||||
description=f"Wallet top-up via Stripe",
|
existing = WalletTransaction.objects.filter(stripe_session_id=session_id).first()
|
||||||
stripe_session_id=session['id']
|
|
||||||
)
|
if not existing:
|
||||||
user.refresh_from_db()
|
user.add_balance(
|
||||||
print(f"💰 New balance: {user.wallet_balance}")
|
amount=amount,
|
||||||
|
description=f"Wallet top-up via Stripe",
|
||||||
|
stripe_session_id=session_id
|
||||||
|
)
|
||||||
|
logger.info(f"Webhook payment processed for user {user_id}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Webhook payment already processed: {session_id}")
|
||||||
|
|
||||||
return {'success': True, 'message': 'Payment processed successfully'}
|
return {'success': True, 'message': 'Payment processed successfully'}
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
print(f"❌ User not found: {user_id}")
|
logger.error(f"User not found for webhook: {user_id}")
|
||||||
return {'success': False, 'error': 'User not found'}
|
return {'success': False, 'error': 'User not found'}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing webhook payment: {e}")
|
||||||
|
return {'success': False, 'error': 'Processing error'}
|
||||||
else:
|
else:
|
||||||
print("❌ No user_id in session")
|
logger.error("No user ID in webhook session")
|
||||||
return {'success': False, 'error': 'No user reference'}
|
return {'success': False, 'error': 'No user reference'}
|
||||||
else:
|
else:
|
||||||
print(f"ℹ️ Ignored event type: {event['type']}")
|
logger.debug(f"Ignored webhook event type: {event['type']}")
|
||||||
|
|
||||||
return {'success': True, 'message': 'Event processed'}
|
return {'success': True, 'message': 'Event processed'}
|
||||||
|
|
||||||
|
|||||||
228
wallet/views.py
228
wallet/views.py
@ -1,15 +1,27 @@
|
|||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
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
|
from .stripe_handler import StripePaymentHandler
|
||||||
import datetime
|
import datetime
|
||||||
import stripe
|
import stripe
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import logging
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
# Global webhook logs for debugging (in production, use proper logging)
|
logger = logging.getLogger(__name__)
|
||||||
webhook_logs = []
|
|
||||||
|
# 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
|
@login_required
|
||||||
@ -32,14 +44,22 @@ def wallet_view(request):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ratelimit(key='user', rate='5/m', method=UNSAFE, block=False)
|
||||||
def wallet_topup_view(request):
|
def wallet_topup_view(request):
|
||||||
"""Wallet top-up page"""
|
"""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':
|
if request.method == 'POST':
|
||||||
amount = request.POST.get('amount')
|
amount = request.POST.get('amount')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
amount = float(amount)
|
amount = float(amount)
|
||||||
if amount not in [10, 50, 100, 500]:
|
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')
|
messages.error(request, 'Invalid amount selected')
|
||||||
return redirect('wallet:wallet_topup')
|
return redirect('wallet:wallet_topup')
|
||||||
|
|
||||||
@ -47,44 +67,58 @@ def wallet_topup_view(request):
|
|||||||
stripe_handler = StripePaymentHandler()
|
stripe_handler = StripePaymentHandler()
|
||||||
session_data = stripe_handler.create_checkout_session(request.user, amount, request)
|
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'])
|
return redirect(session_data['payment_url'])
|
||||||
|
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError) as e:
|
||||||
|
logger.error(f"Invalid amount format from user {request.user.id}: {e}")
|
||||||
messages.error(request, 'Invalid amount')
|
messages.error(request, 'Invalid amount')
|
||||||
return redirect('wallet:wallet_topup')
|
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, 'Unable to process payment at this time. Please try again.')
|
||||||
|
return redirect('wallet:wallet_topup')
|
||||||
|
|
||||||
return render(request, 'wallet/wallet_topup.html')
|
return render(request, 'wallet/wallet_topup.html')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ratelimit(key='user', rate='10/m', method='GET', block=False)
|
||||||
def wallet_topup_success_view(request):
|
def wallet_topup_success_view(request):
|
||||||
"""Payment success page with automatic payment verification (NO WEBHOOKS NEEDED)"""
|
"""Payment success page with automatic payment verification"""
|
||||||
|
# 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')
|
session_id = request.GET.get('session_id')
|
||||||
|
|
||||||
if not session_id:
|
if not session_id:
|
||||||
|
logger.warning(f"No session ID provided for user {request.user.id}")
|
||||||
messages.error(request, 'No payment session found. Please contact support if you completed a payment.')
|
messages.error(request, 'No payment session found. Please contact support if you completed a payment.')
|
||||||
return redirect('wallet:wallet')
|
return redirect('wallet:wallet')
|
||||||
|
|
||||||
# Verify payment directly with Stripe API (bypasses webhook issues)
|
# Verify payment directly with Stripe API
|
||||||
try:
|
try:
|
||||||
stripe_handler = StripePaymentHandler()
|
stripe_handler = StripePaymentHandler()
|
||||||
|
|
||||||
print(f"💳 [SUCCESS PAGE] Verifying payment for session: {session_id}")
|
logger.info(f"Verifying payment for user {request.user.id}, session: {session_id}")
|
||||||
result = stripe_handler.verify_payment(session_id)
|
result = stripe_handler.verify_payment(session_id)
|
||||||
|
|
||||||
if result['success']:
|
if result['success']:
|
||||||
if result['processed']:
|
if result['processed']:
|
||||||
messages.success(request, f'Payment successful! {result["amount"]} AED has been added to your wallet.')
|
messages.success(request, f'Payment successful! {result["amount"]} AED has been added to your wallet.')
|
||||||
print(f"✅ [SUCCESS PAGE] Payment verified and wallet updated for user {request.user.id}")
|
logger.info(f"Payment verified and wallet updated for user {request.user.id}")
|
||||||
else:
|
else:
|
||||||
messages.info(request, 'Payment already processed. Your wallet balance is up to date.')
|
messages.info(request, 'Payment already processed. Your wallet balance is up to date.')
|
||||||
print(f"ℹ️ [SUCCESS PAGE] Payment already processed for session {session_id}")
|
logger.info(f"Payment already processed for session {session_id}")
|
||||||
else:
|
else:
|
||||||
messages.warning(request, f'Payment verification failed: {result.get("error", "Unknown error")}. Please contact support.')
|
messages.warning(request, 'Payment verification failed. Please contact support.')
|
||||||
print(f"❌ [SUCCESS PAGE] Payment verification failed: {result}")
|
logger.error(f"Payment verification failed for user {request.user.id}: {result.get('error', 'Unknown error')}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ [SUCCESS PAGE] Error verifying payment: {e}")
|
logger.error(f"Error verifying payment for user {request.user.id}: {e}")
|
||||||
messages.error(request, 'Unable to verify payment. Please contact support if you completed a payment.')
|
messages.error(request, 'Unable to verify payment. Please contact support if you completed a payment.')
|
||||||
|
|
||||||
return redirect('wallet:wallet')
|
return redirect('wallet:wallet')
|
||||||
@ -98,147 +132,103 @@ def wallet_topup_cancel_view(request):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@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):
|
def stripe_debug_view(request):
|
||||||
"""Debug endpoint to show Stripe API configuration and test connectivity"""
|
"""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 = {
|
debug_info = {
|
||||||
'timestamp': datetime.datetime.now().isoformat(),
|
'timestamp': datetime.datetime.now().isoformat(),
|
||||||
'user_id': request.user.id,
|
'user_id': request.user.id,
|
||||||
'user_email': request.user.email,
|
'environment': 'production' if not settings.DEBUG else 'development',
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Test Stripe API connectivity
|
# Basic connectivity test without exposing sensitive data
|
||||||
print(f"🔍 [STRIPE DEBUG] Testing Stripe API connectivity...")
|
|
||||||
|
|
||||||
# Get API key info (masked)
|
|
||||||
api_key = settings.STRIPE_SECRET_KEY
|
api_key = settings.STRIPE_SECRET_KEY
|
||||||
debug_info['stripe_api_key_last4'] = api_key[-4:] if api_key else 'Not set'
|
debug_info['stripe_api_configured'] = bool(api_key)
|
||||||
debug_info['stripe_api_key_prefix'] = api_key[:7] if api_key else 'Not set'
|
|
||||||
debug_info['stripe_api_version'] = stripe.api_version
|
debug_info['stripe_api_version'] = stripe.api_version
|
||||||
|
|
||||||
# Test account connectivity
|
# Test account connectivity (minimal info)
|
||||||
try:
|
try:
|
||||||
account = stripe.Account.retrieve()
|
account = stripe.Account.retrieve()
|
||||||
debug_info['stripe_account'] = {
|
debug_info['stripe_account'] = {
|
||||||
'id': account.id,
|
'id': account.id[:8] + '...', # Partial ID only
|
||||||
'email': account.email,
|
|
||||||
'display_name': account.display_name,
|
|
||||||
'country': account.country,
|
|
||||||
'default_currency': account.default_currency,
|
|
||||||
'business_profile': account.business_profile,
|
|
||||||
'charges_enabled': account.charges_enabled,
|
'charges_enabled': account.charges_enabled,
|
||||||
'payouts_enabled': account.payouts_enabled,
|
'payouts_enabled': account.payouts_enabled,
|
||||||
}
|
}
|
||||||
print(f"✅ [STRIPE DEBUG] Account connected: {account.id}")
|
logger.info(f"Stripe debug accessed by superuser {request.user.id}")
|
||||||
except Exception as account_error:
|
except Exception as account_error:
|
||||||
debug_info['stripe_account_error'] = str(account_error)
|
debug_info['stripe_account_error'] = 'Connection failed'
|
||||||
print(f"❌ [STRIPE DEBUG] Account error: {account_error}")
|
logger.error(f"Stripe account error in debug: {account_error}")
|
||||||
|
|
||||||
# Test recent checkout sessions
|
|
||||||
try:
|
|
||||||
sessions = stripe.checkout.Session.list(limit=5)
|
|
||||||
debug_info['recent_sessions'] = []
|
|
||||||
for session in sessions.data:
|
|
||||||
debug_info['recent_sessions'].append({
|
|
||||||
'id': session.id,
|
|
||||||
'status': session.status,
|
|
||||||
'payment_status': session.payment_status,
|
|
||||||
'amount_total': session.amount_total,
|
|
||||||
'currency': session.currency,
|
|
||||||
'customer_email': session.customer_email,
|
|
||||||
'client_reference_id': session.client_reference_id,
|
|
||||||
'created': session.created,
|
|
||||||
'metadata': session.metadata,
|
|
||||||
})
|
|
||||||
print(f"✅ [STRIPE DEBUG] Retrieved {len(sessions.data)} recent sessions")
|
|
||||||
except Exception as sessions_error:
|
|
||||||
debug_info['sessions_error'] = str(sessions_error)
|
|
||||||
print(f"❌ [STRIPE DEBUG] Sessions error: {sessions_error}")
|
|
||||||
|
|
||||||
# Test recent payments
|
|
||||||
try:
|
|
||||||
charges = stripe.Charge.list(limit=5)
|
|
||||||
debug_info['recent_charges'] = []
|
|
||||||
for charge in charges.data:
|
|
||||||
debug_info['recent_charges'].append({
|
|
||||||
'id': charge.id,
|
|
||||||
'amount': charge.amount,
|
|
||||||
'currency': charge.currency,
|
|
||||||
'status': charge.status,
|
|
||||||
'paid': charge.paid,
|
|
||||||
'customer': charge.customer,
|
|
||||||
'description': charge.description,
|
|
||||||
'created': charge.created,
|
|
||||||
'metadata': charge.metadata,
|
|
||||||
})
|
|
||||||
print(f"✅ [STRIPE DEBUG] Retrieved {len(charges.data)} recent charges")
|
|
||||||
except Exception as charges_error:
|
|
||||||
debug_info['charges_error'] = str(charges_error)
|
|
||||||
print(f"❌ [STRIPE DEBUG] Charges error: {charges_error}")
|
|
||||||
|
|
||||||
debug_info['status'] = 'success'
|
debug_info['status'] = 'success'
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_info['error'] = str(e)
|
debug_info['error'] = 'Configuration error'
|
||||||
debug_info['status'] = 'error'
|
debug_info['status'] = 'error'
|
||||||
print(f"❌ [STRIPE DEBUG] General error: {e}")
|
logger.error(f"Stripe debug error: {e}")
|
||||||
|
|
||||||
return JsonResponse(debug_info, indent=2)
|
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
|
@csrf_exempt
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@ratelimit(key='ip', rate='50/m', method='POST', block=True)
|
||||||
def stripe_webhook_view(request):
|
def stripe_webhook_view(request):
|
||||||
"""Handle Stripe webhook events with comprehensive logging"""
|
"""Secure Stripe webhook handler with IP validation and rate limiting"""
|
||||||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
timestamp = datetime.datetime.now().isoformat()
|
||||||
|
remote_ip = request.META.get('REMOTE_ADDR', 'unknown')
|
||||||
|
|
||||||
# Log everything for debugging
|
# Security: Validate request comes from Stripe
|
||||||
print(f"🎯 [{timestamp}] Stripe webhook received!")
|
if not is_stripe_ip(remote_ip) and not settings.DEBUG:
|
||||||
print(f"🎯 Method: {request.method}")
|
logger.warning(f"Webhook from unauthorized IP: {remote_ip}")
|
||||||
print(f"🎯 Content-Type: {request.content_type}")
|
return JsonResponse({'status': 'error', 'message': 'Unauthorized'}, status=401)
|
||||||
print(f"🎯 Remote IP: {request.META.get('REMOTE_ADDR', 'unknown')}")
|
|
||||||
print(f"🎯 User Agent: {request.META.get('HTTP_USER_AGENT', 'unknown')}")
|
|
||||||
print(f"🎯 Full headers: {dict(request.META)}")
|
|
||||||
|
|
||||||
# Store in webhook logs for the test page
|
# Security: Validate content type
|
||||||
webhook_log_entry = {
|
if request.content_type != 'application/json':
|
||||||
'timestamp': timestamp,
|
logger.warning(f"Invalid content type from {remote_ip}: {request.content_type}")
|
||||||
'method': request.method,
|
return JsonResponse({'status': 'error', 'message': 'Invalid content type'}, status=400)
|
||||||
'headers': dict(request.META),
|
|
||||||
'body': request.body.decode('utf-8') if request.body else '',
|
|
||||||
'content_type': request.content_type,
|
|
||||||
'source': 'stripe_webhook',
|
|
||||||
'ip_address': request.META.get('REMOTE_ADDR', 'unknown'),
|
|
||||||
'user_agent': request.META.get('HTTP_USER_AGENT', 'unknown')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add to webhook logs
|
# Security: Validate payload size (max 64KB)
|
||||||
webhook_logs.append(webhook_log_entry)
|
if len(request.body) > 65536:
|
||||||
if len(webhook_logs) > 50:
|
logger.warning(f"Oversized payload from {remote_ip}: {len(request.body)} bytes")
|
||||||
webhook_logs.pop(0)
|
return JsonResponse({'status': 'error', 'message': 'Payload too large'}, status=413)
|
||||||
|
|
||||||
if request.method != 'POST':
|
|
||||||
print(f"❌ Invalid method: {request.method}")
|
|
||||||
return JsonResponse({'status': 'error', 'message': f'Method {request.method} not allowed'}, status=405)
|
|
||||||
|
|
||||||
payload = request.body
|
payload = request.body
|
||||||
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
sig_header = request.META.get('HTTP_STRIPE_SIGNATURE')
|
||||||
|
|
||||||
print(f"📦 Payload length: {len(payload)} bytes")
|
logger.info(f"Webhook received from {remote_ip}, payload size: {len(payload)} bytes")
|
||||||
print(f"📦 Payload preview: {payload[:200]}...")
|
|
||||||
print(f"🔐 Signature header: {sig_header is not None}")
|
|
||||||
print(f"🔐 Full signature header: {sig_header}")
|
|
||||||
|
|
||||||
# Always return success first to see if Stripe is reaching us
|
|
||||||
if not sig_header:
|
if not sig_header:
|
||||||
print(f"⚠️ No Stripe signature - might be a test request")
|
logger.warning(f"No Stripe signature from {remote_ip}")
|
||||||
return JsonResponse({'status': 'received', 'message': 'No signature verification'})
|
return JsonResponse({'status': 'error', 'message': 'No signature'}, status=400)
|
||||||
|
|
||||||
stripe_handler = StripePaymentHandler()
|
try:
|
||||||
result = stripe_handler.handle_webhook(payload, sig_header)
|
stripe_handler = StripePaymentHandler()
|
||||||
|
result = stripe_handler.handle_webhook(payload, sig_header)
|
||||||
print(f"✅ Webhook result: {result}")
|
|
||||||
|
if result['success']:
|
||||||
if result['success']:
|
logger.info(f"Webhook processed successfully from {remote_ip}")
|
||||||
return JsonResponse({'status': 'success'})
|
return JsonResponse({'status': 'success'})
|
||||||
else:
|
else:
|
||||||
return JsonResponse({'status': 'error', 'message': result['error']}, status=400)
|
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)
|
||||||
Loading…
Reference in New Issue
Block a user