From 4f8f101ba8ae07bf3a7a7ab90e158fd952a2528c Mon Sep 17 00:00:00 2001 From: adevopg Date: Wed, 15 Jul 2026 18:02:54 +0000 Subject: [PATCH] =?UTF-8?q?Impedir=20cuentas=20Battle.net=20duplicadas=20a?= =?UTF-8?q?l=20activar;=20citar=20el=20l=C3=ADmite=20real=20de=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `registerAccount` ya rechazaba un correo con cuenta bnet, pero `activateAccount` insertaba SIN volver a comprobarlo, y entre registrarse y activar el correo puede dejar de estar libre. Como `battlenet_accounts.email` no tiene índice único, la BD tampoco lo frenaba: hay 3 filas TEST@TEST.COM de 2024 que lo demuestran. Ahora se recomprueba antes del INSERT y se borra la activación, que ya no sirve. Nuevo error `emailExists` (ES/EN) para no soltar un «enlace inválido» que despista. MAX_GAME_ACCOUNTS sube a lib/bnet.ts y lo usa el correo de activación: el texto prometía 10 cuentas mientras el código cortaba en 8, justo por estar el número escrito a mano en los dos sitios. El límite del panel ya funcionaba (API + aviso «Has alcanzado el máximo de cuentas»); solo se centraliza la constante. Verificado en producción con una activación pendiente de un correo que ya tenía bnet: devuelve emailExists, no crea la cuenta y el contador no sube. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[locale]/activate-account/ActivateClient.tsx | 2 +- .../app/api/account/add-game-account/route.ts | 3 +-- web-next/lib/bnet.ts | 7 +++++++ web-next/lib/emails.ts | 5 ++++- web-next/lib/register.ts | 15 +++++++++++++++ web-next/messages/en.json | 1 + web-next/messages/es.json | 1 + 7 files changed, 30 insertions(+), 4 deletions(-) diff --git a/web-next/app/[locale]/activate-account/ActivateClient.tsx b/web-next/app/[locale]/activate-account/ActivateClient.tsx index 098f4d8..41a222c 100644 --- a/web-next/app/[locale]/activate-account/ActivateClient.tsx +++ b/web-next/app/[locale]/activate-account/ActivateClient.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react' import { useTranslations } from 'next-intl' -const ERROR_KEYS = ['invalidLink', 'expiredLink'] as const +const ERROR_KEYS = ['invalidLink', 'expiredLink', 'emailExists'] as const export function ActivateClient({ hash, realm }: { hash: string; realm: string }) { const t = useTranslations('Activate') diff --git a/web-next/app/api/account/add-game-account/route.ts b/web-next/app/api/account/add-game-account/route.ts index 307987a..bdf412c 100644 --- a/web-next/app/api/account/add-game-account/route.ts +++ b/web-next/app/api/account/add-game-account/route.ts @@ -2,9 +2,8 @@ import type { ResultSetHeader, RowDataPacket } from 'mysql2' import { db, DB } from '@/lib/db' import { getSession } from '@/lib/session' import { authenticate } from '@/lib/auth' -import { gameMakeRegistration, makeGameAccountUsername, normalizeEmail } from '@/lib/bnet' +import { gameMakeRegistration, makeGameAccountUsername, normalizeEmail, MAX_GAME_ACCOUNTS } from '@/lib/bnet' -const MAX_GAME_ACCOUNTS = 8 // Battle.net: WoW1…WoW8 /** * Crea una nueva cuenta de juego (WoW2, WoW3…) bajo la Battle.net de la sesión. diff --git a/web-next/lib/bnet.ts b/web-next/lib/bnet.ts index 89d0673..1f90539 100644 --- a/web-next/lib/bnet.ts +++ b/web-next/lib/bnet.ts @@ -147,6 +147,13 @@ export function makeGameAccountUsername(bnetId: number, index = 1): string { return `${bnetId}#${index}` } +/** + * Cuentas de juego por cuenta Battle.net (WoW1…WoW8). Vive aquí, y no en la ruta + * que la aplica, para que el correo de activación cite el límite REAL: el texto + * anterior prometía 10 mientras el código ya cortaba en 8. + */ +export const MAX_GAME_ACCOUNTS = 8 + /** * Nombre visible de una cuenta de juego: «15#1» → «WOW1» (el nº tras # es la * cuenta de juego bajo la Battle.net; con varias serían WOW1, WOW2, WOW3…). diff --git a/web-next/lib/emails.ts b/web-next/lib/emails.ts index 710cc8c..01c453c 100644 --- a/web-next/lib/emails.ts +++ b/web-next/lib/emails.ts @@ -21,6 +21,8 @@ * entra con el correo, no con un usuario). */ +import { MAX_GAME_ACCOUNTS } from './bnet' + const SERVER_NAME = 'NightSpire' /** Raíz del sitio, sin barra final. De aquí salen todos los enlaces e imágenes. */ @@ -234,7 +236,8 @@ export function activationEmailHtml(email: string, link: string, locale = 'es'): ), pData(`Cuenta: ${esc(email)}

