12 Commits

Author SHA1 Message Date
Inna 297257b753 Armería/Armory: buscador, ficha de personaje con equipo y visor 3D
Nueva sección Armería (menú Jugadores del header, pública). Migra la idea del
módulo armory de FusionCMS a Next, ajustada al diseño del sitio y en ES/EN.

DATOS (verificado antes de construir): item_template no existe en 3.4.3, pero los
datos de ítem ya están en MySQL (django_wow.item_data, 44.873 filas con nombre
es/en, calidad, item_level, inventory_type y display_id). El equipo del personaje
sale de acore_characters (character_inventory + item_instance, con transmog) y se
resuelve contra item_data en dos pasos (sin joins entre esquemas). lib/armory.ts +
lib/wow-data.ts (razas/clases/slots ES/EN y colores de clase).

RUTAS:
- /armory: buscador con pestañas personajes/ítems/hermandades (SSR por searchParams),
  ítems con tooltip/color/icono de wowhead.
- /armory/character/[guid]: identidad, equipo (tooltips wowhead, color por calidad)
  y visor 3D.
- /armory/guild/[guid]: miembros con enlace a su ficha.

VISOR 3D (components/ArmoryModel3D): usa la librería wow-model-viewer (generateModels)
con jQuery + ZamModelViewer de wowhead cargados bajo demanda, alimentado con
raza+género+display_ids del equipo (transmog si lo hay). Es integración cliente que
depende del CDN de zamimg y NO se puede verificar sin navegador: va todo en try/catch
con temporizador de reserva y aviso si no carga, para no romper la ficha.

