Commit Graph

96 Commits

Author SHA1 Message Date
Inna e00a439dd9 Cierra el flujo de login: logout, selector de cuenta y /account con guardas
- Route handlers: /api/auth/logout (destroy sesión), /api/auth/select-account
  (fija la cuenta de juego elegida, valida que pertenece a la bnet en sesión).
- app/[locale]/select-account: página SSR (guarda: sin bnetId -> login) + lista de
  cuentas de juego (SelectAccountForm cliente) -> /account.
- app/[locale]/account: página protegida (sin bnetId -> login; sin username ->
  select-account) con datos de sesión + LogoutButton.
- Textos en messages (Account, SelectAccount).

Verificado: guardas locale-aware (ES /account -> /login, EN /en/account -> /en/login,
307), logout OK, select-account sin sesión 401. Redirect i18n con next-intl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:46:42 +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
Inna 81d599ed20 Limpieza: elimina los 15 JS handlers jQuery reemplazados por las islas React
Borra los handlers de nw-js-handlers/ de los formularios ya migrados a TSX, que
ninguna plantilla referencia: login, register, change_password, change_email,
security_token, customize, change_race, change_faction, level_up, gold,
transfer_character, revive, unstuck, rename_guild, vote_points. También quita el
<script> inline de Stripe redundante en rename_guild (lo carga frontend/stripe.ts).

Se conservan los aún referenciados por páginas NO migradas o stubs: my_account
(paneles plegables), rename_response (rename_cancel), promo_code, trade_points,
recruit, store (store_response, store_novawow_response, create_check).

Verificado: check OK, la web sigue funcionando; los JS borrados dan 404 y los en
uso 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:06:31 +00:00
Inna 68cfe7594b Fase 3 (isla): recuperar ítems (restore_items) en React/TSX, flujo de 2 pasos
restore_items_view ahora responde JSON cuando la petición es AJAX (X-Requested-With):
GET con character_name -> {items}; POST recover_id -> {success, message}. Se conserva
el flujo legacy (redirect+messages) para no-AJAX. RestoreItemsForm.tsx: paso 1 elegir
personaje y buscar ítems borrados (SOAP), paso 2 elegir ítem y recuperarlo; refresca la
lista tras recuperar. La vista pasa character_names por json_script.

Verificado: check OK, render de la isla, redirige a login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:01:19 +00:00
Inna 08c36619f0 Fase 3 (isla): recuperar personaje (restore_character) en React/TSX
restore_character_view pasa de redirect+messages a JSON en POST: guid -> SOAP
.char del restore -> {success, message, redirect}. RestoreCharacterForm.tsx
(select de personajes borrados + botón) hace fetch y en éxito redirige a my-account.
La vista pasa character_data por json_script; se conserva la rama sin personajes.

Verificado: check OK, ambas ramas renderizan, redirige a login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:57:49 +00:00
Inna ff458379b0 Fase 3 (isla): historial de baneos en React/TSX
ban_history_view construye ban_rows (baneos de cuenta + personaje combinados, con
estado Activo/Inactivo y origen Cuenta/Personaje) y los pasa por json_script a la
tabla genérica HistoryTable. Sustituye la tabla Django con doble bucle.

Verificado: check OK, render de la isla con datos, redirige a login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:54:12 +00:00
Inna 230d401c75 Fase 3 (islas): historiales de seguridad y transacciones en React/TSX
Tabla genérica reutilizable HistoryTable (columnas por data-columns, filas por
json_script) + entry history_table:
- security_history: intentos de conexión (username/status/ip/fecha). Se elimina la
  caja "actividad" duplicada. Las vistas pasan login_attempts_data serializable.
- trans_history: historial de transacciones (StripeLog) con transactions_data.

