import crypto from 'node:crypto' import type { RowDataPacket, ResultSetHeader } from 'mysql2' import { db, DB } from './db' import { authenticate } from './auth' import { bnetMakeRegistration, normalizeEmail, EMAIL_RE } from './bnet' import { checkSecurityToken } from './security-token' import { sendMail } from './mail' import { confirmNewEmailHtml, confirmOldEmailHtml, oldEmailNotificationHtml } from './emails' import type { SessionData } from './session' export interface Result { success: boolean error?: string } export async function requestEmailChange( session: SessionData, curPassword: string, curEmail: string, newEmail: string, confEmail: string, token: string, ): Promise { const current = session.bnetEmail ?? '' // ya normalizado (MAYÚS) const userId = session.accountId ?? 0 if (!curPassword || !curEmail || !newEmail || !confEmail || !token) return { success: false, error: 'missingFields' } if (normalizeEmail(curEmail) !== current) return { success: false, error: 'wrongCurrentEmail' } if (newEmail !== confEmail || !EMAIL_RE.test(newEmail)) return { success: false, error: 'invalidEmail' } if (!(await checkSecurityToken(userId, token))) return { success: false, error: 'invalidToken' } if (!(await authenticate(current, curPassword))) return { success: false, error: 'wrongCurrentPassword' } const oldHash = crypto.randomBytes(16).toString('hex') const newHash = crypto.randomBytes(16).toString('hex') await db(DB.default).query( 'INSERT INTO accountactivation (username, email, old_email, password, recruiter_id, hash, old_email_hash, is_used, is_new_email_used, created_at) ' + 'VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NOW())', [session.username ?? null, newEmail, current, curPassword, userId, newHash, oldHash], ) const site = process.env.SITE_URL || '' const link = `${site}/confirm-old-email?hash=${oldHash}` await sendMail( current, `Confirma el cambio de correo - NightSpire`, confirmOldEmailHtml(current, session.username ?? null, newEmail, link), ) return { success: true } } export async function confirmOldEmail(hash: string): Promise { // `old_email` es el correo con el que la cuenta se identifica AHORA: el cambio // aún no ha terminado, así que es con ese con el que hay que saludar. const [rows] = await db(DB.default).query( 'SELECT id, email, old_email, hash, username FROM accountactivation WHERE old_email_hash = ? AND is_used = 0', [hash], ) const act = rows[0] if (!act) return { success: false, error: 'invalidLink' } await db(DB.default).query('UPDATE accountactivation SET is_used = 1 WHERE id = ?', [act.id]) const site = process.env.SITE_URL || '' const link = `${site}/confirm-new-email?hash=${act.hash}` await sendMail( act.email, `Confirma tu nuevo correo - NightSpire`, confirmNewEmailHtml(act.old_email || act.email, act.username ?? null, link), ) return { success: true } } export async function confirmNewEmail(hash: string): Promise { const [rows] = await db(DB.default).query( 'SELECT id, email, old_email, password, username FROM accountactivation WHERE hash = ? AND is_new_email_used = 0 AND old_email IS NOT NULL', [hash], ) const act = rows[0] if (!act) return { success: false, error: 'invalidLink' } await db(DB.default).query('UPDATE accountactivation SET is_new_email_used = 1 WHERE id = ?', [act.id]) const newNorm = normalizeEmail(act.email) const oldNorm = normalizeEmail(act.old_email || '') try { const reg = bnetMakeRegistration(newNorm, act.password) const [bnetRows] = await db(DB.auth).query( 'SELECT id FROM battlenet_accounts WHERE email = ?', [oldNorm], ) if (!bnetRows[0]) return { success: false, error: 'accountNotFound' } const bnetId = bnetRows[0].id await db(DB.auth).query( 'UPDATE battlenet_accounts SET email = ?, srp_version = ?, salt = ?, verifier = ? WHERE id = ?', [newNorm, reg.srpVersion, reg.salt, reg.verifier, bnetId], ) await db(DB.auth).query('UPDATE account SET email = ?, reg_mail = ? WHERE battlenet_account = ?', [ newNorm, newNorm, bnetId, ]) } catch { return { success: false, error: 'genericError' } } // Avisar al correo ANTIGUO de que ya no manda en la cuenta. Va después de que el // cambio haya cuajado (si falla arriba, no hay nada que avisar) y sin enlaces: // solo informa. Si alguien secuestra una sesión y cambia el correo, esta es la // única forma que tiene el dueño de enterarse, porque al entrar se usa el correo. // `sendMail` nunca lanza (devuelve false), así que no puede tumbar un cambio ya // hecho: como mucho el aviso no sale. await sendMail( oldNorm, `Cambio de correo realizado - NightSpire`, oldEmailNotificationHtml(oldNorm, act.username ?? null, newNorm), ) await db(DB.default).query('DELETE FROM accountactivation WHERE id = ?', [act.id]) return { success: true } }