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>
720 lines
33 KiB
Python
720 lines
33 KiB
Python
from ._base import *
|
|
|
|
|
|
class LoginForm(forms.Form):
|
|
email = forms.EmailField(max_length=320, required=True)
|
|
password = forms.CharField(widget=forms.PasswordInput, required=True)
|
|
def sha1(data):
|
|
"""Función auxiliar para calcular SHA-1."""
|
|
return hashlib.sha1(data).digest()
|
|
def calculate_srp6_verifier(username, password, salt):
|
|
"""Calcula el verifier usando SRP6 basado en la implementación PHP."""
|
|
g = gmpy2.mpz(7)
|
|
N = gmpy2.mpz('894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7', 16)
|
|
|
|
# Paso 1: Calcular H(username:password)
|
|
h1 = sha1((username.upper() + ':' + password.upper()).encode('utf-8'))
|
|
|
|
# Usar el salt proporcionado (sin invertir)
|
|
salt_bytes = bytes.fromhex(salt)
|
|
|
|
# Paso 2: Calcular H(salt + H(username:password))
|
|
h2 = sha1(salt_bytes + h1)
|
|
|
|
# Convertir el hash a un entero (little-endian)
|
|
h2_int = gmpy2.mpz.from_bytes(h2, 'little')
|
|
|
|
# Calcular el verifier usando g^h2 mod N
|
|
verifier = gmpy2.powmod(g, h2_int, N)
|
|
verifier_bytes = verifier.to_bytes(32, 'little')
|
|
|
|
# Convertir el resultado a hexadecimal en mayúsculas
|
|
return binascii.hexlify(verifier_bytes).decode('utf-8').upper()
|
|
def authenticate(email, password):
|
|
"""Autentica una cuenta Battle.net por email (SRP6 v2)."""
|
|
try:
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
account = bnet.get_bnet_account_by_email(cursor, email)
|
|
if not account:
|
|
return None
|
|
if bnet.bnet_verify(account['email'], password, account['salt'], account['verifier']):
|
|
return account
|
|
return None
|
|
except Exception:
|
|
return None
|
|
def _set_game_account_session(request, game_account):
|
|
"""Fija en sesión la cuenta de juego seleccionada (para las demás vistas)."""
|
|
request.session['username'] = game_account['username'].upper()
|
|
request.session['account_id'] = game_account['id']
|
|
def login_view(request):
|
|
if request.method == 'POST':
|
|
if not verify_turnstile(request):
|
|
return JsonResponse({'success': False, 'error': 'Verificación anti-bots fallida. Reintenta.'})
|
|
form = LoginForm(request.POST)
|
|
if form.is_valid():
|
|
email = form.cleaned_data['email']
|
|
password = form.cleaned_data['password']
|
|
ip_address = request.META.get('REMOTE_ADDR')
|
|
|
|
account = authenticate(email, password)
|
|
if account:
|
|
log_login_attempt(email, "Exito", ip_address)
|
|
|
|
# Guardar la identidad Battle.net en la sesión
|
|
request.session['bnet_id'] = account['id']
|
|
request.session['bnet_email'] = account['email']
|
|
|
|
# Resolver las cuentas de juego enlazadas
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
game_accounts = bnet.get_game_accounts_for_bnet(cursor, account['id'])
|
|
|
|
if len(game_accounts) == 1:
|
|
# Una sola cuenta de juego: seleccionar automáticamente
|
|
_set_game_account_session(request, game_accounts[0])
|
|
elif len(game_accounts) > 1:
|
|
# Varias: dejar la selección pendiente (my-account redirige al selector)
|
|
request.session.pop('username', None)
|
|
request.session.pop('account_id', None)
|
|
# 0 cuentas: se queda sin username; el selector mostrará el aviso
|
|
|
|
return JsonResponse({'success': True})
|
|
else:
|
|
log_login_attempt(email, "Fracaso", ip_address)
|
|
return JsonResponse({'success': False, 'error': "Correo o contraseña incorrectos"})
|
|
else:
|
|
return JsonResponse({'success': False, 'error': "Formulario no valido"})
|
|
|
|
form = LoginForm()
|
|
return render(request, 'auth/login.html', {'form': form})
|
|
def select_account_view(request):
|
|
"""Selector de cuenta de juego para cuentas Battle.net con varias."""
|
|
bnet_id = request.session.get('bnet_id')
|
|
if not bnet_id:
|
|
return redirect('login')
|
|
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
game_accounts = bnet.get_game_accounts_for_bnet(cursor, bnet_id)
|
|
|
|
# La selección la hace la isla React vía GET (/api/account/select/), sin POST.
|
|
|
|
# Autoseleccionar si solo hay una
|
|
if len(game_accounts) == 1:
|
|
_set_game_account_session(request, game_accounts[0])
|
|
return redirect('my-account')
|
|
|
|
return render(request, 'auth/select_account.html', {
|
|
'game_accounts': game_accounts,
|
|
'bnet_email': request.session.get('bnet_email'),
|
|
})
|
|
def logout_view(request):
|
|
for key in ('username', 'account_id', 'bnet_id', 'bnet_email'):
|
|
request.session.pop(key, None)
|
|
return redirect('index')
|
|
def register_view(request):
|
|
if request.method == 'POST':
|
|
if not verify_turnstile(request):
|
|
return JsonResponse({'success': False, 'message': 'Verificación anti-bots fallida. Reintenta.'})
|
|
password = request.POST.get('password', '').strip()
|
|
conf_password = request.POST.get('conf-password', '').strip()
|
|
email = request.POST.get('email', '').strip()
|
|
conf_email = request.POST.get('conf-email', '').strip()
|
|
recruiter = request.POST.get('recruiter', '').strip()
|
|
|
|
# En 3.4.3 la identidad es el email (cuenta Battle.net); no hay username.
|
|
if not password or not email:
|
|
return JsonResponse({'success': False, 'message': 'Por favor, complete todos los campos.'})
|
|
|
|
if password != conf_password:
|
|
return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'})
|
|
|
|
if len(password) > 16:
|
|
return JsonResponse({'success': False, 'message': 'La contraseña no debe exceder los 16 caracteres.'})
|
|
|
|
if email != conf_email or not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', email):
|
|
return JsonResponse({'success': False, 'message': 'El correo electrónico no es válido.'})
|
|
|
|
# Verificar si ya existe una cuenta Battle.net con ese email
|
|
email_norm = bnet.normalize_email(email)
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
cursor.execute("SELECT COUNT(*) FROM battlenet_accounts WHERE email = %s", [email_norm])
|
|
if cursor.fetchone()[0] > 0:
|
|
return JsonResponse({'success': False, 'message': 'Ya existe una cuenta con ese correo electrónico.'})
|
|
|
|
# Comprobar que no haya un registro de activación pendiente para ese email
|
|
if AccountActivation.objects.filter(email=email, old_email__isnull=True).exists():
|
|
AccountActivation.objects.filter(email=email, old_email__isnull=True).delete()
|
|
|
|
# Validar y obtener recruiter_id (id de cuenta de juego del reclutador)
|
|
recruiter_id = 0
|
|
if recruiter:
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
cursor.execute("SELECT id FROM account WHERE username = %s", [recruiter])
|
|
recruiter_data = cursor.fetchone()
|
|
if recruiter_data:
|
|
recruiter_id = recruiter_data[0]
|
|
else:
|
|
with connections['acore_characters'].cursor() as char_cursor:
|
|
char_cursor.execute("SELECT account FROM characters WHERE name = %s", [recruiter])
|
|
character_data = char_cursor.fetchone()
|
|
if character_data:
|
|
recruiter_id = character_data[0]
|
|
else:
|
|
return JsonResponse({'success': False, 'message': 'El reclutador ingresado no existe.'})
|
|
|
|
# Generar un hash único para activación. Los salt/verifier se calculan al
|
|
# activar (tanto el de bnet como el de la cuenta de juego).
|
|
activation_hash = get_random_string(32)
|
|
|
|
AccountActivation.objects.create(
|
|
email=email,
|
|
password=password,
|
|
recruiter_id=recruiter_id,
|
|
hash=activation_hash
|
|
)
|
|
|
|
# Enviar correo de activación
|
|
activation_link = f"{settings.URL_PRINCIPAL}/es/activate-account?act={activation_hash}"
|
|
context = {
|
|
'username': email,
|
|
'password': password,
|
|
'activation_link': activation_link,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR
|
|
}
|
|
enviar_correo(
|
|
subject=f'Activación de la cuenta {email} - {settings.NOMBRE_SERVIDOR}',
|
|
to_email=email,
|
|
template='emails/activation.html',
|
|
context=context
|
|
)
|
|
|
|
message = f"""
|
|
<div class="alert-message" id="create-response" style="display: block;">
|
|
<span class="ok-form-response">La cuenta '{email}' ha sido creada.</span><br><br>
|
|
<span>Se ha enviado un enlace de activación al correo {email}.</span>
|
|
</div>
|
|
"""
|
|
return JsonResponse({'success': True, 'message': message})
|
|
|
|
return render(request, 'auth/register.html')
|
|
def activate_account_view(request):
|
|
activation_hash = request.GET.get('act')
|
|
|
|
try:
|
|
activation = AccountActivation.objects.get(hash=activation_hash)
|
|
|
|
if activation.is_expired():
|
|
return render(request, 'auth/activation_invalid.html')
|
|
|
|
email_norm = bnet.normalize_email(activation.email)
|
|
password = activation.password
|
|
last_ip = request.META.get('REMOTE_ADDR')
|
|
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
# Evitar duplicados si el enlace se usa dos veces
|
|
cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm])
|
|
existing = cursor.fetchone()
|
|
if existing:
|
|
activation.delete()
|
|
return render(request, 'auth/activation_success.html')
|
|
|
|
# 1) Crear la cuenta Battle.net (SRP6 v2)
|
|
bnet_salt, bnet_verifier, srp_version = bnet.bnet_make_registration(email_norm, password)
|
|
cursor.execute(
|
|
"INSERT INTO battlenet_accounts (email, srp_version, salt, verifier) "
|
|
"VALUES (%s, %s, %s, %s)",
|
|
[email_norm, srp_version, bnet_salt, bnet_verifier]
|
|
)
|
|
|
|
# 2) Recuperar el id de la cuenta bnet recién creada
|
|
cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [email_norm])
|
|
bnet_id = cursor.fetchone()[0]
|
|
|
|
# 3) Crear la cuenta de juego enlazada (<bnetId>#1, SRP6 Grunt)
|
|
game_username = bnet.make_game_account_username(bnet_id, 1)
|
|
game_salt, game_verifier = bnet.game_make_registration(game_username, password)
|
|
cursor.execute(
|
|
"INSERT INTO account "
|
|
"(username, salt, verifier, email, reg_mail, recruiter, joindate, "
|
|
" last_ip, expansion, battlenet_account, battlenet_index) "
|
|
"VALUES (%s, %s, %s, %s, %s, %s, NOW(), %s, %s, %s, %s)",
|
|
[
|
|
game_username, game_salt, game_verifier,
|
|
email_norm, email_norm, activation.recruiter_id or 0,
|
|
last_ip, 2, bnet_id, 1
|
|
]
|
|
)
|
|
|
|
activation.delete()
|
|
return render(request, 'auth/activation_success.html')
|
|
|
|
except AccountActivation.DoesNotExist:
|
|
return render(request, 'auth/activation_invalid.html')
|
|
def recover_account_view(request):
|
|
"""Recuperación de cuenta (Battle.net, por email). 3 tipos:
|
|
password (enlace de reset), accountname (lista de cuentas), activation (reenvío).
|
|
Respuestas genéricas para no revelar si el correo existe (anti-enumeración).
|
|
"""
|
|
if request.method == 'POST':
|
|
if not verify_turnstile(request):
|
|
return JsonResponse({'success': False, 'message': 'Verificación anti-bots fallida. Reintenta.'})
|
|
rtype = request.POST.get('type', '').strip()
|
|
email = request.POST.get('email', '').strip()
|
|
if not email or not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', email):
|
|
return JsonResponse({'success': False, 'message': 'Introduce un correo de Gmail válido.'})
|
|
|
|
if rtype == 'password':
|
|
try:
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
account = bnet.get_bnet_account_by_email(cursor, email)
|
|
except Exception as e:
|
|
logger.warning("recover(password): BD de cuentas no disponible: %s", e)
|
|
account = None
|
|
if account:
|
|
token = get_random_string(64)
|
|
PasswordReset.objects.create(email=email, token=token)
|
|
reset_link = f"{settings.URL_PRINCIPAL}/es/reset-password?token={token}"
|
|
enviar_correo(
|
|
subject=f'Restablecer contraseña - {settings.NOMBRE_SERVIDOR}',
|
|
to_email=email,
|
|
template='emails/password_reset.html',
|
|
context={'reset_link': reset_link, 'email': email,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR,
|
|
'URL_PRINCIPAL': settings.URL_PRINCIPAL},
|
|
)
|
|
return JsonResponse({'success': True, 'message': 'Si existe una cuenta con ese correo, te hemos enviado un enlace para restablecer la contraseña.'})
|
|
|
|
if rtype == 'accountname':
|
|
try:
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
account = bnet.get_bnet_account_by_email(cursor, email)
|
|
game_accounts = bnet.get_game_accounts_for_bnet(cursor, account['id']) if account else []
|
|
except Exception as e:
|
|
logger.warning("recover(accountname): BD de cuentas no disponible: %s", e)
|
|
account, game_accounts = None, []
|
|
if account:
|
|
enviar_correo(
|
|
subject=f'Tus cuentas de juego - {settings.NOMBRE_SERVIDOR}',
|
|
to_email=email,
|
|
template='emails/account_names.html',
|
|
context={'email': email, 'accounts': game_accounts,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR,
|
|
'URL_PRINCIPAL': settings.URL_PRINCIPAL},
|
|
)
|
|
return JsonResponse({'success': True, 'message': 'Si existe una cuenta con ese correo, te hemos enviado la lista de cuentas asociadas.'})
|
|
|
|
if rtype == 'activation':
|
|
activation = AccountActivation.objects.filter(
|
|
email=email, old_email__isnull=True).order_by('-created_at').first()
|
|
if activation:
|
|
activation_link = f"{settings.URL_PRINCIPAL}/es/activate-account?act={activation.hash}"
|
|
enviar_correo(
|
|
subject=f'Activación de la cuenta {email} - {settings.NOMBRE_SERVIDOR}',
|
|
to_email=email,
|
|
template='emails/activation.html',
|
|
context={'username': email, 'password': activation.password,
|
|
'activation_link': activation_link,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR},
|
|
)
|
|
return JsonResponse({'success': True, 'message': 'Si hay una activación pendiente para ese correo, te hemos reenviado el enlace.'})
|
|
|
|
return JsonResponse({'success': False, 'message': 'Opción no válida.'})
|
|
|
|
return render(request, 'auth/recover_account.html')
|
|
|
|
|
|
def reset_password_view(request):
|
|
"""Fija una nueva contraseña a partir de un token de PasswordReset (por email).
|
|
Re-deriva el verifier SRP6 v2 de la cuenta Battle.net (igual que change_password)."""
|
|
if request.method == 'POST':
|
|
token = request.POST.get('token', '').strip()
|
|
new_password = request.POST.get('new-password', '').strip()
|
|
conf_password = request.POST.get('conf-password', '').strip()
|
|
|
|
pr = PasswordReset.objects.filter(token=token, used=False).first()
|
|
if not pr or pr.is_expired():
|
|
return JsonResponse({'success': False, 'message': 'El enlace no es válido o ha caducado.'})
|
|
if not new_password or new_password != conf_password:
|
|
return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'})
|
|
if len(new_password) > 16:
|
|
return JsonResponse({'success': False, 'message': 'La contraseña no debe exceder los 16 caracteres.'})
|
|
|
|
new_salt, new_verifier, srp_version = bnet.bnet_make_registration(pr.email, new_password)
|
|
try:
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
account = bnet.get_bnet_account_by_email(cursor, pr.email)
|
|
if not account:
|
|
return JsonResponse({'success': False, 'message': 'No se ha encontrado la cuenta.'})
|
|
cursor.execute(
|
|
"UPDATE battlenet_accounts SET srp_version = %s, salt = %s, verifier = %s WHERE id = %s",
|
|
[srp_version, new_salt, new_verifier, account['id']]
|
|
)
|
|
except Exception as e:
|
|
logger.error("reset_password: error al actualizar la cuenta: %s", e)
|
|
return JsonResponse({'success': False, 'message': 'No se ha podido actualizar la contraseña. Inténtalo más tarde.'})
|
|
pr.used = True
|
|
pr.save(update_fields=['used'])
|
|
return JsonResponse({'success': True,
|
|
'message': 'Contraseña restablecida. Ya puedes iniciar sesión.',
|
|
'redirect': reverse('login')})
|
|
|
|
token = request.GET.get('token', '').strip()
|
|
pr = PasswordReset.objects.filter(token=token, used=False).first()
|
|
valid = bool(pr and not pr.is_expired())
|
|
return render(request, 'auth/reset_password.html', {'token': token, 'valid': valid})
|
|
def change_password_view(request):
|
|
# Verificar si el usuario está autenticado mediante la sesión
|
|
username = request.session.get('username')
|
|
if not username:
|
|
return redirect('login')
|
|
|
|
# Obtener el usuario desde la base de datos de `acore_auth`
|
|
user_data = get_user_from_acore(username)
|
|
if not user_data:
|
|
return redirect('login')
|
|
|
|
user_id = user_data['id']
|
|
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()
|
|
|
|
# Validar campos vacíos
|
|
if not current_password or not new_password or not conf_new_password or not security_token:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Por favor, complete todos los campos.</span>'})
|
|
|
|
# Validar que la nueva contraseña coincida con su confirmación
|
|
if new_password != conf_new_password:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Las contraseñas no coinciden.</span>'})
|
|
|
|
# Validar longitud de la nueva contraseña
|
|
if len(new_password) > 16:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">La contraseña nueva no debe exceder los 16 caracteres.</span>'})
|
|
|
|
# Verificar si el token de seguridad ha sido generado
|
|
existing_token = SecurityToken.objects.filter(user_id=user_id).first()
|
|
if not existing_token:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">No tienes un token de seguridad generado. Por favor, genera uno antes de cambiar tu contraseña.</span>'})
|
|
|
|
# Verificar si el token proporcionado es correcto
|
|
if existing_token.token != security_token:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">El token ingresado es incorrecto.</span>'})
|
|
|
|
# Verificar la contraseña actual contra la cuenta Battle.net (por email)
|
|
if not authenticate(email, current_password):
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">La contraseña actual es incorrecta.</span>'})
|
|
|
|
# Generar salt y verifier SRP6 v2 para la nueva contraseña (bnet)
|
|
new_salt_bytes, new_verifier_bytes, srp_version = bnet.bnet_make_registration(email, new_password)
|
|
|
|
# Localizar la cuenta bnet (por sesión o por email) y actualizarla
|
|
bnet_id = request.session.get('bnet_id')
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
if not bnet_id:
|
|
account = bnet.get_bnet_account_by_email(cursor, email)
|
|
bnet_id = account['id'] if account else None
|
|
if not bnet_id:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">No se ha encontrado la cuenta.</span>'})
|
|
cursor.execute(
|
|
"UPDATE battlenet_accounts SET srp_version = %s, salt = %s, verifier = %s WHERE id = %s",
|
|
[srp_version, new_salt_bytes, new_verifier_bytes, bnet_id]
|
|
)
|
|
|
|
# Cerrar la sesión por seguridad
|
|
for key in ('username', 'account_id', 'bnet_id', 'bnet_email'):
|
|
request.session.pop(key, None)
|
|
|
|
# Redirigir al usuario a la página de inicio de sesión
|
|
return JsonResponse({
|
|
'success': True,
|
|
'message': '<span class="ok-form-response">Contraseña cambiada exitosamente. Has sido desconectado por seguridad.</span>',
|
|
'redirect': True
|
|
})
|
|
|
|
return render(request, 'account/change_password.html')
|
|
def get_user_from_acore(username):
|
|
"""
|
|
Obtiene la información del usuario desde la base de datos `acore_auth`.
|
|
"""
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
cursor.execute("SELECT id, email FROM account WHERE username = %s", [username])
|
|
result = cursor.fetchone()
|
|
if result:
|
|
return {'id': result[0], 'email': result[1]}
|
|
return None
|
|
def security_token_view(request):
|
|
# Verificar si el usuario está autenticado mediante la sesión
|
|
username = request.session.get('username')
|
|
if not username:
|
|
return redirect('index')
|
|
|
|
# Obtener el usuario desde la base de datos de `acore_auth`
|
|
user_data = get_user_from_acore(username)
|
|
if not user_data:
|
|
return redirect('index')
|
|
|
|
user_id = user_data['id']
|
|
email = user_data['email']
|
|
ip_address = request.META.get('REMOTE_ADDR')
|
|
|
|
# Verificar si existe un token activo para mostrar la fecha en la plantilla
|
|
existing_token = SecurityToken.objects.filter(user_id=user_id).first()
|
|
|
|
# Obtener la fecha del token o "Sin solicitar" si no hay token
|
|
token_date = "Sin solicitar"
|
|
if existing_token:
|
|
token_date = existing_token.created_at.strftime('%H:%M:%S %d-%m-%Y')
|
|
|
|
if request.method == 'POST':
|
|
# Verificar si el correo del usuario está vacío
|
|
if not email:
|
|
# Si no tiene correo, devolver un mensaje de error en formato JSON
|
|
return JsonResponse({
|
|
'success': False,
|
|
'message': f'<span class="red-form-response">Debes añadir un correo electrónico a tu cuenta antes de solicitar un Token de seguridad.</span>'
|
|
})
|
|
# Verificar si ya existe un token y si se puede generar uno nuevo después de 7 días
|
|
if existing_token:
|
|
# Comprobar si han pasado al menos 7 días desde la creación del token
|
|
days_since_creation = (timezone.now() - existing_token.created_at).days
|
|
if days_since_creation < 7:
|
|
remaining_days = 7 - days_since_creation
|
|
return JsonResponse({
|
|
'success': False,
|
|
'message': f'<span class="red-form-response">Sólo puedes solicitar un nuevo Token de seguridad cada 7 días, faltan {remaining_days} días para generar un nuevo Token.</span>'
|
|
})
|
|
|
|
# Generar un nuevo token
|
|
token = secrets.token_urlsafe(4)[:6]
|
|
expires_at = timezone.now() + timedelta(days=7)
|
|
|
|
# Eliminar el token anterior y crear uno nuevo
|
|
if existing_token:
|
|
existing_token.delete()
|
|
|
|
# Crear y guardar el nuevo token
|
|
new_token = SecurityToken.objects.create(
|
|
user_id=user_id,
|
|
token=token,
|
|
ip_address=ip_address,
|
|
expires_at=expires_at # Esta fecha solo se usa para el control de los 7 días
|
|
)
|
|
|
|
# Enviar el correo con el token al usuario
|
|
send_security_token_email(email, username, token, ip_address)
|
|
|
|
# Obtener la nueva fecha del token
|
|
new_token_date = new_token.created_at.strftime('%H:%M:%S %d-%m-%Y')
|
|
|
|
# Responder con un mensaje exitoso y la nueva fecha
|
|
return JsonResponse({
|
|
'success': True,
|
|
'message': f'<span class="ok-form-response">Se ha enviado el Token de seguridad al correo {email}.</span>',
|
|
'token_date': new_token_date
|
|
})
|
|
|
|
# Pasar `token_date` al contexto de la plantilla
|
|
return render(request, 'account/security_token.html', {
|
|
'token_date': token_date
|
|
})
|
|
def send_security_token_email(email, username, token, ip_address):
|
|
"""
|
|
Envía un correo electrónico con el token de seguridad.
|
|
"""
|
|
context = {
|
|
'username': username,
|
|
'token': token,
|
|
'ip_address': ip_address,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR
|
|
}
|
|
|
|
# Llamada a tu función personalizada `enviar_correo`
|
|
enviar_correo(
|
|
subject=f'Token de seguridad de la cuenta {username} - {settings.NOMBRE_SERVIDOR}',
|
|
to_email=email,
|
|
template='emails/security_token.html', # Asegúrate de que este template exista
|
|
context=context
|
|
)
|
|
def change_email_view(request):
|
|
username = request.session.get('username')
|
|
if not username:
|
|
return redirect('login')
|
|
|
|
# Obtener el usuario desde la base de datos de `acore_auth`
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
cursor.execute("""
|
|
SELECT id, email, reg_mail
|
|
FROM account
|
|
WHERE username = %s
|
|
""", [username])
|
|
account_data = cursor.fetchone()
|
|
|
|
if not account_data:
|
|
return redirect('login')
|
|
|
|
user_id, current_email, reg_email = account_data
|
|
security_token = SecurityToken.objects.filter(user_id=user_id).first()
|
|
|
|
if request.method == 'POST':
|
|
print("Datos recibidos:", request.POST)
|
|
|
|
current_password = request.POST.get('cur-password', '').strip()
|
|
current_email_input = request.POST.get('cur-email', '').strip()
|
|
new_email = request.POST.get('new-email', '').strip()
|
|
conf_new_email = request.POST.get('conf-new-email', '').strip()
|
|
token = request.POST.get('security-token', '').strip()
|
|
|
|
# Validar los campos
|
|
if not all([current_password, current_email_input, new_email, conf_new_email, token]):
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Por favor, complete todos los campos.</span>'})
|
|
|
|
if current_email_input != current_email:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">El correo actual no es correcto.</span>'})
|
|
|
|
if new_email != conf_new_email:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Los correos no coinciden.</span>'})
|
|
|
|
if not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', new_email):
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">El nuevo correo no es válido.</span>'})
|
|
|
|
if not security_token or security_token.token != token:
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Token de seguridad incorrecto.</span>'})
|
|
|
|
# Verificar la contraseña contra la cuenta Battle.net (por email actual)
|
|
if not authenticate(current_email, current_password):
|
|
return JsonResponse({'success': False, 'message': '<span class="red-form-response">Contraseña incorrecta.</span>'})
|
|
|
|
# Generar hashes
|
|
old_email_hash = secrets.token_urlsafe(16)
|
|
new_email_hash = secrets.token_urlsafe(16)
|
|
|
|
# Guardar en `AccountActivation`. Se conserva la contraseña porque al
|
|
# cambiar el email hay que recalcular el verifier de bnet (el usuario SRP
|
|
# depende del email).
|
|
AccountActivation.objects.create(
|
|
username=username,
|
|
email=new_email,
|
|
old_email=current_email,
|
|
password=current_password,
|
|
recruiter_id=user_id,
|
|
hash=new_email_hash,
|
|
old_email_hash=old_email_hash
|
|
)
|
|
|
|
# Enviar confirmación al correo actual
|
|
confirm_old_email_link = f"{settings.URL_PRINCIPAL}/es/confirm-old-email?hash={old_email_hash}"
|
|
context_old_email = {
|
|
'username': username,
|
|
'confirm_link': confirm_old_email_link,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR
|
|
}
|
|
enviar_correo(
|
|
subject=f'Confirme el cambio de correo para {username}',
|
|
to_email=current_email,
|
|
template='emails/confirm_old_email.html',
|
|
context=context_old_email
|
|
)
|
|
|
|
return JsonResponse({'success': True, 'message': '<span class="ok-form-response">Se ha enviado un correo de confirmación al correo actual.</span>'})
|
|
|
|
return render(request, 'account/change_email.html')
|
|
def confirm_old_email_view(request):
|
|
hash = request.GET.get('hash')
|
|
activation = AccountActivation.objects.filter(old_email_hash=hash, is_used=False).first()
|
|
|
|
if not activation or activation.is_expired():
|
|
return JsonResponse({
|
|
'success': False,
|
|
'message': '<span class="red-form-response">Enlace no válido o expirado.</span>',
|
|
'redirect_url': reverse('expired-link')
|
|
})
|
|
|
|
# Marcar solo el old_email_hash como usado
|
|
activation.is_used = True
|
|
activation.save()
|
|
|
|
# Enviar correo al nuevo correo para confirmar el cambio
|
|
activation_link = f"{settings.URL_PRINCIPAL}/es/confirm-new-email?hash={activation.hash}"
|
|
context_new_email = {
|
|
'username': activation.username,
|
|
'activation_link': activation_link,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR
|
|
}
|
|
enviar_correo(
|
|
subject=f'Confirme su nuevo correo para {activation.username}',
|
|
to_email=activation.email,
|
|
template='emails/confirm_new_email.html',
|
|
context=context_new_email
|
|
)
|
|
|
|
return JsonResponse({'success': True, 'message': '<span class="ok-form-response">Correo confirmado. Se ha enviado un enlace al nuevo correo.</span>'})
|
|
def confirm_new_email_view(request):
|
|
hash = request.GET.get('hash')
|
|
activation = AccountActivation.objects.filter(hash=hash, is_new_email_used=False).first()
|
|
|
|
if not activation or activation.is_expired():
|
|
return JsonResponse({
|
|
'success': False,
|
|
'message': '<span class="red-form-response">Enlace no válido o expirado.</span>',
|
|
'redirect_url': reverse('expired-link')
|
|
})
|
|
|
|
# Marcar solo el new_email_hash como usado
|
|
activation.is_new_email_used = True
|
|
activation.save()
|
|
|
|
# Cambiar el email en la cuenta Battle.net implica recalcular salt/verifier,
|
|
# porque el usuario SRP = SHA256(UPPER(email)).
|
|
new_email_norm = bnet.normalize_email(activation.email)
|
|
old_email_norm = bnet.normalize_email(activation.old_email or '')
|
|
new_salt, new_verifier, srp_version = bnet.bnet_make_registration(new_email_norm, activation.password)
|
|
|
|
with connections['acore_auth'].cursor() as cursor:
|
|
# Localizar la cuenta bnet por su email actual (el antiguo)
|
|
cursor.execute("SELECT id FROM battlenet_accounts WHERE email = %s", [old_email_norm])
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
return JsonResponse({
|
|
'success': False,
|
|
'message': '<span class="red-form-response">No se ha encontrado la cuenta Battle.net.</span>',
|
|
})
|
|
bnet_id = row[0]
|
|
|
|
# Actualizar email + verifier en battlenet_accounts
|
|
cursor.execute(
|
|
"UPDATE battlenet_accounts SET email = %s, srp_version = %s, salt = %s, verifier = %s WHERE id = %s",
|
|
[new_email_norm, srp_version, new_salt, new_verifier, bnet_id]
|
|
)
|
|
# Actualizar el email de las cuentas de juego enlazadas (cosmético)
|
|
cursor.execute(
|
|
"UPDATE account SET email = %s, reg_mail = %s WHERE battlenet_account = %s",
|
|
[new_email_norm, new_email_norm, bnet_id]
|
|
)
|
|
|
|
# Enviar notificación al correo anterior
|
|
context_old_email = {
|
|
'username': activation.username,
|
|
'new_email': activation.email,
|
|
'NOMBRE_SERVIDOR': settings.NOMBRE_SERVIDOR
|
|
}
|
|
enviar_correo(
|
|
subject=f'Su correo ha sido cambiado en {settings.NOMBRE_SERVIDOR}',
|
|
to_email=activation.old_email,
|
|
template='emails/old_email_notification.html',
|
|
context=context_old_email
|
|
)
|
|
|
|
return JsonResponse({'success': True, 'message': '<span class="ok-form-response">El cambio de correo ha sido confirmado y completado con éxito.</span>'})
|
|
def expired_link_view(request):
|
|
return render(request, 'account/expired_link.html')
|
|
def log_login_attempt(username, status, ip_address):
|
|
# Registrar intento de inicio de sesión en el historial
|
|
LoginAttempt.objects.create(
|
|
username=username,
|
|
status=status,
|
|
ip_address=ip_address,
|
|
timestamp=timezone.now()
|
|
)
|