Datos embebidos con json_script (sin endpoint extra). Verificado: check OK, ambas
plantillas renderizan la isla con datos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:52:20 +00:00
Inna cefa30dabc Fase 3 (isla): panel de votación (vote_points) en React/TSX
vote_points_view ya devolvía JSON en POST. VotePanel.tsx renderiza los sitios de
votación (nombre, icono, PV) y cada botón abre el sitio en otra pestaña y hace POST
del voto -> acredita PV o muestra el cooldown que devuelve el backend. La vista pasa
vote_sites_data (serializable) por json_script. Se elimina el JS jQuery y el panel
duplicado; la sección informativa se queda en Django.

Verificado: check OK, render de la isla correcto, redirige a login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:48:32 +00:00
Inna a4da7f068b Fase 3 (isla): renombrar hermandad en React/TSX
rename_guild_view ya devolvía JSON (session_id de Stripe). GuildRenameForm.tsx:
select de hermandades (donde el usuario es GM) + nuevo nombre/confirmar + contraseña
+ token -> Stripe Checkout. La vista pasa guild_names por json_script; se conserva
la rama {% if no_guild_leader_message %} en Django. Se elimina el JS jQuery muerto.

Verificado: check OK, render de ambas ramas correcto, redirige a login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:45:48 +00:00
Inna 6be04e4e37 Fase 3: migra gold, transfer, revive y unstuck a islas
- revive/unstuck (acciones SOAP gratuitas): reutilizan CharacterPurchaseForm; se
  hace el precio opcional (botón sin "por X €" cuando no hay precio). El componente
  ya maneja la respuesta sin session_id mostrando el mensaje.
- gold: variante GoldForm (personaje + cantidad de oro -> Stripe). La vista pasa
  ahora character_names y gold_options (serializables) por json_script.
- transfer: variante TransferForm (personaje + cuenta destino + token -> Stripe).
  La vista pasa character_names por json_script.

Verificado: check OK, plantillas renderizan las islas, las 4 páginas redirigen a
login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:42:35 +00:00
Inna 823b8d0014 Fase 3: migra 4 servicios de personaje más a la isla reutilizable
customize, change_race, change_faction y level_up comparten el patrón exacto de
rename (select 'character' + precio -> Stripe), así que reutilizan la isla
CharacterPurchaseForm / entry character_purchase cambiando solo los data-* (url,
label, clase de botón). Sin JS nuevo. Se eliminan los <script> jQuery y Stripe
inline de cada plantilla.

Verificado: check OK, las 4 páginas redirigen a login sin sesión y las plantillas
renderizan la isla correctamente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:35:52 +00:00
Inna 2258df4b09 Fase 3 (isla reutilizable): servicios de personaje de pago; aplica a rename
Componente genérico CharacterPurchaseForm (selector de personaje + botón con precio)
+ entry character_purchase, configurable por data-* en la plantilla. Al enviar, la
vista devuelve {session_id, stripe_public_key} y se redirige al Checkout de Stripe
(helper stripe.ts que carga Stripe.js). Reutilizable por rename/customize/race/
faction/level/gold/transfer... cambiando solo los data-* y la lista de personajes
(json_script). Primer uso: rename_character.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:33:14 +00:00
Inna b1107c0a54 Fase 3 (isla): cambiar correo en React/TSX
change_email_view ya devolvía JSON en POST (sesión requerida). ChangeEmailForm.tsx
replica el form (contraseña actual con mostrar-ocultar, correo actual/nuevo/confirmar,
token) y hace fetch form-urlencoded + CSRF a change-email; mensaje HTML. Sustituye el
<form> + change_email_response.js por #change-email-app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:29:29 +00:00
Inna d0febb4855 Fase 3 (islas): cambiar contraseña y token de seguridad en React/TSX
Ambas vistas ya devolvían JSON en POST y exigen sesión, así que sin cambio de backend:

- ChangePasswordForm.tsx: 4 campos (actual/nueva/confirmar/token) con mostrar-ocultar;
  POST form-urlencoded + CSRF a change_password; mensaje HTML; si el backend responde
  redirect:true (logout por seguridad), redirige a login.
- SecurityTokenForm.tsx: muestra la fecha del token y un botón que solicita uno nuevo;
  POST -> actualiza la fecha con token_date de la respuesta.
