cac0d28aa9
- Backend: helper verify_turnstile en _base (verifica el token contra challenges.cloudflare.com/siteverify; fail-closed ante error de red; si no hay secreto configurado, no bloquea). Se exige en el POST de login_view, register_view y recover_account_view -> rechazo con mensaje si falla. - settings: TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY desde .env (el secreto NO se commitea). La clave de sitio se expone a las plantillas por el context processor (TURNSTILE_SITE_KEY). - Frontend: hook useTurnstile (carga el script de Cloudflare una vez y renderiza el widget, devuelve el token). LoginForm/RegisterForm/RecoverForm incluyen el widget, mandan cf-turnstile-response y deshabilitan el submit hasta tener token. El sitekey llega por data-sitekey en el div de montaje. Verificado: sin token los 3 POST se rechazan; siteverify acepta la clave secreta (error invalid-input-response con token falso, no invalid-input-secret). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
3.9 KiB
Python
102 lines
3.9 KiB
Python
"""Imports y utilidades compartidas por el paquete de vistas (`home.views`).
|
|
|
|
Cada submódulo de vistas hace `from ._base import *` para heredar estos imports
|
|
y el decorador `require_paid_stripe`.
|
|
"""
|
|
import socket, hashlib, gmpy2, binascii, os, re, json, logging, secrets
|
|
from collections import Counter
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
from functools import wraps
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth import logout
|
|
from django.db import connections
|
|
from django.http import HttpResponse, JsonResponse
|
|
from django.shortcuts import render, redirect
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.crypto import get_random_string
|
|
from django.utils.timezone import now
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
import stripe
|
|
|
|
from ..ac_soap import execute_soap_command
|
|
from ..library_correo import enviar_correo
|
|
from ..pagos_stripe import create_checkout_session, is_session_paid
|
|
from ..pagos_sumup import crear_checkout, crear_checkout_battlepay, obtener_checkout
|
|
from ..zone_definitions import get_zone_name
|
|
from .. import bnet
|
|
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,
|
|
PasswordReset,
|
|
)
|
|
|
|
# Configuración del logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def verify_turnstile(request):
|
|
"""Verifica el token de Cloudflare Turnstile enviado por el formulario.
|
|
|
|
Devuelve True si no hay secreto configurado (captcha desactivado) o si Cloudflare
|
|
confirma el token. Ante fallo de red devuelve False (fail-closed).
|
|
"""
|
|
import json
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
secret = settings.TURNSTILE_SECRET_KEY
|
|
if not secret:
|
|
return True
|
|
token = request.POST.get('cf-turnstile-response', '').strip()
|
|
if not token:
|
|
return False
|
|
payload = urllib.parse.urlencode({
|
|
'secret': secret,
|
|
'response': token,
|
|
'remoteip': request.META.get('REMOTE_ADDR', ''),
|
|
}).encode()
|
|
try:
|
|
req = urllib.request.Request(
|
|
'https://challenges.cloudflare.com/turnstile/v0/siteverify', data=payload)
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
result = json.loads(resp.read().decode())
|
|
return bool(result.get('success'))
|
|
except Exception as e:
|
|
logger.warning("Turnstile: fallo al verificar: %s", e)
|
|
return False
|
|
|
|
|
|
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
|