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"""
La cuenta '{email}' ha sido creada.

Se ha enviado un enlace de activación al correo {email}.
""" 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 (#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': 'Por favor, complete todos los campos.'}) # Validar que la nueva contraseña coincida con su confirmación if new_password != conf_new_password: return JsonResponse({'success': False, 'message': 'Las contraseñas no coinciden.'}) # Validar longitud de la nueva contraseña if len(new_password) > 16: return JsonResponse({'success': False, 'message': 'La contraseña nueva no debe exceder los 16 caracteres.'}) # 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': 'No tienes un token de seguridad generado. Por favor, genera uno antes de cambiar tu contraseña.'}) # Verificar si el token proporcionado es correcto if existing_token.token != security_token: return JsonResponse({'success': False, 'message': 'El token ingresado es incorrecto.'}) # Verificar la contraseña actual contra la cuenta Battle.net (por email) if not authenticate(email, current_password): return JsonResponse({'success': False, 'message': 'La contraseña actual es incorrecta.'}) # 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': '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_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': 'Contraseña cambiada exitosamente. Has sido desconectado por seguridad.', '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'Debes añadir un correo electrónico a tu cuenta antes de solicitar un Token de seguridad.' }) # 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'Sólo puedes solicitar un nuevo Token de seguridad cada 7 días, faltan {remaining_days} días para generar un nuevo Token.' }) # 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'Se ha enviado el Token de seguridad al correo {email}.', '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': 'Por favor, complete todos los campos.'}) if current_email_input != current_email: return JsonResponse({'success': False, 'message': 'El correo actual no es correcto.'}) if new_email != conf_new_email: return JsonResponse({'success': False, 'message': 'Los correos no coinciden.'}) if not re.match(r'^[a-zA-Z0-9._%+-]+@gmail\.com$', new_email): return JsonResponse({'success': False, 'message': 'El nuevo correo no es válido.'}) if not security_token or security_token.token != token: return JsonResponse({'success': False, 'message': 'Token de seguridad incorrecto.'}) # Verificar la contraseña contra la cuenta Battle.net (por email actual) if not authenticate(current_email, current_password): return JsonResponse({'success': False, 'message': 'Contraseña incorrecta.'}) # 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': 'Se ha enviado un correo de confirmación al correo actual.'}) 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': 'Enlace no válido o expirado.', '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': 'Correo confirmado. Se ha enviado un enlace al nuevo correo.'}) 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': 'Enlace no válido o expirado.', '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': 'No se ha encontrado la cuenta Battle.net.', }) 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': 'El cambio de correo ha sido confirmado y completado con éxito.'}) 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() )