Registro + activación por email en Next.js (SRP6 + nodemailer)
- lib/mail.ts: transporte SMTP (nodemailer, mismas creds Gmail que Django). - lib/register.ts: registerAccount (valida, comprueba email existente, crea fila en home_accountactivation reutilizando la tabla de Django, envía email de activación); activateAccount (crea battlenet_accounts con SRP6 v2 + account con SRP6 Grunt, como activate_account_view: expansion=2, battlenet_index=1; borra la activación). - Route handlers /api/auth/register y /api/auth/activate. - Páginas app/[locale]/register (RegisterForm cliente) y app/[locale]/activate-account (ActivateClient auto-POST al abrir el enlace). Textos en messages (Register, Activate). Validado: validaciones (missingFields/passwordMismatch/invalidEmail/passwordTooLong); ciclo completo activación->crea bnet+account->login OK con la cuenta creada (needsSelection=false), password mala->invalidCredentials. Todo en TS, crypto validada. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function activationEmailHtml(email: string, link: 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;margin:0 0 8px">Nova WoW</h1>
|
||||
<h2 style="margin:0 0 16px">Activa tu cuenta</h2>
|
||||
<p>La cuenta <strong>${email}</strong> se ha creado. Pulsa para activarla (enlace válido 1 hora):</p>
|
||||
<p><a href="${link}" style="display:inline-block;padding:12px 22px;background:#d79602;color:#1b120b;text-decoration:none;border-radius:6px;font-weight:bold">Activar cuenta</a></p>
|
||||
<p style="font-size:13px;word-break:break-all"><a href="${link}" style="color:#2471a9">${link}</a></p>
|
||||
</div></body></html>`
|
||||
}
|
||||
|
||||
export async function registerAccount(input: RegisterInput): Promise<Result> {
|
||||
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<RowDataPacket[]>(
|
||||
'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 home_accountactivation WHERE email = ? AND old_email IS NULL', [email])
|
||||
|
||||
// Reclutador opcional
|
||||
let recruiterId = 0
|
||||
if (recruiter) {
|
||||
try {
|
||||
const [a] = await db(DB.auth).query<RowDataPacket[]>('SELECT id FROM account WHERE username = ?', [recruiter])
|
||||
if (a[0]) {
|
||||
recruiterId = a[0].id
|
||||
} else {
|
||||
const [c] = await db(DB.characters).query<RowDataPacket[]>(
|
||||
'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 home_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<Result> {
|
||||
const [rows] = await db(DB.default).query<RowDataPacket[]>(
|
||||
'SELECT id, email, password, recruiter_id, created_at FROM home_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<ResultSetHeader>(
|
||||
'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<ResultSetHeader>(
|
||||
'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 home_accountactivation WHERE id = ?', [act.id])
|
||||
return { success: true }
|
||||
}
|
||||
Reference in New Issue
Block a user