579396c34a
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>
105 lines
4.2 KiB
TypeScript
105 lines
4.2 KiB
TypeScript
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 { confirmNewEmailHtml, confirmOldEmailHtml } from './emails'
|
|
import type { SessionData } from './session'
|
|
|
|
const GMAIL_RE = /^[a-zA-Z0-9._%+-]+@gmail\.com$/
|
|
|
|
export interface Result {
|
|
success: boolean
|
|
error?: string
|
|
}
|
|
|
|
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 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(session.username ?? current, newEmail, link),
|
|
)
|
|
return { success: true }
|
|
}
|
|
|
|
export async function confirmOldEmail(hash: string): Promise<Result> {
|
|
const [rows] = await db(DB.default).query<RowDataPacket[]>(
|
|
'SELECT id, 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.username ?? act.email, link),
|
|
)
|
|
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 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<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 accountactivation WHERE id = ?', [act.id])
|
|
return { success: true }
|
|
}
|