import crypto from 'node:crypto' import type { ResultSetHeader, RowDataPacket } from 'mysql2' import { db, DB } from './db' import { normalizeEmail, bnetMakeRegistration, gameMakeRegistration, makeGameAccountUsername } from './bnet' import { sendMail } from './mail' import { activationEmailHtml } from './emails' const GMAIL_RE = /^[a-zA-Z0-9._%+-]+@gmail\.com$/ export interface RegisterInput { password: string confPassword: string email: string confEmail: string recruiter?: string } export interface Result { success: boolean error?: string } export async function registerAccount(input: RegisterInput): Promise { const password = (input.password || '').trim() const confPassword = (input.confPassword || '').trim() const email = (input.email || '').trim() const confEmail = (input.confEmail || '').trim() const recruiter = (input.recruiter || '').trim() if (!password || !email) return { success: false, error: 'missingFields' } if (password !== confPassword) return { success: false, error: 'passwordMismatch' } if (password.length > 16) return { success: false, error: 'passwordTooLong' } if (email !== confEmail || !GMAIL_RE.test(email)) return { success: false, error: 'invalidEmail' } // ¿ya existe una cuenta Battle.net con ese email? try { const [rows] = await db(DB.auth).query( 'SELECT COUNT(*) AS n FROM battlenet_accounts WHERE email = ?', [normalizeEmail(email)], ) if (Number(rows[0]?.n ?? 0) > 0) return { success: false, error: 'emailExists' } } catch { // BD de cuentas no disponible en dev: continuamos. } // Borrar cualquier activación de registro pendiente para ese email await db(DB.default).query('DELETE FROM accountactivation WHERE email = ? AND old_email IS NULL', [email]) // Reclutador opcional let recruiterId = 0 if (recruiter) { try { const [a] = await db(DB.auth).query('SELECT id FROM account WHERE username = ?', [recruiter]) if (a[0]) { recruiterId = a[0].id } else { const [c] = await db(DB.characters).query( 'SELECT account FROM characters WHERE name = ?', [recruiter], ) if (c[0]) recruiterId = c[0].account else return { success: false, error: 'recruiterNotFound' } } } catch { return { success: false, error: 'recruiterNotFound' } } } const hash = crypto.randomBytes(16).toString('hex') // 32 hex chars (col hash = CharField(32)) await db(DB.default).query( 'INSERT INTO accountactivation (email, password, recruiter_id, hash, created_at, is_used, is_new_email_used) VALUES (?, ?, ?, ?, NOW(), 0, 0)', [email, password, recruiterId, hash], ) const link = `${process.env.SITE_URL}/activate-account?act=${hash}` await sendMail(email, `Activación de la cuenta ${email} - Nova WoW`, activationEmailHtml(email, link)) return { success: true } } export async function activateAccount(hash: string, lastIp = '0.0.0.0'): Promise { const [rows] = await db(DB.default).query( 'SELECT id, email, password, recruiter_id, created_at FROM accountactivation WHERE hash = ? AND old_email IS NULL', [hash], ) const act = rows[0] if (!act) return { success: false, error: 'invalidLink' } if (Date.now() - new Date(act.created_at).getTime() > 3600_000) return { success: false, error: 'expiredLink' } const emailNorm = normalizeEmail(act.email) const password: string = act.password // Cuenta Battle.net (SRP6 v2) const bnet = bnetMakeRegistration(emailNorm, password) const [bnetRes] = await db(DB.auth).query( 'INSERT INTO battlenet_accounts (email, srp_version, salt, verifier) VALUES (?, ?, ?, ?)', [emailNorm, bnet.srpVersion, bnet.salt, bnet.verifier], ) const bnetId = bnetRes.insertId // Cuenta de juego (SRP6 Grunt) const gameUsername = makeGameAccountUsername(bnetId, 1) const game = gameMakeRegistration(gameUsername, password) await db(DB.auth).query( 'INSERT INTO account (username, salt, verifier, email, reg_mail, recruiter, joindate, last_ip, expansion, battlenet_account, battlenet_index) ' + 'VALUES (?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?, 1)', [gameUsername, game.salt, game.verifier, emailNorm, emailNorm, act.recruiter_id || 0, lastIp, 2, bnetId], ) await db(DB.default).query('DELETE FROM accountactivation WHERE id = ?', [act.id]) return { success: true } }