- Plantillas: los <form> + scripts jQuery se sustituyen por las islas
  (#change-password-app / #security-token-app) con data-csrf/data-url. El texto
  informativo y los enlaces se quedan en Django.

Verificado: check OK; ambas vistas siguen redirigiendo a login/index sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:26:30 +00:00
Inna b2069df4c3 Fase 3 (isla): dashboard de «Mi cuenta» en React/TSX
De las ~508 líneas de partials/my-account.html, solo la parte superior es dinámica
(datos de cuenta, estado, puntos, token, personajes); el resto son paneles de
navegación estáticos (enlaces), que se quedan en Django.

- API: account_me_api -> /api/account/me/ (protegido por sesión, 401 si no hay):
  user_info, account_status, dp/vp, créditos Battlepay, token de seguridad y
  personajes. Reutiliza get_account_characters/get_account_status/formatear_fecha.
  Resiliente si acore no está disponible.
- React: AccountDashboard.tsx (fieldsets de datos + estado con contador de baneo en
  vivo + rejilla de personajes). Entry my_account.
- partials/my-account.html: los 2 fieldsets + la rejilla de personajes + el script
  del contador se sustituyen por #account-dashboard-app; los paneles de utilidades
  e historiales siguen en Django.

Verificado: check OK, /api/account/me/ 401 sin sesión, my-account redirige a login.
(El render con datos reales requiere sesión + BD AzerothCore.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:23:17 +00:00
Inna cac0d28aa9 Añade Cloudflare Turnstile (captcha) a login, registro y recuperación
- Backend: helper verify_turnstile en _base (verifica el token contra
  challenges.cloudflare.com/siteverify; fail-closed ante error de red; si no hay
  secreto configurado, no bloquea). Se exige en el POST de login_view,
  register_view y recover_account_view -> rechazo con mensaje si falla.
- settings: TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY desde .env (el secreto NO
  se commitea). La clave de sitio se expone a las plantillas por el context
  processor (TURNSTILE_SITE_KEY).
- Frontend: hook useTurnstile (carga el script de Cloudflare una vez y renderiza
  el widget, devuelve el token). LoginForm/RegisterForm/RecoverForm incluyen el
  widget, mandan cf-turnstile-response y deshabilitan el submit hasta tener token.
  El sitekey llega por data-sitekey en el div de montaje.

Verificado: sin token los 3 POST se rechazan; siteverify acepta la clave secreta
(error invalid-input-response con token falso, no invalid-input-secret).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:15:27 +00:00
Inna 8920585b24 Implementa la recuperación de cuenta (contraseña / cuentas / activación) + islas
recover_account_view era un stub que solo renderizaba (los formularios no tenían
backend y el HTML arrastraba un reCAPTCHA de otro dominio). Ahora es funcional:

Backend:
- Modelo PasswordReset (token+email+expiración 1h) + migración 0014.
- recover_account_view maneja POST JSON con 3 tipos, todo por email (bnet), con
  respuestas genéricas anti-enumeración:
  * password: crea token y envía enlace de reset (emails/password_reset.html).
  * accountname: envía las cuentas de juego ligadas (emails/account_names.html).
  * activation: reenvía el enlace de activación pendiente (emails/activation.html).
- reset_password_view: GET valida el token (isla), POST re-deriva el verifier
  SRP6 v2 de la cuenta bnet (bnet.bnet_make_registration) y lo actualiza; marca el
  token usado. Ruta 'reset-password'.
- Resiliente si la BD de cuentas (AzerothCore) no está disponible: mensaje limpio,
  nunca 500.

Frontend (islas Vite): RecoverForm.tsx (selector de tipo + email) y
ResetPassword.tsx (nueva contraseña con token). Se elimina el reCAPTCHA roto; se
puede añadir uno propio si se aportan claves.

Verificado en producción: los 3 flujos devuelven éxito genérico; reset valida
token y contraseñas; token válido no provoca 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:00:42 +00:00
Inna 45f48c2799 Fase 2 (isla): selector de cuenta de juego en React/TSX, sin POST
El selector de cuenta de juego (bnet con varias) pasa a isla React y la selección
se hace vía GET (a petición), no POST:

- Nuevo endpoint GET /api/account/select/?account_id=<id> (account_select_api):
  idempotente y solo permite elegir cuentas ligadas a la bnet en sesión; fija la
  cuenta en sesión y devuelve {success, redirect}. 401 si no hay sesión bnet.
- select_account_view: se elimina el manejo de POST (queda solo la lógica GET:
  redirect si no hay bnet, autoseleccionar si hay una, render si hay varias).
- SelectAccount.tsx lee las cuentas embebidas con json_script y, al pulsar, hace
  fetch GET y redirige. Nueva entry 'select_account'.
- select_account.html: el <form> POST se sustituye por la isla + json_script.

Verificado: check OK, endpoint 401 sin sesión, /select-account/ redirige a login.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:50:37 +00:00
Inna 8156befe4f Fase 2 (isla): formulario de registro en React/TSX
register_view ya devolvía JSON en POST, así que sin cambio de backend:
RegisterForm.tsx replica register.js (password/conf con mostrar-ocultar, email/conf,
reclutador opcional, checkbox de términos que habilita el botón) y hace fetch
form-urlencoded + CSRF a {% url 'register' %}. El mensaje de éxito llega como HTML
(dangerouslySetInnerHTML); los de error como texto en rojo.

- partials/register.html: el <form> + register.js se sustituyen por #register-app
  (data-csrf/data-register-url) + {% vite_asset %}. Info-box y rama "ya conectado"
  siguen en Django.
- Nueva entry 'register' en vite.config.ts.

Verificado: check OK, la página monta la isla, POST con datos inválidos devuelve
el JSON de validación correcto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:45:00 +00:00
Inna 0909567e0d Fase 2 (isla): formulario de login en React/TSX
El login se migra a una isla React. login_view ya devolvía JSON en POST, así que
el backend no cambia: LoginForm.tsx hace fetch (form-urlencoded + CSRF) al mismo
{% url 'login' %} y replica el comportamiento del antiguo login_response.js
(mostrar/ocultar contraseña, estados del botón, éxito -> redirige a my-account,
alert/locked, errores transitorios).

- partials/login.html: el <form> + el script jQuery se sustituyen por
  #login-app (con data-csrf/data-login-url/data-success-url) + {% vite_asset %}.
  La rama "ya conectado" y los enlaces (recover, create-account) siguen en Django.
- CSRF vía data-csrf="{{ csrf_token }}" (fija la cookie y da el token); el fetch
  lo manda como X-CSRFToken y csrfmiddlewaretoken. El navegador adjunta Referer.
- Nueva entry 'login' en vite.config.ts.

Verificado: check OK, la página monta la isla, POST devuelve JSON correcto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:41:46 +00:00
Inna b8cd6bac0c Distingue WoW Classic en la etiqueta de expansión
3.4.3 (54261) es WotLK Classic, no el WotLK retail/clásico (3.3.5a=12340). La
etiqueta ahora diferencia: 12340->'WotLK', 3.4.x->'WotLK Classic',
15595->'Cataclysm', 4.4.x->'Cataclysm Classic'. Nova WoW -> 'WotLK Classic'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:35:26 +00:00
Inna 3be9a30dcc Ajusta el corte WotLK/Cataclysm Classic al build real 3.4.3 (54261)
El build de Nova WoW es 54261 (WotLK Classic 3.4.3), que caía en la franja
marcada como Cataclysm. WotLK Classic llega a 54261 y Cataclysm Classic empieza
en 4.4.0=54332, así que el corte se fija en 54261. ServerSelection -> build 54261.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:32:58 +00:00
Inna d1becaaf14 Corrige a 3.4.3 (WotLK Classic + bnet) textos y lógica de expansión
- Plantillas: los textos hardcodeados "WotLK 3.3.5a" pasan a "3.4.3" (title,
  description, keywords, og:* en head.html; encabezado de la portada).
- Expansión: se centraliza en ServerSelection (helper expansion_from_gamebuild
  + propiedad .expansion) y se reconoce WoW Classic: WotLK Classic 3.4.x
  (~44832–51536, p.ej. 3.4.3=51505) además de 3.3.5a (12340), y Cataclysm
  Classic 4.4.x además de 4.3.4 (15595). Se elimina la lógica duplicada de los
  4 sitios (api, context_processor, pages x2) que solo mapeaban 12340->WotLK.

El ServerSelection de Nova WoW queda en build 51505 (3.4.3) -> etiqueta WotLK,
puerto 1119 (bnetserver).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:31:40 +00:00
Inna d612b7916c Fase 1 (isla 2): estado del servidor de la portada en React/TSX
Segunda isla de la home: la tabla de estado (nombre, online, login, hora,
dirección) pasa de plantilla Django a componente React.

- API: home_status_api -> /api/home/status/ (server_name, address, expansion,
  online_characters, status). Reutiliza check_server_status y
  get_online_characters_count de views/pages.
- React: ServerStatus.tsx (fetch + reloj en vivo con setInterval, mismas clases
  CSS). El entry home.tsx ahora monta dos islas: #home-news-app y #home-status-app.
- partials/noticias.html: la <table> de estado se sustituye por el contenedor.

Verificado: check OK, ambos endpoints 200, la home monta las dos islas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:22:32 +00:00
Inna 98c3cbdbc6 Fase 0/1 migración a TSX: islas React con Vite + API JSON (piloto: noticias)
Arquitectura elegida: islas React/TSX montadas sobre las plantillas Django
existentes (django-vite), alimentadas por endpoints JSON bajo /api/. El backend,
las URLs y la auth por sesión se mantienen; se migra sección a sección.

Fase 0 (tooling):
- frontend/ con Vite + React 18 + TypeScript. Build a static/dist con manifest.
- django-vite 3.1 en INSTALLED_APPS + DJANGO_VITE (dev_mode=DEBUG,
  static_url_prefix='dist'). STATIC_URL pasa a '/static/' (absoluto) para que los
  assets no choquen con el <base href> de la plantilla. WhiteNoise sirve el bundle.
- .gitignore: frontend/node_modules y static/dist (artefactos de build).

Fase 1 (piloto: lista de noticias de la portada):
- API: home/views/api.py -> home_news_api (JsonResponse); ruta /api/home/news/
  montada en home/api_urls.py fuera de i18n_patterns.
- React: NewsList.tsx hace fetch de la API y renderiza las noticias (mismas clases
  CSS que el HTML original). Entry src/entries/home.tsx monta en #home-news-app.
- partials/noticias.html: el bucle {% for noticia %} se reemplaza por el contenedor
  de montaje + {% vite_asset %}.

Verificado en producción: manage.py check OK, /api/home/news/ 200, la home incluye
el mount y el <script> del bundle, y el bundle se sirve (200).

Build en deploy: cd frontend && npm run build && collectstatic (ver frontend/README.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:18:35 +00:00
Inna 1ee126a69f Despliegue en nightspire.gg: dominio real, estáticos y home resiliente
- settings: ALLOWED_HOSTS y CSRF_TRUSTED_ORIGINS a nightspire.gg /
  www.nightspire.gg (+ IP del servidor). Se retira el antiguo novawow.com.
- WhiteNoise para servir estáticos desde gunicorn (el reverse proxy Caddy
  corre como usuario 'caddy' y no puede leer /root): middleware +
  STORAGES con CompressedStaticFilesStorage. Añadido a requirements.txt.
- home: get_online_characters_count() ahora tolera que la BD de personajes
  (AzerothCore) no exista/esté vacía y devuelve 0 en vez de provocar un 500
  en la portada (mismo criterio que el context_processor get_server_info).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 20:01:43 +00:00
Inna 96ab91a82b Corrige ContentCreator.__str__ (self.titles -> self.title)
El campo se llama `title`; `self.titles` no existía y provocaba
AttributeError al representar el modelo (p. ej. en el admin de Django).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:16:22 +00:00
Inna 585285640d Divide home/models.py en el paquete home/models/ por secciones
El módulo (~310 líneas, 30 modelos) se convierte en un paquete cuyo
__init__.py re-exporta todos los modelos, de modo que `from home.models
import X`, el admin y el resto de consumidores siguen funcionando igual.

Módulos por sección: pricing (precios/costes de servicios + GuildRenameSettings),
store (Category, Item, StripeLog, Pedido), site (contenido/config del sitio),
recruit, accounts (activación, token de seguridad, LoginAttempt), voting
(VoteSite, VoteLog, HomeApiPoints) e history. Las claves foráneas por clase
directa quedan en el mismo módulo; cada submódulo importa solo lo que usa.

Se eliminan imports de cabecera muertos (ValidationError, uuid, secrets) y las
líneas de import duplicadas del original.

Migración-neutral: `makemigrations home --dry-run` -> "No changes detected"
(app_label=home y nombres/tablas de modelo intactos). Definición de cada
modelo idéntica (extracción por AST). `manage.py check` sin incidencias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:15:12 +00:00
Inna b3184fbdfe Divide home/views.py en el paquete home/views/ por secciones
El módulo monolítico (~3200 líneas) se convierte en un paquete cuyo
__init__.py re-exporta todas las vistas, de modo que `views.X`,
`from home.views import X` y la cadena 'home.views.not_found' (handler404)
siguen funcionando sin tocar urls.py.

Módulos por sección: _base (imports comunes + require_paid_stripe), auth,
pages, account, recruit, characters, store, points, guild, history,
players y payments. Cada submódulo hereda los imports con
`from ._base import *`; las dependencias entre secciones se resuelven con
imports explícitos (guild->auth, recruit/history->account).

Se eliminan dos definiciones duplicadas que quedaban muertas al cargar:
- get_character_image (primera copia, idéntica a la efectiva)
- store_novawow_success_view (stub sin SOAP, pisado por la versión real)

Validado con `manage.py check` (0 issues), resolución de las 100 rutas y
un chequeo estático de nombres no definidos por módulo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:09:40 +00:00
Inna 758435fe77 Corrige bugs de la revisión de home/views.py (dinero, bypass de pago, 500s)
Seguridad / dinero:
- store_novawow_view: recalcula precios y valida ítems contra la BD (Item) en vez
  de confiar en el precio/ítems que manda el cliente; guarda en sesión un carrito
  ya validado para la entrega
- *_success_view (x9): @require_paid_stripe verifica el pago real en Stripe por
  session_id (Stripe lo añade a la success_url) y marca StripeLog.fulfilled para
  evitar re-entregas; cierra el bypass de ir directo a la success-url sin pagar
- gold_character_view: deja de truncar el precio (int(4.99)->4); pasa el precio real

Correctitud / 500s:
- change_password_view: .get(...,'') evita AttributeError si falta un campo
- revive_character_view: lógica de éxito corregida (vacío=éxito, no vacío=error,
  None=fallo de comunicación) — antes marcaba error como éxito y caía a HTML
- store category y recruit_a_friend: guardas para Category.DoesNotExist y cuenta None

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:56:20 +00:00
Inna faa9aabdbf Corrige security_token_view: bug de indentación que impedía generar el token
- 'if not email:' estaba a 5 espacios, anidando todo el bloque bajo él: con un
  email válido se saltaba la generación del token (solo re-renderizaba) y el
  código de creación quedaba inalcanzable
- re-indentado para que la generación del token cuelgue de 'if request.method == POST'

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:44:26 +00:00
Inna b5d46fad5c Activa CSRF global en el portal (CsrfViewMiddleware)
- añade django.middleware.csrf.CsrfViewMiddleware a MIDDLEWARE (estaba ausente:
  el CSRF estaba desactivado en todo el sitio)
- head.html: $.ajaxSetup envía X-CSRFToken en todo POST AJAX de jQuery + expone
  window.CSRF_TOKEN (fuerza la cookie csrftoken en cada página)
- store_novawow.html: el fetch() nativo ahora manda X-CSRFToken (era el único que
  se rompía al activar el middleware)
- register.html: añade {% csrf_token %} al formulario
- retira @csrf_exempt de login/register/restore_character/restore_items
  (solo los webhooks stripe/sumup siguen exentos)
- auditoría previa: el resto de POST (AJAX jQuery + forms nativos con token) ya cubierto

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:42:34 +00:00
Inna 89ea16ba4d Foro: correcciones de la revisión de seguridad y correctitud
- CSRF real en las vistas mutadoras con @csrf_protect (el middleware global no
  estaba activo; verificado: POST sin token -> 403). Se documenta el hueco global.
- bloquea acceso a temas de foros ocultos/borrados por URL directa (view_topic y reply)
- conteo de posts respeta borrados para moderadores (paginación correcta)
- tema bloqueado impide editar/borrar posts a no-moderadores (_topic_locked_for)
- moderador ve el formulario de respuesta en temas bloqueados (can_reply)
- filtro forum_safe: sanea el HTML también al renderizar (defensa en profundidad)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:34:03 +00:00
Inna 071ba031a1 Foro: ocultar contador de visitas cuando la columna no existe
- corrige que se mostrara '1 visitas' sin la columna forum_topics.views:
  el incremento y la visualización solo ocurren si has_views_column() es true

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:25:53 +00:00
Inna eb93758c43 Foro: paginación en la búsqueda y contador de visitas por tema
- búsqueda paginada (count_search_topics + offset/limit) con controles en la plantilla
- contador de visitas: columna opcional forum_topics.views con detección automática
  (has_views_column), increment_views al ver el tema, mostrado en lista y cabecera
- sql/forum_views.sql (ALTER opcional) + docs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:22:05 +00:00
Inna fd49d6cd96 Foro: buscador de temas y nombre de autor legible
- forum_search: busca temas por título y contenido (foros visibles)
- db.get_display_name: nombre visible del autor = parte local del email de la cuenta
  (en vez de <bnetId>#1), usado en el panel de posts y la lista de temas
- buscador en la portada del foro + plantilla search.html
- docs/FORO.md actualizado

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:18:34 +00:00
Inna 874a0c1ad9 Foro: editor CKEditor 5 en los mensajes y moderación por POST+CSRF
- forum/forms.py: TopicForm/ReplyForm/EditPostForm con CKEditor5Widget (rich text)
- vistas crear/responder/editar pasan el form; el HTML del editor se sanea con nh3
- acciones de moderación (bloquear, fijar, mover, borrar/restaurar temas y posts)
  ahora exigen POST y van con CSRF; las plantillas usan formularios en vez de enlaces GET
- docs/FORO.md actualizado

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:15:35 +00:00
Inna 2c30844bf7 Añade foro de comunidad integrado (app forum) con el diseño de NovaWoW
- app 'forum': portada (categorías/foros), ver foro y tema (paginado), crear tema,
  responder, editar/borrar posts y moderación (fijar, bloquear, mover, borrar/restaurar)
- lee/escribe la BD acore_web (5ª conexión, DB_NAME_WEB) por SQL directo, tablas
  forum_categories/forums/forum_topics/forum_posts/forum_reads (portado de NovaWeb-main)
- identidad = cuenta de juego en sesión; permisos simplificados por gmlevel
  (FORUM_MOD_GMLEVEL) en vez de la matriz group_level×permission del original
- saneado del HTML de posts con nh3 (evita el XSS del original); POST+CSRF en formularios
- plantillas con los partials del sitio; enlace 'FOROS' del menú apunta al foro interno
- sql/forum_schema.sql y docs/FORO.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:10:55 +00:00
Inna 2162fe34d0 Muestra el email Battle.net como nombre visible en vez de <bnetId>#1
- header: saludo con bnet_email (fallback al username de juego)
- my-account: 'Cuenta (Battle.net)' = email y 'Cuenta de juego' = <bnetId>#1
- session['username'] se mantiene intacto para las consultas a la BD

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:52:45 +00:00
Inna 4edd08cd1a Mi cuenta: enlace a Battlepay y saldo de créditos bnet
- my_account: añade battlepay_credits (leído de battlenet_accounts.battlePayCredits)
- bnet.get_battlepay_credits() defensivo (0 si la columna no existe)
- partials/my-account.html: muestra 'Créditos Battlepay' y añade botón 'Tienda del cliente' que enlaza a /es/battlepay/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:51:03 +00:00
Inna 0fef9ac35b Integra Battlepay con SumUp (órdenes battlepay_orders)
- pagos_sumup.py: crear_checkout_battlepay(reference,...) y obtener_checkout()
- battlepay_view: lista las órdenes PENDING de la cuenta de juego
- battlepay_pay_view: crea checkout SumUp para una orden concreta (checkout_reference=reference)
- sumup_webhook: marca la orden PAID (valida estado contra la API de SumUp si falta)
- plantillas account/battlepay.html y partials/pago_sumup.html
- sql/battlepay_sumup.sql (tablas battlepay_orders/battlepay_price) y docs actualizados

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:48:16 +00:00
Inna 215061776b Adapta el portal a 3.4.3 (cuentas Battle.net + selector de cuenta de juego)
- home/bnet.py: SRP6 v2 (PBKDF2-HMAC-SHA512) para battlenet_accounts y SRP6
  Grunt (SHA1) para las cuentas de juego, con helpers bnet<->game account
- login por email contra battlenet_accounts; selector de cuenta de juego
  cuando la cuenta bnet tiene varias (my_account redirige al selector)
- registro/activación crean battlenet_accounts + account enlazada (<bnetId>#1,
  battlenet_account/battlenet_index)
- cambio de contraseña sobre battlenet_accounts; cambio de email recalcula
  salt/verifier (el usuario SRP depende del email)
- AccountActivation: identidad por email (username/salt/verifier opcionales) + migración
- plantillas login/register por email + nueva plantilla del selector
- docs/MIGRACION_3.4.3.md con el detalle y los pasos de verificación

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:43:11 +00:00
Inna b43b888f53 Añade README de despliegue y soporte Docker
- README.md: guía de instalación (venv, .env, migraciones, gunicorn) y notas de producción/seguridad
- Dockerfile: imagen Python 3.14 con deps de sistema para mysqlclient/Pillow
- docker-compose.yml: servicios web (Django+gunicorn) y db (MySQL 8.4) con las 4 BBDD
- docker/entrypoint.sh: espera a MySQL, migra, collectstatic y arranca gunicorn
- docker/mysql-init.sql: crea django_wow + bases acore_*
- .dockerignore y gunicorn en requirements

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:16:29 +00:00
Inna f787b0d06e Mueve secretos a variables de entorno (.env)
- settings.py: SECRET_KEY, DEBUG, credenciales MySQL (x4 BBDD), SOAP,
  SumUp, Stripe y SMTP ahora se leen de variables de entorno via python-dotenv
- añade .env.example como plantilla y python-dotenv a requirements
- .gitignore: ignora .env
- verificado: manage.py check OK y settings cargan valores desde .env

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:01:57 +00:00
Inna e1c04a92e3 Actualiza dependencias a las últimas versiones (Django 6.0.7, Python 3.14)
- Django 4.2.17 -> 6.0.7
- stripe 11.4.1 -> 15.3.0, pillow 11 -> 12, django-extensions 3.2.3 -> 4.1
- resto de paquetes actualizados a su última versión
- verificado: manage.py check, makemigrations --check y arranque WSGI OK
- .gitignore: ignora .venv/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:58:45 +00:00
adevopg 9c92778816 Producion 2025-03-14 19:03:51 +01:00