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": [