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:
@@ -4,7 +4,7 @@ import { useState } from 'react'
|
|||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { Turnstile } from '@/components/Turnstile'
|
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'
|
type RecoverType = 'password' | 'accountname' | 'securitytoken' | 'activation'
|
||||||
|
|
||||||
export function RecoverForm() {
|
export function RecoverForm() {
|
||||||
@@ -17,8 +17,6 @@ export function RecoverForm() {
|
|||||||
const [sent, setSent] = useState(false)
|
const [sent, setSent] = useState(false)
|
||||||
const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null)
|
const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null)
|
||||||
|
|
||||||
const usesUsername = type === 'password'
|
|
||||||
|
|
||||||
function changeType(newType: RecoverType) {
|
function changeType(newType: RecoverType) {
|
||||||
setType(newType)
|
setType(newType)
|
||||||
setValue('')
|
setValue('')
|
||||||
@@ -89,11 +87,9 @@ export function RecoverForm() {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{usesUsername ? (
|
{/* Las 4 opciones piden CORREO: con Battle.net la cuenta es el correo.
|
||||||
<input type="text" maxLength={20} placeholder={t('username')} required value={value} onChange={(e) => setValue(e.target.value)} />
|
La de contraseña pedía el usuario («15#1»), que ya no vale. */}
|
||||||
) : (
|
<input type="email" maxLength={254} placeholder={t('email')} required value={value} onChange={(e) => setValue(e.target.value)} />
|
||||||
<input type="email" maxLength={254} placeholder={t('email')} required value={value} onChange={(e) => setValue(e.target.value)} />
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
+22
-31
@@ -1,7 +1,7 @@
|
|||||||
import crypto from 'node:crypto'
|
import crypto from 'node:crypto'
|
||||||
import type { RowDataPacket } from 'mysql2'
|
import type { RowDataPacket } from 'mysql2'
|
||||||
import { db, DB } from './db'
|
import { db, DB } from './db'
|
||||||
import { normalizeEmail, bnetMakeRegistration, GMAIL_RE } from './bnet'
|
import { normalizeEmail, bnetMakeRegistration, EMAIL_RE } from './bnet'
|
||||||
import { sendMail } from './mail'
|
import { sendMail } from './mail'
|
||||||
import {
|
import {
|
||||||
activationEmailHtml,
|
activationEmailHtml,
|
||||||
@@ -22,38 +22,33 @@ export interface Result {
|
|||||||
* `value` es el nombre de usuario (type=password) o el correo (resto).
|
* `value` es el nombre de usuario (type=password) o el correo (resto).
|
||||||
*/
|
*/
|
||||||
export async function requestRecovery(type: string, value: string): Promise<Result> {
|
export async function requestRecovery(type: string, value: string): Promise<Result> {
|
||||||
value = (value || '').trim()
|
|
||||||
const site = process.env.SITE_URL || ''
|
const site = process.env.SITE_URL || ''
|
||||||
|
|
||||||
// La contraseña se recupera por NOMBRE DE USUARIO (p.ej. 15#1): se resuelve el
|
// Las CUATRO opciones se recuperan por CORREO (con Battle.net, la cuenta es el
|
||||||
// correo Battle.net asociado y se envía el enlace de restablecimiento.
|
// 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 (type === 'password') {
|
||||||
if (!value || value.length > 20) return { success: false, error: 'invalidUsername' }
|
|
||||||
try {
|
try {
|
||||||
const [acc] = await db(DB.auth).query<RowDataPacket[]>(
|
const [bn] = await db(DB.auth).query<RowDataPacket[]>(
|
||||||
'SELECT battlenet_account FROM account WHERE username = ? LIMIT 1',
|
'SELECT email FROM battlenet_accounts WHERE email = ? LIMIT 1',
|
||||||
[value],
|
[normalizeEmail(email)],
|
||||||
)
|
)
|
||||||
const bnetId = acc[0]?.battlenet_account
|
// El correo se toma de la BD, NO el tecleado: el verifier SRP6 de bnet se
|
||||||
if (bnetId) {
|
// deriva del correo, así que `resetPassword` necesita exactamente la misma
|
||||||
const [bn] = await db(DB.auth).query<RowDataPacket[]>(
|
// forma (MAYÚSCULAS) o generaría un verifier con el que no se podría entrar.
|
||||||
'SELECT email FROM battlenet_accounts WHERE id = ?',
|
const found = bn[0]?.email
|
||||||
[bnetId],
|
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
|
const link = `${site}/reset-password?token=${token}`
|
||||||
if (email) {
|
await sendMail(found, 'Restablecer contraseña - NightSpire', passwordResetEmailHtml(found, link))
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* acore no disponible: respuesta genérica igualmente */
|
/* acore no disponible: respuesta genérica igualmente */
|
||||||
@@ -61,10 +56,6 @@ export async function requestRecovery(type: string, value: string): Promise<Resu
|
|||||||
return { success: true }
|
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') {
|
if (type === 'securitytoken') {
|
||||||
try {
|
try {
|
||||||
const [bn] = await db(DB.auth).query<RowDataPacket[]>(
|
const [bn] = await db(DB.auth).query<RowDataPacket[]>(
|
||||||
|
|||||||
@@ -225,14 +225,12 @@
|
|||||||
"typeAccountName": "Account name",
|
"typeAccountName": "Account name",
|
||||||
"typeSecurityToken": "Security token",
|
"typeSecurityToken": "Security token",
|
||||||
"typeActivation": "Activation link",
|
"typeActivation": "Activation link",
|
||||||
"username": "Username",
|
|
||||||
"email": "Email address",
|
"email": "Email address",
|
||||||
"submit": "Request",
|
"submit": "Request",
|
||||||
"sending": "Requesting data",
|
"sending": "Requesting data",
|
||||||
"sent": "Data sent",
|
"sent": "Data sent",
|
||||||
"success": "If an account exists with those details, we have sent you the information.",
|
"success": "If an account exists with those details, we have sent you the information.",
|
||||||
"invalidEmail": "Enter a valid Gmail address.",
|
"invalidEmail": "Enter a valid email address.",
|
||||||
"invalidUsername": "Enter a valid username.",
|
|
||||||
"genericError": "Something went wrong. Please try again later.",
|
"genericError": "Something went wrong. Please try again later.",
|
||||||
"captchaFailed": "Anti-bot verification failed. Please retry.",
|
"captchaFailed": "Anti-bot verification failed. Please retry.",
|
||||||
"infoSections": [
|
"infoSections": [
|
||||||
|
|||||||
@@ -225,14 +225,12 @@
|
|||||||
"typeAccountName": "Nombre de cuenta",
|
"typeAccountName": "Nombre de cuenta",
|
||||||
"typeSecurityToken": "Token de seguridad",
|
"typeSecurityToken": "Token de seguridad",
|
||||||
"typeActivation": "Enlace de activación",
|
"typeActivation": "Enlace de activación",
|
||||||
"username": "Nombre de usuario",
|
|
||||||
"email": "Correo electrónico",
|
"email": "Correo electrónico",
|
||||||
"submit": "Solicitar",
|
"submit": "Solicitar",
|
||||||
"sending": "Solicitando datos",
|
"sending": "Solicitando datos",
|
||||||
"sent": "Datos enviados",
|
"sent": "Datos enviados",
|
||||||
"success": "Si existe una cuenta con esos datos, te hemos enviado la información.",
|
"success": "Si existe una cuenta con esos datos, te hemos enviado la información.",
|
||||||
"invalidEmail": "Introduce un correo de Gmail válido.",
|
"invalidEmail": "Introduce un correo electrónico válido.",
|
||||||
"invalidUsername": "Introduce un nombre de usuario válido.",
|
|
||||||
"genericError": "Algo ha salido mal. Inténtalo más tarde.",
|
"genericError": "Algo ha salido mal. Inténtalo más tarde.",
|
||||||
"captchaFailed": "Verificación anti-bots fallida. Reintenta.",
|
"captchaFailed": "Verificación anti-bots fallida. Reintenta.",
|
||||||
"infoSections": [
|
"infoSections": [
|
||||||
|
|||||||
Reference in New Issue
Block a user