Recuperar cuenta: siempre por correo, y sin exigir Gmail

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 19:05:26 +00:00
parent 79520a57ff
commit fea1316f3c
4 changed files with 28 additions and 45 deletions
+22 -31
View File
@@ -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<Result> {
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<RowDataPacket[]>(
'SELECT battlenet_account FROM account WHERE username = ? LIMIT 1',
[value],
const [bn] = await db(DB.auth).query<RowDataPacket[]>(
'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<RowDataPacket[]>(
'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<Resu
return { success: true }
}
// El resto se recupera por CORREO.
const email = value
if (!email || !GMAIL_RE.test(email)) return { success: false, error: 'invalidEmail' }
if (type === 'securitytoken') {
try {
const [bn] = await db(DB.auth).query<RowDataPacket[]>(