Files
NightSpire/web-next/lib/register.ts
T
Inna 579396c34a Rebranding a NightSpire en toda la web
Marca, dominios y correos: UltimoWoW/Nova WoW -> NightSpire, cualquier URL de
esos dominios -> https://www.nightspire.gg y los correos -> consultas@nightspire.gg.
98 sustituciones en 16 ficheros de textos de usuario (messages/, app/, components/).

Assets: logo largo de cabecera y vídeo sustituidos por los de NightSpire (el logo
nuevo es 214x28, igual que el viejo, así que no hace falta tocar el CSS).

Redes: apuntan a las cuentas de NightSpire, tanto en el pie de la web como en el
de los correos (que seguían enlazando a las de NovaWoW desde el porte del diseño).

Cookie de sesión novawow_session -> nightspire_session. Cierra la sesión de todos
los conectados una vez; se asume ahora, recién hecho el cutover.

Ficheros del tema renombrados: novawow-style.css -> nightspire-style.css y
novawow-main-logo-transparent.webp -> nightspire-main-logo-transparent.webp, para
que la marca vieja no quede ni en las rutas.

El foro externo (foro.ultimowow.com) pasa a ser el nuestro: <Link> a /forum, que
respeta el idioma y no abre pestaña nueva.

NO se tocan y es a propósito:
- Los comentarios que nombran AzerothCore: describen el comportamiento REAL del
  core (límite de 12 ítems por correo, convención de baneos, SOAP...). Cambiarlos
  a NightSpire los volvería falsos. Además AzerothCore no aparece en la web: las
  14 menciones son todas comentarios de código.
- El regex de brandify() sigue buscando los nombres VIEJOS: son los que hay
  escritos en las noticias de la BD.
- lib/changelog.ts REPO='Inna/NovaWoW': es la ruta real del repo en Gitea.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:15:57 +00:00

113 lines
4.4 KiB
TypeScript

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<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 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 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} - NightSpire`, 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 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 accountactivation WHERE id = ?', [act.id])
return { success: true }
}