Cambio de email con doble confirmación (completa el bloque de auth)
- lib/change-email.ts: requestEmailChange (valida pass/email/token, crea activación con old_email/old_email_hash, email al correo actual), confirmOldEmail (marca is_used, email al nuevo), confirmNewEmail (re-deriva verifier con el nuevo email + actualiza battlenet_accounts y account; borra la activación). - components/ConfirmClient.tsx: cliente genérico que confirma un hash por POST al abrir. - Routes /api/account/change-email, /confirm-old-email, /confirm-new-email. - Páginas /change-email (protegida) y /confirm-old-email, /confirm-new-email (auto). - Catálogos ChangeEmail, ConfirmOldEmail, ConfirmNewEmail. Verificado: change-email protegida (401/redirect), confirmaciones con hash inválido -> invalidLink sin crash. AUTH 100% reimplementado en Next.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import crypto from 'node:crypto'
|
||||
import type { RowDataPacket, ResultSetHeader } from 'mysql2'
|
||||
import { db, DB } from './db'
|
||||
import { authenticate } from './auth'
|
||||
import { bnetMakeRegistration, normalizeEmail } from './bnet'
|
||||
import { checkSecurityToken } from './security-token'
|
||||
import { sendMail } from './mail'
|
||||
import type { SessionData } from './session'
|
||||
|
||||
const GMAIL_RE = /^[a-zA-Z0-9._%+-]+@gmail\.com$/
|
||||
|
||||
export interface Result {
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
function shell(title: string, body: string): string {
|
||||
return `<!DOCTYPE html><html lang="es"><body style="font-family:Arial,sans-serif;background:#1b120b;color:#e8dccb;padding:24px">
|
||||
<div style="max-width:600px;margin:0 auto;background:#241812;border:1px solid #4a3320;border-radius:8px;padding:28px">
|
||||
<h1 style="color:#d79602">Nova WoW</h1><h2>${title}</h2>${body}</div></body></html>`
|
||||
}
|
||||
|
||||
export async function requestEmailChange(
|
||||
session: SessionData,
|
||||
curPassword: string,
|
||||
curEmail: string,
|
||||
newEmail: string,
|
||||
confEmail: string,
|
||||
token: string,
|
||||
): Promise<Result> {
|
||||
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 || !GMAIL_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 home_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 - Nova WoW`,
|
||||
shell('Confirma el cambio de correo', `<p>Has solicitado cambiar tu correo a <strong>${newEmail}</strong>. Confirma desde tu correo actual:</p><p><a href="${link}" style="color:#d79602">Confirmar</a></p><p style="font-size:13px">${link}</p>`),
|
||||
)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function confirmOldEmail(hash: string): Promise<Result> {
|
||||
const [rows] = await db(DB.default).query<RowDataPacket[]>(
|
||||
'SELECT id, email, hash FROM home_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 home_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 - Nova WoW`,
|
||||
shell('Confirma tu nuevo correo', `<p>Confirma este correo como el nuevo de tu cuenta:</p><p><a href="${link}" style="color:#d79602">Confirmar nuevo correo</a></p><p style="font-size:13px">${link}</p>`),
|
||||
)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function confirmNewEmail(hash: string): Promise<Result> {
|
||||
const [rows] = await db(DB.default).query<RowDataPacket[]>(
|
||||
'SELECT id, email, old_email, password FROM home_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 home_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<RowDataPacket[]>(
|
||||
'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<ResultSetHeader>(
|
||||
'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' }
|
||||
}
|
||||
await db(DB.default).query('DELETE FROM home_accountactivation WHERE id = ?', [act.id])
|
||||
return { success: true }
|
||||
}
|
||||
Reference in New Issue
Block a user