Implementa la recuperación de cuenta (contraseña / cuentas / activación) + islas
recover_account_view era un stub que solo renderizaba (los formularios no tenían backend y el HTML arrastraba un reCAPTCHA de otro dominio). Ahora es funcional: Backend: - Modelo PasswordReset (token+email+expiración 1h) + migración 0014. - recover_account_view maneja POST JSON con 3 tipos, todo por email (bnet), con respuestas genéricas anti-enumeración: * password: crea token y envía enlace de reset (emails/password_reset.html). * accountname: envía las cuentas de juego ligadas (emails/account_names.html). * activation: reenvía el enlace de activación pendiente (emails/activation.html). - reset_password_view: GET valida el token (isla), POST re-deriva el verifier SRP6 v2 de la cuenta bnet (bnet.bnet_make_registration) y lo actualiza; marca el token usado. Ruta 'reset-password'. - Resiliente si la BD de cuentas (AzerothCore) no está disponible: mensaje limpio, nunca 500. Frontend (islas Vite): RecoverForm.tsx (selector de tipo + email) y ResetPassword.tsx (nueva contraseña con token). Se elimina el reCAPTCHA roto; se puede añadir uno propio si se aportan claves. Verificado en producción: los 3 flujos devuelven éxito genérico; reset valida token y contraseñas; token válido no provoca 500. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -246,7 +246,115 @@ def activate_account_view(request):
|
||||
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':
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user