From fea1316f3ccd73b014c8f52586c7ab93cf3193f4 Mon Sep 17 00:00:00 2001 From: adevopg Date: Wed, 15 Jul 2026 19:05:26 +0000 Subject: [PATCH] Recuperar cuenta: siempre por correo, y sin exigir Gmail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Las 4 opciones exigían Gmail (`GMAIL_RE`), así que las cuentas ya creadas no podían recuperarse: de las 17 bnet que existen, 16 NO son de Gmail. Ahora solo se valida que sea un correo (`EMAIL_RE`), en un único sitio antes del tipo. El REGISTRO sigue exigiendo Gmail: eso no se toca. - «Contraseña» se pedía por nombre de usuario («15#1»); ahora por correo, como las otras tres. El campo del formulario deja de alternar texto/correo. En el flujo de contraseña, el correo que se guarda en `passwordreset` se toma de la BD y NO del tecleado: el verifier SRP6 de bnet se deriva del correo, así que `resetPassword` necesita exactamente la misma forma (MAYÚSCULAS) o generaría un verifier con el que no se podría entrar. Quedan sin uso y se borran: `usesUsername`, la clave de error `invalidUsername` y el texto `username`. `Recover.invalidEmail` decía «Introduce un correo de Gmail válido» y pasa a ser genérico. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-next/app/[locale]/recover/RecoverForm.tsx | 12 ++--- web-next/lib/recover.ts | 53 ++++++++----------- web-next/messages/en.json | 4 +- web-next/messages/es.json | 4 +- 4 files changed, 28 insertions(+), 45 deletions(-) diff --git a/web-next/app/[locale]/recover/RecoverForm.tsx b/web-next/app/[locale]/recover/RecoverForm.tsx index d09634a..f3570c3 100644 --- a/web-next/app/[locale]/recover/RecoverForm.tsx +++ b/web-next/app/[locale]/recover/RecoverForm.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { useTranslations } from 'next-intl' import { Turnstile } from '@/components/Turnstile' -const ERROR_KEYS = ['invalidEmail', 'invalidUsername', 'captchaFailed'] as const +const ERROR_KEYS = ['invalidEmail', 'captchaFailed'] as const type RecoverType = 'password' | 'accountname' | 'securitytoken' | 'activation' export function RecoverForm() { @@ -17,8 +17,6 @@ export function RecoverForm() { const [sent, setSent] = useState(false) const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) - const usesUsername = type === 'password' - function changeType(newType: RecoverType) { setType(newType) setValue('') @@ -89,11 +87,9 @@ export function RecoverForm() { - {usesUsername ? ( - setValue(e.target.value)} /> - ) : ( - setValue(e.target.value)} /> - )} + {/* Las 4 opciones piden CORREO: con Battle.net la cuenta es el correo. + La de contraseña pedía el usuario («15#1»), que ya no vale. */} + setValue(e.target.value)} /> diff --git a/web-next/lib/recover.ts b/web-next/lib/recover.ts index 32e8c97..edc3401 100644 --- a/web-next/lib/recover.ts +++ b/web-next/lib/recover.ts @@ -1,7 +1,7 @@ import crypto from 'node:crypto' import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' -import { normalizeEmail, bnetMakeRegistration, GMAIL_RE } from './bnet' +import { normalizeEmail, bnetMakeRegistration, EMAIL_RE } from './bnet' import { sendMail } from './mail' import { activationEmailHtml, @@ -22,38 +22,33 @@ export interface Result { * `value` es el nombre de usuario (type=password) o el correo (resto). */ export async function requestRecovery(type: string, value: string): Promise { - value = (value || '').trim() const site = process.env.SITE_URL || '' - // La contraseña se recupera por NOMBRE DE USUARIO (p.ej. 15#1): se resuelve el - // correo Battle.net asociado y se envía el enlace de restablecimiento. + // Las CUATRO opciones se recuperan por CORREO (con Battle.net, la cuenta es el + // correo). Solo se valida que lo sea: NO se exige Gmail, porque el registro solo + // lo admite hoy pero las cuentas ya creadas tienen correos de todo tipo y no + // podrían recuperarse. + const email = (value || '').trim() + if (!email || !EMAIL_RE.test(email)) return { success: false, error: 'invalidEmail' } + if (type === 'password') { - if (!value || value.length > 20) return { success: false, error: 'invalidUsername' } try { - const [acc] = await db(DB.auth).query( - 'SELECT battlenet_account FROM account WHERE username = ? LIMIT 1', - [value], + const [bn] = await db(DB.auth).query( + 'SELECT email FROM battlenet_accounts WHERE email = ? LIMIT 1', + [normalizeEmail(email)], ) - const bnetId = acc[0]?.battlenet_account - if (bnetId) { - const [bn] = await db(DB.auth).query( - 'SELECT email FROM battlenet_accounts WHERE id = ?', - [bnetId], + // El correo se toma de la BD, NO el tecleado: el verifier SRP6 de bnet se + // deriva del correo, así que `resetPassword` necesita exactamente la misma + // forma (MAYÚSCULAS) o generaría un verifier con el que no se podría entrar. + const found = bn[0]?.email + if (found) { + const token = crypto.randomBytes(32).toString('hex') + await db(DB.default).query( + 'INSERT INTO passwordreset (email, token, created_at, used) VALUES (?, ?, NOW(), 0)', + [found, token], ) - const email = bn[0]?.email - if (email) { - const token = crypto.randomBytes(32).toString('hex') - await db(DB.default).query( - 'INSERT INTO passwordreset (email, token, created_at, used) VALUES (?, ?, NOW(), 0)', - [email, token], - ) - const link = `${site}/reset-password?token=${token}` - await sendMail( - email, - 'Restablecer contraseña - NightSpire', - passwordResetEmailHtml(email, link), - ) - } + const link = `${site}/reset-password?token=${token}` + await sendMail(found, 'Restablecer contraseña - NightSpire', passwordResetEmailHtml(found, link)) } } catch { /* acore no disponible: respuesta genérica igualmente */ @@ -61,10 +56,6 @@ export async function requestRecovery(type: string, value: string): Promise( diff --git a/web-next/messages/en.json b/web-next/messages/en.json index f10a3c9..9f7c051 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -225,14 +225,12 @@ "typeAccountName": "Account name", "typeSecurityToken": "Security token", "typeActivation": "Activation link", - "username": "Username", "email": "Email address", "submit": "Request", "sending": "Requesting data", "sent": "Data sent", "success": "If an account exists with those details, we have sent you the information.", - "invalidEmail": "Enter a valid Gmail address.", - "invalidUsername": "Enter a valid username.", + "invalidEmail": "Enter a valid email address.", "genericError": "Something went wrong. Please try again later.", "captchaFailed": "Anti-bot verification failed. Please retry.", "infoSections": [ diff --git a/web-next/messages/es.json b/web-next/messages/es.json index dbb4a38..08d7f47 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -225,14 +225,12 @@ "typeAccountName": "Nombre de cuenta", "typeSecurityToken": "Token de seguridad", "typeActivation": "Enlace de activación", - "username": "Nombre de usuario", "email": "Correo electrónico", "submit": "Solicitar", "sending": "Solicitando datos", "sent": "Datos enviados", "success": "Si existe una cuenta con esos datos, te hemos enviado la información.", - "invalidEmail": "Introduce un correo de Gmail válido.", - "invalidUsername": "Introduce un nombre de usuario válido.", + "invalidEmail": "Introduce un correo electrónico válido.", "genericError": "Algo ha salido mal. Inténtalo más tarde.", "captchaFailed": "Verificación anti-bots fallida. Reintenta.", "infoSections": [