Files
NightSpire/home/views/_base.py
T
Inna cac0d28aa9 Añade Cloudflare Turnstile (captcha) a login, registro y recuperación
- 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>
2026-07-12 21:15:27 +00:00

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