Corrige bugs de la revisión de home/views.py (dinero, bypass de pago, 500s)
Seguridad / dinero: - store_novawow_view: recalcula precios y valida ítems contra la BD (Item) en vez de confiar en el precio/ítems que manda el cliente; guarda en sesión un carrito ya validado para la entrega - *_success_view (x9): @require_paid_stripe verifica el pago real en Stripe por session_id (Stripe lo añade a la success_url) y marca StripeLog.fulfilled para evitar re-entregas; cierra el bypass de ir directo a la success-url sin pagar - gold_character_view: deja de truncar el precio (int(4.99)->4); pasa el precio real Correctitud / 500s: - change_password_view: .get(...,'') evita AttributeError si falta un campo - revive_character_view: lógica de éxito corregida (vacío=éxito, no vacío=error, None=fallo de comunicación) — antes marcaba error como éxito y caía a HTML - store category y recruit_a_friend: guardas para Category.DoesNotExist y cuenta None Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.7 on 2026-07-12 17:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0012_alter_accountactivation_salt_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stripelog',
|
||||
name='fulfilled',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -296,6 +296,7 @@ class StripeLog(models.Model):
|
||||
mode = models.CharField(max_length=10, choices=[("test", "Test"), ("live", "Live")]) # Modo de Stripe
|
||||
character_name = models.CharField(max_length=100) # Nombre del personaje que compró
|
||||
timestamp = models.DateTimeField(auto_now_add=True) # Fecha y hora
|
||||
fulfilled = models.BooleanField(default=False) # Si la entrega ya se procesó (evita re-entregas)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.username} compró {self.product_name} por {self.amount}€ ({self.mode})"
|
||||
|
||||
@@ -4,8 +4,25 @@ from .models import StripeLog
|
||||
from django.utils.timezone import now
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
|
||||
def is_session_paid(session_id):
|
||||
"""True si la sesión de Stripe existe y está realmente pagada."""
|
||||
if not session_id:
|
||||
return False
|
||||
try:
|
||||
s = stripe.checkout.Session.retrieve(session_id)
|
||||
return bool(s and getattr(s, "payment_status", None) == "paid")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def create_checkout_session(account_id, username, email, acore_ip, amount, character_name, currency="eur", success_url=None, cancel_url=None, product_name=""):
|
||||
try:
|
||||
# Añadir el id de sesión a la success_url para poder verificar el pago
|
||||
# al volver (Stripe sustituye {CHECKOUT_SESSION_ID} por el id real).
|
||||
if success_url:
|
||||
sep = "&" if "?" in success_url else "?"
|
||||
success_url = f"{success_url}{sep}session_id={{CHECKOUT_SESSION_ID}}"
|
||||
|
||||
# 🔹 Crear sesión de pago en Stripe
|
||||
session = stripe.checkout.Session.create(
|
||||
payment_method_types=["card"],
|
||||
|
||||
+97
-26
@@ -8,7 +8,8 @@ from django.contrib.auth import logout
|
||||
from .pagos_sumup import crear_checkout, crear_checkout_battlepay, obtener_checkout
|
||||
from django import forms
|
||||
import stripe
|
||||
from .pagos_stripe import create_checkout_session
|
||||
from .pagos_stripe import create_checkout_session, is_session_paid
|
||||
from functools import wraps
|
||||
from .models import Noticia, ClienteCategoria, ServerSelection, RecruitAFriend, DownloadClientPage, ContentCreator, RecruitReward, ClaimedReward, AccountActivation, SecurityToken, GuildRenameSettings, VoteSite, VoteLog, HomeApiPoints, UnstuckHistory, ReviveHistory, RenamePrice, CustomizePrice, ChangeRacePrice, ChangeFactionPrice, LevelUpPrice, GoldPrice, TransferPrice, Category, Item, StripeLog, LoginAttempt, Pedido
|
||||
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@@ -26,6 +27,33 @@ from decimal import Decimal, ROUND_HALF_UP
|
||||
# Configuración del logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def require_paid_stripe(redirect_to='index'):
|
||||
"""
|
||||
Protege las vistas de "éxito" de pago: exige un `session_id` de Stripe
|
||||
realmente PAGADO en la URL (Stripe lo añade tras el pago) y evita re-entregas
|
||||
marcando el StripeLog como `fulfilled`. Cierra el bypass de ir directo a la
|
||||
success-url sin pagar.
|
||||
"""
|
||||
def deco(view):
|
||||
@wraps(view)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
session_id = request.GET.get('session_id')
|
||||
if not is_session_paid(session_id):
|
||||
messages.error(request, 'No se ha podido verificar el pago.')
|
||||
return redirect(redirect_to)
|
||||
log = StripeLog.objects.filter(session_id=session_id).first()
|
||||
if log is not None and log.fulfilled:
|
||||
messages.info(request, 'Este pago ya había sido procesado.')
|
||||
return redirect(redirect_to)
|
||||
response = view(request, *args, **kwargs)
|
||||
if log is not None:
|
||||
log.fulfilled = True
|
||||
log.save(update_fields=['fulfilled'])
|
||||
return response
|
||||
return wrapper
|
||||
return deco
|
||||
|
||||
@csrf_exempt
|
||||
def stripe_webhook(request):
|
||||
payload = request.body
|
||||
@@ -541,7 +569,10 @@ def recruit_a_friend_view(request):
|
||||
if request.method == 'POST':
|
||||
if not username:
|
||||
return JsonResponse({'success': False, 'message': 'Debes estar logueado para reclamar una recompensa.'})
|
||||
|
||||
|
||||
if not account_id or account_status is None:
|
||||
return JsonResponse({'success': False, 'message': 'No se ha encontrado tu cuenta.'})
|
||||
|
||||
reward_id = request.POST.get('reward_id')
|
||||
character_name = request.POST.get('character')
|
||||
|
||||
@@ -903,10 +934,10 @@ def change_password_view(request):
|
||||
email = user_data['email']
|
||||
|
||||
if request.method == 'POST':
|
||||
current_password = request.POST.get('cur-password').strip()
|
||||
new_password = request.POST.get('new-password').strip()
|
||||
conf_new_password = request.POST.get('conf-new-password').strip()
|
||||
security_token = request.POST.get('security-token').strip()
|
||||
current_password = request.POST.get('cur-password', '').strip()
|
||||
new_password = request.POST.get('new-password', '').strip()
|
||||
conf_new_password = request.POST.get('conf-new-password', '').strip()
|
||||
security_token = request.POST.get('security-token', '').strip()
|
||||
|
||||
# Validar campos vacíos
|
||||
if not current_password or not new_password or not conf_new_password or not security_token:
|
||||
@@ -1321,6 +1352,7 @@ def rename_character_view(request):
|
||||
|
||||
return render(request, 'account/rename_character.html', {'characters': characters, 'price': price})
|
||||
|
||||
@require_paid_stripe()
|
||||
def rename_success_view(request):
|
||||
# Procesar el comando SOAP tras el pago exitoso
|
||||
character_name = request.session.get('rename_character')
|
||||
@@ -1592,6 +1624,7 @@ def customize_character_view(request):
|
||||
return render(request, 'account/customize_character.html', {'characters': characters, 'price': price})
|
||||
|
||||
|
||||
@require_paid_stripe()
|
||||
def customize_success_view(request):
|
||||
# Procesar el comando SOAP tras el pago exitoso
|
||||
character_name = request.session.get('customize_character')
|
||||
@@ -1682,6 +1715,7 @@ def change_race_character_view(request):
|
||||
return render(request, 'account/change_race.html', {'characters': characters, 'price': price})
|
||||
|
||||
|
||||
@require_paid_stripe()
|
||||
def change_race_success_view(request):
|
||||
# Procesar el comando SOAP tras el pago exitoso
|
||||
character_name = request.session.get('change_race_character')
|
||||
@@ -1772,6 +1806,7 @@ def change_faction_character_view(request):
|
||||
return render(request, 'account/change_faction.html', {'characters': characters, 'price': price})
|
||||
|
||||
|
||||
@require_paid_stripe()
|
||||
def change_faction_success_view(request):
|
||||
# Procesar el comando SOAP tras el pago exitoso
|
||||
character_name = request.session.get('change_faction_character')
|
||||
@@ -1863,6 +1898,7 @@ def level_up_character_view(request):
|
||||
return render(request, 'account/level_up.html', {'characters': characters, 'price': price})
|
||||
|
||||
|
||||
@require_paid_stripe()
|
||||
def level_up_success_view(request):
|
||||
character_name = request.session.get('levelup_character')
|
||||
if not character_name:
|
||||
@@ -1944,8 +1980,8 @@ def gold_character_view(request):
|
||||
if not gold_price:
|
||||
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Cantidad de oro no válida.</span>'})
|
||||
|
||||
# Convertir el precio a formato que Stripe acepta (en centavos)
|
||||
stripe_price = int(float(gold_price.price) * 1)
|
||||
# El precio va en euros (con decimales); el helper lo pasa a céntimos (×100)
|
||||
stripe_price = gold_price.price
|
||||
|
||||
# Crear sesión de pago con Stripe
|
||||
success_url = request.build_absolute_uri(reverse('gold-success'))
|
||||
@@ -1981,6 +2017,7 @@ def gold_character_view(request):
|
||||
return render(request, 'account/gold_character.html', {'characters': characters, 'gold_prices': gold_prices})
|
||||
|
||||
|
||||
@require_paid_stripe()
|
||||
def gold_success_view(request):
|
||||
# Procesar el pago exitoso
|
||||
transaction = request.session.pop('gold_transaction', None)
|
||||
@@ -2129,6 +2166,7 @@ def transfer_character_view(request):
|
||||
|
||||
|
||||
|
||||
@require_paid_stripe()
|
||||
def transfer_success_view(request):
|
||||
transfer_data = request.session.get('transfer_data')
|
||||
if not transfer_data:
|
||||
@@ -2185,8 +2223,11 @@ def store_novawow_view(request):
|
||||
|
||||
# Si se está solicitando una categoría específica, devolver solo los ítems de esa categoría
|
||||
if category_name:
|
||||
items_in_category = Category.objects.get(name=category_name).items.values()
|
||||
return JsonResponse({'success': True, 'items': list(items_in_category)})
|
||||
try:
|
||||
category = Category.objects.get(name=category_name)
|
||||
except Category.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'message': 'Categoría no encontrada'}, status=404)
|
||||
return JsonResponse({'success': True, 'items': list(category.items.values())})
|
||||
|
||||
# Obtener carrito desde la sesión
|
||||
carrito = request.session.get('carrito', [])
|
||||
@@ -2199,25 +2240,49 @@ def store_novawow_view(request):
|
||||
# Obtener datos del body
|
||||
data = json.loads(request.body)
|
||||
character_name = data.get('character')
|
||||
carrito = data.get('carrito')
|
||||
carrito_cliente = data.get('carrito') or []
|
||||
|
||||
if not character_name:
|
||||
return JsonResponse({'success': False, 'message': 'Por favor, selecciona un personaje.'})
|
||||
|
||||
# ACTUALIZAR EL CARRITO Y FORZAR LA SESIÓN
|
||||
request.session['carrito'] = carrito
|
||||
request.session.modified = True # Forzar que Django guarde la sesión inmediatamente
|
||||
|
||||
# RECALCULAR EL PRECIO CON EL CARRITO ACTUALIZADO
|
||||
total_price = sum(Decimal(item['precio']) for item in carrito)
|
||||
total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
|
||||
# Verificar si el personaje pertenece al usuario
|
||||
if character_name not in characters:
|
||||
return JsonResponse({'success': False, 'message': 'No tienes permiso para renombrar este personaje.'})
|
||||
return JsonResponse({'success': False, 'message': 'No tienes permiso para este personaje.'})
|
||||
|
||||
# Definir siempre `product_description` antes de usarlo
|
||||
product_description = ", ".join([f"{item.get('nombre', 'Ítem desconocido')} x{item.get('cantidad', 1)}" for item in carrito])
|
||||
if not carrito_cliente:
|
||||
return JsonResponse({'success': False, 'message': 'El carrito está vacío.'})
|
||||
|
||||
# VALIDAR ÍTEMS Y PRECIOS CONTRA LA BASE DE DATOS (nunca confiar en el
|
||||
# precio ni en los ítems que manda el cliente).
|
||||
try:
|
||||
ids_solicitados = [int(item['id']) for item in carrito_cliente]
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return JsonResponse({'success': False, 'message': 'Carrito no válido.'})
|
||||
|
||||
items_db = {i.item_id: i for i in Item.objects.filter(item_id__in=ids_solicitados)}
|
||||
|
||||
carrito = [] # carrito saneado que se guardará en sesión
|
||||
total_price = Decimal('0.00')
|
||||
for item in carrito_cliente:
|
||||
item_id = int(item['id'])
|
||||
db_item = items_db.get(item_id)
|
||||
if not db_item:
|
||||
return JsonResponse({'success': False, 'message': f'Ítem no disponible en la tienda (ID {item_id}).'})
|
||||
try:
|
||||
cantidad = max(1, int(item.get('cantidad', 1)))
|
||||
except (TypeError, ValueError):
|
||||
cantidad = 1
|
||||
total_price += db_item.price * cantidad
|
||||
carrito.append({'id': item_id, 'nombre': db_item.name, 'cantidad': cantidad})
|
||||
|
||||
total_price_with_iva = (total_price * Decimal('1.21')).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
|
||||
# Guardar en sesión el carrito YA VALIDADO (para la entrega tras el pago)
|
||||
request.session['carrito'] = carrito
|
||||
request.session.modified = True
|
||||
|
||||
# Descripción del producto a partir de los datos validados
|
||||
product_description = ", ".join([f"{it['nombre']} x{it['cantidad']}" for it in carrito])
|
||||
|
||||
# Obtener detalles del usuario
|
||||
with connections['acore_auth'].cursor() as cursor:
|
||||
@@ -2266,6 +2331,7 @@ def store_novawow_view(request):
|
||||
|
||||
from collections import Counter
|
||||
|
||||
@require_paid_stripe(redirect_to='store-novawow')
|
||||
def store_novawow_success_view(request):
|
||||
# Procesar el comando SOAP tras el pago exitoso
|
||||
character_name = request.session.get('store_novawow')
|
||||
@@ -2350,13 +2416,17 @@ def revive_character_view(request):
|
||||
command = f".revive {character_name}"
|
||||
response = execute_soap_command(command)
|
||||
|
||||
# Si la respuesta es vacía o None, asumir éxito
|
||||
if response:
|
||||
# None = fallo de comunicación con el servidor
|
||||
if response is None:
|
||||
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Error de comunicación con el servidor. Intente nuevamente más tarde.</span>'})
|
||||
|
||||
# Respuesta vacía = éxito (el comando .revive no devuelve texto si va bien)
|
||||
if response.strip() == "":
|
||||
ReviveHistory.objects.create(character_name=character_name, used_at=now())
|
||||
return JsonResponse({'success': True, 'message': f'<span class="ok-form-response">El personaje {character_name} ha sido revivido con éxito.</span>'})
|
||||
|
||||
if response is None:
|
||||
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Error de comunicación con el servidor. Intente nuevamente más tarde.</span>'})
|
||||
# Respuesta no vacía = el servidor devolvió un mensaje/error
|
||||
return JsonResponse({'success': False, 'message': f'<span class="red-form-response">{response}</span>'})
|
||||
|
||||
return render(request, 'account/revive_character.html', {'characters': characters.keys()})
|
||||
|
||||
@@ -2516,6 +2586,7 @@ def rename_guild_view(request):
|
||||
})
|
||||
|
||||
|
||||
@require_paid_stripe()
|
||||
def guild_rename_success_view(request):
|
||||
# Obtener los datos de la sesión
|
||||
guild_rename_data = request.session.get('guild_rename')
|
||||
|
||||
Reference in New Issue
Block a user