Commit Graph

8 Commits

Author SHA1 Message Date
Inna edf1feedf6 El correo sale en MAYÚSCULAS en los correos de token y activación
- Token: el asunto era «Token de seguridad - NightSpire» a secas. El original
  decía «Token de seguridad de la cuenta {username} - {NOMBRE_SERVIDOR}»; la
  reescritura se dejó la cuenta por el camino. Ese correo sale de
  `session.bnetEmail`, que ya va normalizado, así que las MAYÚSCULAS son gratis.
- Activación: asunto y cuerpo pasan a normalizar el correo, para que se vea igual
  que en el del token. El DESTINATARIO va sin normalizar: es la dirección tal
  cual la escribió el usuario.

El correo de activación se manda desde DOS sitios: `registerAccount` y
`recover.ts` (el reenvío del enlace, «no me llegó»). Se cambian los dos o saldría
distinto según por dónde lo pidieras. Lo delató el build: /api/auth/activate
cargaba un chunk y /api/auth/recover otro con la versión vieja.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:40:33 +00:00
Inna 8c85cf2dbe El enlace de activación vuelve a ser alfanumérico; avisar a qué correo se envió
Dos cosas que el port de Django cambió sin querer:

- El hash de activación se generaba con `randomBytes(16).toString('hex')`: 32
  caracteres, sí, pero solo `0-9a-f`. El original era `get_random_string(32)`,
  alfanumérico con mayúsculas y minúsculas (`?act=P4JHQDJey2jJDnEBnqUePI5O2qEFB1`).
- El aviso de cuenta creada era un genérico «Revisa tu correo», mientras que el
  original decía qué cuenta se creó y a qué dirección fue el enlace. Se recupera
  el formato de dos líneas (con id `create-response`, como el original).

El muestreo por rechazo del token de seguridad se sube a `lib/random-token.ts` y
lo comparten los dos generadores, que solo se diferencian en el alfabeto: letras
para el token que se teclea a mano, alfanumérico para el hash de la URL.

Verificado con 100.000 hashes: todos casan /^[A-Za-z0-9]{32}$/, salen los 62
caracteres y la desviación por carácter se queda en 1,3% (ruido).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:34:53 +00:00
Inna 6e4a123dde El token de seguridad son 6 letras al azar, no base64url sesgado
El token se generaba con `randomBytes(4).toString('base64url').slice(0, 6)`,
portado tal cual del Django original (`secrets.token_urlsafe(4)[:6]`), y tenía
dos taras para un código que se lee y se teclea desde un correo:

- Metía `-` y `_`.
- 4 bytes son 32 bits, pero 6 caracteres base64 codifican 36: al último solo
  le llegaban 2 bits reales, así que SIEMPRE terminaba en A, Q, g o w
  (comprobado sobre 20.000 tokens: 4 valores distintos en esa posición).

Ahora son 6 letras A-Z/a-z uniformes, con muestreo por rechazo porque 256 no es
múltiplo de 52 y `byte % 52` favorecería a las primeras letras. Verificado con
300.000 tokens: todos casan /^[A-Za-z]{6}$/, las 52 letras aparecen en la última
posición y la desviación por letra se queda en el 1,4% (ruido).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:11:22 +00:00
Inna 75e4ceed4c El token de seguridad caducado ya no vale; traducir 2 errores que faltaban
checkSecurityToken guardaba expires_at pero NO lo comprobaba: un token
caducado seguía siendo válido para regalos y servicios. Se comprueba en SQL
(`expires_at > NOW()`) y no en JS, para que los dos lados de la comparación
sean hora del servidor: expires_at se inserta como Date de JS y compararlo
contra Date.now() dependería de que Node y MySQL tuvieran la misma zona.

Además faltaban `errors.notAuthenticated` e `invalidRequest` en el namespace
Store (ES y EN): si se caducaba la sesión, send-gift decía «Ha ocurrido un
error» en vez de pedir que vuelvas a iniciar sesión — justo el mensaje que
hace pensar que el botón sigue roto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:03:39 +00:00
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
Inna cccc70338d Quitar el prefijo home_ de las 41 tablas del portal
El prefijo venía del nombre de la app Django (`home`), jubilada y borrada hoy,
así que ya no significaba nada: home_stripelog -> stripelog. La BD se sigue
llamando django_wow por historia (renombrarla es otra operación).

