import crypto from 'node:crypto' import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' import { normalizeEmail, bnetMakeRegistration } from './bnet' import { sendMail } from './mail' const GMAIL_RE = /^[a-zA-Z0-9._%+-]+@gmail\.com$/ export interface Result { success: boolean error?: string redirect?: string } function emailShell(title: string, bodyHtml: string): string { return `

Nova WoW

${title}

${bodyHtml}
` } /** * Recuperación por email/usuario (anti-enumeración: respuesta genérica siempre). * `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. 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 bnetId = acc[0]?.battlenet_account if (bnetId) { const [bn] = await db(DB.auth).query( 'SELECT email FROM battlenet_accounts WHERE id = ?', [bnetId], ) const email = bn[0]?.email if (email) { const token = crypto.randomBytes(32).toString('hex') await db(DB.default).query( 'INSERT INTO home_passwordreset (email, token, created_at, used) VALUES (?, ?, NOW(), 0)', [email, token], ) const link = `${site}/reset-password?token=${token}` await sendMail( email, 'Restablecer contraseña - Nova WoW', emailShell( 'Restablecer contraseña', `

Pulsa para elegir una nueva contraseña (válido 1 hora):

Restablecer

${link}

`, ), ) } } } catch { /* acore no disponible: respuesta genérica igualmente */ } 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( 'SELECT id FROM battlenet_accounts WHERE email = ?', [normalizeEmail(email)], ) if (bn[0]) { const [games] = await db(DB.auth).query( 'SELECT id, username FROM account WHERE battlenet_account = ? ORDER BY battlenet_index', [bn[0].id], ) const ids = games.map((g) => g.id) if (ids.length) { const placeholders = ids.map(() => '?').join(',') const [toks] = await db(DB.default).query( `SELECT user_id, token FROM home_securitytoken WHERE user_id IN (${placeholders})`, ids, ) if (toks.length) { const nameById = new Map(games.map((g) => [g.id, g.username])) const list = toks .map((r) => `
  • ${nameById.get(r.user_id) ?? r.user_id}: ${r.token}
  • `) .join('') await sendMail( email, 'Tu Token de seguridad - Nova WoW', emailShell( 'Tu Token de seguridad', `

    Token(s) de seguridad de las cuentas asociadas a ${email}:

      ${list}

    Consérvalo en un lugar seguro y no lo compartas.

    `, ), ) } } } } catch { /* ignore */ } return { success: true } } if (type === 'accountname') { try { const [acc] = await db(DB.auth).query( 'SELECT id FROM battlenet_accounts WHERE email = ?', [normalizeEmail(email)], ) if (acc[0]) { const [games] = await db(DB.auth).query( 'SELECT username FROM account WHERE battlenet_account = ? ORDER BY battlenet_index', [acc[0].id], ) const list = games.map((g) => `
  • ${g.username}
  • `).join('') || '
  • ' await sendMail( email, 'Tus cuentas de juego - Nova WoW', emailShell('Tus cuentas de juego', `

    Cuentas asociadas a ${email}:

      ${list}
    `), ) } } catch { /* ignore */ } return { success: true } } if (type === 'activation') { try { const [rows] = await db(DB.default).query( 'SELECT hash, password FROM home_accountactivation WHERE email = ? AND old_email IS NULL ORDER BY created_at DESC LIMIT 1', [email], ) if (rows[0]) { const link = `${site}/activate-account?act=${rows[0].hash}` await sendMail( email, `Activación de la cuenta ${email} - Nova WoW`, emailShell('Activa tu cuenta', `

    Activar cuenta

    ${link}

    `), ) } } catch { /* ignore */ } return { success: true } } return { success: false, error: 'invalidType' } } export async function resetPassword(token: string, newPassword: string, confPassword: string): Promise { const [rows] = await db(DB.default).query( 'SELECT id, email, created_at FROM home_passwordreset WHERE token = ? AND used = 0', [token], ) const pr = rows[0] if (!pr) return { success: false, error: 'invalidLink' } if (Date.now() - new Date(pr.created_at).getTime() > 3600_000) return { success: false, error: 'expiredLink' } if (!newPassword || newPassword !== confPassword) return { success: false, error: 'passwordMismatch' } if (newPassword.length > 16) return { success: false, error: 'passwordTooLong' } try { const reg = bnetMakeRegistration(pr.email, newPassword) const [res] = await db(DB.auth).query( 'UPDATE battlenet_accounts SET srp_version = ?, salt = ?, verifier = ? WHERE email = ?', [reg.srpVersion, reg.salt, reg.verifier, normalizeEmail(pr.email)], ) // @ts-expect-error affectedRows if (!res.affectedRows) return { success: false, error: 'accountNotFound' } } catch { return { success: false, error: 'genericError' } } await db(DB.default).query('UPDATE home_passwordreset SET used = 1 WHERE id = ?', [pr.id]) return { success: true, redirect: '/log-in' } }