Verificado en producción (salvo el render 3D): buscador, ficha con equipo real
(«Camisa de acólito»/«Acolyte's Shirt»), tooltips de wowhead y página de hermandad,
en ES y EN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:29:02 +00:00
Inna af707ad98c Foro: reemplazar el foro anterior por el port del foro estilo FusionCMS
Se sustituye por completo el foro anterior de Next (que estaba vacío: 0 temas, 0
posts) por una adaptación del foro PHP de FusionCMS que tenía el usuario, con su
estructura, sus imágenes y su estética, llevadas a Next.

BASE DE DATOS (acore_web), esquema idéntico al original (sql/forum_fusion.sql):
  forum_categories(id, order, name)
  forum_forums(id, category, order, name, description, icon, colortitle, type)
  forum_topics(id, forum, name, sticky, locked, deleted, created)
  forum_posts(id, topic, poster, text, time, deleted)
Se hizo DROP de las tablas antiguas (forums, forum_categories, forum_topics,
forum_posts) y CREATE de estas, con el seed original (News/Reports/General,
foros con icono, clases con color, subforos por idioma con bandera). Única
diferencia con el dump: forum_topics.created, que faltaba y usan las vistas, y
el recorte del salto de línea final en los nombres de clase.

Los temas no guardan autor ni última actividad: se derivan del primer/último post,
como en el original. `poster` es el AccountID de acore_auth; el nombre se resuelve
al render (username sin el sufijo #N de Battle.net). El texto se guarda como HTML
saneado con nh3 (no BBCode) para encajar con el editor.

EDITOR: TinyMCE community self-hosted (licenseKey gpl), servido desde /tinymce
(scripts/copy-tinymce.mjs en postinstall; public/tinymce en .gitignore por ser
artefacto). El HTML se sanea SIEMPRE en el servidor.

PÁGINAS (app/[locale]/forum): índice con los 3 tipos de foro (fila normal, tarjeta
de clase tipo 1, tarjeta de idioma tipo 2), subforo, tema, crear y editar. La
lógica de forum.js (crear/responder/editar/moderar) se portó a fetch + los avisos
del sitio, sin jQuery/SweetAlert, y la moderación pasa por POST (no GET).

CSS: forum.css adaptado al tema (app/forum.css), rutas de imagen a /forum, dorado
alineado al acento del sitio (#d79602), y se aportan .nice_button/.main-wide/
.pagination que no estaban en theme.css.

ADMIN: lib/admin-forum.ts, sus rutas y AdminForumManager reescritos al esquema
nuevo (categoría, icono, color, tipo, orden; sin _en/visibility).

Se eliminan /forum/search y /forum/user (no existen en el foro nuevo) y los
componentes que quedaban huérfanos (ForumSearchBox, PostActions, TopicModBar,
NewTopicForm). i18n ES/EN completado (next-intl).

Verificado en producción: el índice renderiza en es/en con categorías, clases,
banderas e iconos; los assets y TinyMCE sirven 200; crear tema y responder guardan
las filas con el esquema correcto y las páginas las muestran (autor resuelto, HTML
saneado). Datos de prueba borrados: el foro arranca vacío.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 12:22:08 +00:00
Inna 94f9731b2c Google Analytics 4, con interruptor y respetando el consentimiento
Se añade GA4 (gtag.js) con el componente oficial `@next/third-parties`, que es lo
que recomienda el doc de Next 16 y carga el mismo gtag pero tras la hidratación.

DOS interruptores:
1. `NEXT_PUBLIC_GA_ID` en el .env (no versionado). Vacío o sin poner = no se carga
   nada. Documentada en el README.
2. El consentimiento del visitante. El sitio YA tenía un banner con la categoría
   «Analíticas» que se puede rechazar: soltar gtag en el <head> lo habría
   convertido en mentira, y con visitantes en la UE, en un problema legal.

Para lo segundo se expone `useConsent(categoría)` desde CookieConsent, que además
reacciona en caliente: `writeConsent`/`revokeConsent` ya disparaban el evento
`ns:consentchange`, así que la analítica entra en cuanto se acepta y desaparece
si se revoca, sin recargar. Arranca en `false` a propósito: hasta confirmar el
consentimiento no se carga nada.

Verificado en el build: sin consentimiento no hay ni rastro de googletagmanager
en el HTML, y la condición compilada es `id && consent ? <GoogleAnalytics/> : null`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:49:00 +00:00
Inna 4e089b5ebb Añadir ruta /security-2falogin (verificación en 2 pasos / TOTP)
Activación de 2FA (TOTP) para el login del juego, guardando el secreto en
acore_auth.account.totp_secret tal como lo lee el bnetserver 3.4.3.

- lib/two-factor.ts: TOTP (HMAC-SHA1, 30s, 6 dígitos, ±1) idéntico al core
  (verificado contra los vectores RFC 6238), Base32, otpauth, y codificado
  del secreto: crudo por defecto o AES-128-GCM (ciphertext‖IV12‖tag12) si se
  define TOTP_MASTER_SECRET (mismo hex que TOTPMasterSecret del servidor).
- Flujo seguro sin bloqueos: el secreto se genera y queda PENDIENTE en la
  sesión; sólo se escribe en la cuenta tras verificar un código válido.
- /api/account/2fa: activar (password + token de seguridad), verificar y
  desactivar (con código actual). QR generado en el servidor (el secreto no
  sale a terceros).
- Página + componente fieles al markup (preview, pasos, copiar clave, ojos).

Verificado E2E: activar -> QR/secreto -> verificar -> totp_secret = 20 bytes
(igual al secreto escaneado) -> desactivar -> NULL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:37:13 +00:00
Inna 239392b876 Foro (escritura): crear tema y responder, con saneado HTML
- lib/forum-sanitize.ts: cleanPostHtml con sanitize-html (misma allowlist que
  forum/sanitize.py de nh3: formato básico + a/img/tablas, rel nofollow, esquemas
  http/https/mailto). Verificado XSS-safe (script/onclick/javascript: eliminados).
- lib/forum-write.ts: createTopic (tema + primer post), createPost (respuesta +
  updated_at), forumIsPostable / topicIsReplyable.
- Routes /api/forum/topic y /api/forum/reply (guard de sesión; identidad = cuenta de
  juego: poster=username, poster_id=accountId). Componentes NewTopicForm y ReplyForm
  (clientes). Se muestran solo si hay sesión; el tema cerrado no admite respuesta.

Verificado: escritura 401 sin sesión, saneado correcto. Pendiente: editar/borrar,
moderación (fijar/cerrar/mover), búsqueda, editor enriquecido.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:49:53 +00:00
Inna e7cff56954 Servicios de personaje de pago (Stripe): patrón + rename
- lib/stripe.ts: createCheckoutSession (crea sesión Stripe + StripeLog en
  home_stripelog, success_url con {CHECKOUT_SESSION_ID}), isSessionPaid,
  claimPaidCheckout (verifica pagado + no entregado, marca fulfilled -> evita
  re-entregas; port de require_paid_stripe).
- lib/prices.ts: precios desde home_*price (rename/customize/race/faction/level).
- components/PaidServiceForm.tsx: selector + botón de pago -> redirige a la URL de
  Checkout de Stripe (session.url, sin stripe.js).
- Rename: route /api/character/rename/checkout (guard + valida personaje + crea
  checkout), página /rename (PaidServiceForm) y /rename/success (verifica pago y
  ejecuta SOAP .char rename, i18n). STRIPE_* + NEXT_PUBLIC_STRIPE_PUBLIC_KEY en
  .env.local. Namespace Rename.

Verificado: checkout sin sesión 401, /rename redirige a login, /rename/success con
session_id inválido no entrega el servicio (muestra error). Patrón para los demás
servicios de pago (cambiar precio + comando SOAP).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:21:30 +00:00
Inna f332fc39ca 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>
2026-07-12 22:53:09 +00:00
Inna 60c2e44afd Login end-to-end en Next.js: iron-session + auth SRP6 + página i18n
- lib/session.ts: sesión cifrada httpOnly con iron-session (SESSION_SECRET en
  .env.local). SessionData {bnetId, bnetEmail, username, accountId}.
- lib/auth.ts: authenticate(email,password) verifica contra battlenet_accounts con
  bnetVerify (SRP6 v2); getGameAccounts(bnetId). Resiliente si acore no está.
- app/api/auth/login/route.ts: POST -> autentica, fija sesión, resuelve cuentas de
  juego (1 -> auto; 0/varias -> needsSelection). Devuelve JSON.
- app/[locale]/login: página SSR + LoginForm (cliente, next-intl, router i18n).
  Textos en messages/*.json (namespace Login).
- tsconfig target ES2020 (literales BigInt de bnet.ts).

Validado end-to-end: con una cuenta creada por bnet.py (como haría AzerothCore),
el login TS la verifica OK y emite la cookie de sesión; password incorrecta ->
invalidCredentials; sin campos -> missingFields. Página en ES y EN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:42:23 +00:00
Inna 10ba2d32df Port de bnet.py a TypeScript (SRP6 v2 + Grunt), validado equivalente a Python
lib/bnet.ts reimplementa la criptografía Battle.net de home/bnet.py con BigInt
nativo + node:crypto:
- SRP6 v2 (battlenet_accounts): PBKDF2-HMAC-SHA512, N 2048 bits, g=2, ajuste de bit
  alto, módulo estilo Python, verifier little-endian (ToByteVector).
- SRP6 Grunt/SHA1 (cuenta de juego): g=7, N 256 bits, verifier 32B little-endian.
- bnetMakeRegistration/bnetVerify, gameCalculateVerifier/gameMakeRegistration/
  gameVerify, bnetSrpUsername, normalizeEmail, makeGameAccountUsername.

Validado con vectores cruzados contra la impl. Python (mismos email/pass/salt ->
mismo verifier; verificación en ambos sentidos): TODO COINCIDE. Se añade tsx (dev)
para ejecutar scripts TS (Node del sistema no trae strip-types).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:36:00 +00:00
Inna 032bd0020b i18n con next-intl: raíz multilenguaje (es por defecto, /en) + textos externalizados
- next-intl 4 (compatible Next 16): routing (es default en RAÍZ sin prefijo, en con
  /en, localePrefix 'as-needed'), middleware, request config, navigation helpers.
- Estructura app/[locale]/ (layout con <html lang>, NextIntlClientProvider, metadata
  SEO por idioma vía generateMetadata; page home con getTranslations).
- Catálogos messages/es.json y en.json: TODOS los textos de la UI (nada hardcodeado
  en los .tsx, se usan con t()).

Verificado: / -> español (lang=es, título ES), /en -> inglés (lang=en, título EN),
datos SSR desde MySQL. Añadir idioma = locale en routing + messages/<loc>.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:31:27 +00:00
Inna c36b543184 Full-stack Next.js: la home lee MySQL directamente (sin Django)
Pivote a Next.js full-stack (Django saldrá del todo en el cutover). Next accede a
las mismas BD MySQL que Django:

- lib/db.ts: pools mysql2 (django_wow + acore_*), singleton en globalThis, creds
  en web-next/.env.local (gitignored, copiadas del .env de Django).
- lib/home.ts: getNoticias (home_noticia), getServerStatus (home_serverselection +
  online desde acore_characters + TCP check + expansión por gamebuild, incl. Classic).
- app/page.tsx: home SSR que llama a lib/home (ya NO a la API Django). Se elimina
  lib/api.ts.

Verificado: con el servicio Django PARADO, la home de Next sigue sirviendo 200 con
datos reales -> lee MySQL directo, Django fuera del circuito.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:23:12 +00:00
Inna 478a3bb203 Fase 0 SPA/SSR: scaffold Next.js 16 (App Router) + Tailwind v4 + home SSR
Inicia la reescritura del frontend a una app Next.js con SSR (Django pasará a ser
API JSON). No toca producción: Django sigue sirviendo www; Next corre aparte en :3001.

- web-next/: Next.js 16 App Router, TypeScript, Tailwind CSS v4, alias @/*.
- lib/api.ts: cliente de la API Django (SSR -> 127.0.0.1:8001; navegador -> /api
  relativo vía Caddy).
- app/page.tsx: home con SSR por petición (force-dynamic) que hace fetch de
  /api/home/news/ y /api/home/status/ y renderiza noticias + estado con Tailwind.
- app/layout.tsx: lang=es + SEO (title/description/keywords/OpenGraph) por el
  Metadata API de Next (renderizado en servidor).
- Servicio systemd novawow-next (next start -p 3001, DJANGO_API_BASE).

Nota Next 16 (breaking): params/searchParams son Promise (await); fetch no se
cachea por defecto. Verificado: SSR sirve HTML con datos reales de Django y SEO
en el <head>; producción Django intacta.

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