`), p( - 'Recuerda que se entra tanto a la web como al servidor con tu correo y tu contraseña.
Recuerda que puedes crear las cuentas que quieras desde panel de usuario asociadas a este correo.', + 'Recuerda que se entra tanto a la web como al servidor con tu correo y tu contraseña.
' + + `Recuerda que puedes crear hasta ${MAX_GAME_ACCOUNTS} cuentas que quieras desde panel de usuario asociadas a este correo.`, ), button(link, 'ACTIVAR LA CUENTA'), fallbackLink(link), diff --git a/web-next/lib/register.ts b/web-next/lib/register.ts index 6b96dae..553314d 100644 --- a/web-next/lib/register.ts +++ b/web-next/lib/register.ts @@ -95,6 +95,21 @@ export async function activateAccount(hash: string, lastIp = '0.0.0.0'): Promise const emailNorm = normalizeEmail(act.email) const password: string = act.password + // Volver a comprobar que el correo sigue libre. `registerAccount` ya lo mira, pero + // entre registrarse y activar puede haberse creado la cuenta (otra activación + // pendiente del mismo correo, un cambio de correo confirmado...), y sin esto se + // insertaba una bnet duplicada: `battlenet_accounts.email` no tiene índice único. + const [dup] = await db(DB.auth).query( + 'SELECT COUNT(*) AS n FROM battlenet_accounts WHERE email = ?', + [emailNorm], + ) + if (Number(dup[0]?.n ?? 0) > 0) { + // La activación ya no sirve para nada: la cuenta existe. Se borra para que el + // enlace no quede dando vueltas. + await db(DB.default).query('DELETE FROM accountactivation WHERE id = ?', [act.id]) + return { success: false, error: 'emailExists' } + } + // Cuenta Battle.net (SRP6 v2) const bnet = bnetMakeRegistration(emailNorm, password) const [bnetRes] = await db(DB.auth).query( diff --git a/web-next/messages/en.json b/web-next/messages/en.json index 1fde22d..bfc6c82 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -206,6 +206,7 @@ "canPlay": "You can now log in to the website or start enjoying the server.", "invalidLink": "The activation link is invalid", "expiredLink": "The activation link has expired", + "emailExists": "An account with that email already exists", "needHelp": "If you need help creating an account, you can contact the {realm} team.", "title": "Account activation" }, diff --git a/web-next/messages/es.json b/web-next/messages/es.json index 9103705..9f24c51 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -206,6 +206,7 @@ "canPlay": "Ya puedes conectar a la página web o empezar a disfrutar del servidor.", "invalidLink": "El enlace de activación es inválido", "expiredLink": "El enlace de activación ha caducado", + "emailExists": "Ya existe una cuenta con ese correo", "needHelp": "Si necesitas ayuda para crear una cuenta, puedes contactar con el equipo de {realm}.", "title": "Activación de cuenta" },