Comprobado antes de tocar nada: 0 colisiones con nombres existentes, 0 palabras
reservadas (contrastado contra las 260 de MySQL), sin vistas ni triggers que
dependieran de ellas. El RENAME va en una sola sentencia porque así es atómico,
y MySQL reapunta solo las 4 claves ajenas entre estas tablas.

170 referencias actualizadas en 37 ficheros. Se dejó a propósito
sql/drop_home_securitytoken_authuser_fk.sql sin tocar: es una migración ya
aplicada y reescribirla sería falsear el historial.

De paso salió un fallo previo: prices.ts consultaba `home_restoreitemprice`,
una tabla que NO existe. El try/catch de priceFrom se comía el error y devolvía
siempre el precio por defecto, así que el precio de restaurar objetos nunca fue
configurable. Se deja documentado en el código; crear la tabla es otra decisión.

Verificado tras aplicar: 0 tablas home_, las 51 siguen ahí, y el conteo exacto
de filas cuadra con el volcado previo (store_item 3068, item_data 44873,
stripelog 35, store_order 26, noticia 50). La web responde 200 en todas las
rutas y la portada carga las noticias sin errores.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:49:19 +00:00
Inna 9390eefa93 Correos: portar el diseño del tema y unificarlos en lib/emails.ts
Los correos usaban un shell inline distinto en cada remitente. Ahora salen
todos de una sola maqueta, la de security_token, portada de las plantillas de
Django. De las 7 de Django solo security_token y activation traían el diseño;
las otras 5 eran HTML soso.

Las plantillas de Django eran un volcado del DOM de Gmail. Al portarlas se
quitó lo que no era diseño: los enlaces iban envueltos en el rastreador de
Mailjet (079xk.mjt.lu), que además redirigía a ultimowow.com en vez de a
nosotros; había un píxel de apertura, un "Click on me" oculto y atributos que
mete Gmail al mostrar el correo. Las imágenes apuntaban al proxy de Gmail
(ci3.googleusercontent.com), no a nuestro /static/, así que el diseño se
rompía el día que Google dejara de servirlas.

Ninguna URL va a fuego: todas cuelgan de SITE_URL, así que cambiar de dominio
es tocar el .env. En un correo han de ser absolutas (se abre desde Gmail), así
que se construyen a partir de esa variable.

El logo nw-mail-logo.png daba 404: solo existía el .webp del tema, que es el de
UltimoWoW (león negro + letras "UW"). El auténtico de NovaWoW, un león de acero
sin letras, sobrevivía únicamente en la caché de Gmail y se recuperó de ahí
(600x320 RGBA, íntegro).

Verificado renderizando el original y el nuestro en un navegador y comparando:
cuadran pixel a pixel. Eso destapó dos fallos que se corrigen aquí: faltaba la
etiqueta <h1> de apertura, y el estilo de los párrafos de datos era el de la
letra pequeña del pie (11px/17px) en vez del real (14px/21px).

El activation de Django mandaba la contraseña en texto plano; aquí no se manda.

mail.ts: leía EMAIL_USE_SSL pero ignoraba EMAIL_USE_TLS, así que en el puerto
587 el STARTTLS era oportunista y, si el servidor no lo ofrecía, nodemailer
enviaba en claro. Ahora requireTLS lo hace obligatorio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:33:32 +00:00
Inna 1da7349373 Token de seguridad y cambio de contraseña (páginas protegidas)
- lib/security-token.ts: requestSecurityToken (token de 6 chars, cooldown 7 días,
  guarda en home_securitytoken, email) + checkSecurityToken.
- lib/change-password.ts: changePassword (valida token + contraseña actual con
  authenticate, re-deriva verifier SRP6 v2, actualiza battlenet_accounts).
- Routes /api/account/security-token y /change-password (guard; el cambio hace
  logout de la sesión). Páginas /security-token y /change-password (protegidas) con
  sus forms cliente e i18n (SecurityToken, ChangePassword).

Verificado: páginas redirigen a login sin sesión, APIs 401. Reutiliza
home_securitytoken de Django y la crypto validada.

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