Commit Graph

110 Commits

Author SHA1 Message Date
Inna d5b00a86dc Panel admin propio: gestión de noticias
- lib/admin.ts: isAdmin(session) por allowlist ADMIN_EMAILS (normalizada) o gmlevel
  de AzerothCore (account_access, resiliente si no existe). ADMIN_EMAILS/ADMIN_GMLEVEL
  en .env.local.
- lib/admin-news.ts: listNews / createNews / deleteNews (home_noticia).
- Routes /api/admin/news (POST, guard isAdmin) y /api/admin/news/[id] (DELETE).
- Páginas /admin (dashboard) y /admin/news (protegidas: no-admin -> redirect).
  AdminNewsManager (cliente: crear + listar + borrar).

Verificado: /admin sin permiso redirige, APIs 403; el pipeline crear noticia -> se
renderiza en la home (SSR) funciona. Turnstile bloquea el login por curl (correcto).
Pendiente admin: precios de servicios, foros, sitios de voto, usuarios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:55:34 +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 e743d98777 Foro (lectura): índice, temas y mensajes en Next.js
- lib/forum.ts: getForumIndex (categorías + foros visibles con contadores y último
  tema, port de forum/db.py), getForum/getTopics, getTopic/getPosts (acore_web).
- Páginas app/[locale]/forum (índice), /forum/[forumId] (temas), /forum/topic/[topicId]
  (mensajes; render del HTML ya saneado por Django). Enlace "Foros" en la cabecera.
- Catálogo Forum + Nav.forum.

Verificado: /forum 200, foros/temas inexistentes 404, enlace en la cabecera.
Solo lectura; pendiente escribir (crear tema/responder + saneado nh3->TS) y moderación.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:45:39 +00:00
Inna cceab26999 Cambio de email con doble confirmación (completa el bloque de auth)
- lib/change-email.ts: requestEmailChange (valida pass/email/token, crea activación
  con old_email/old_email_hash, email al correo actual), confirmOldEmail (marca is_used,
  email al nuevo), confirmNewEmail (re-deriva verifier con el nuevo email + actualiza
  battlenet_accounts y account; borra la activación).
- components/ConfirmClient.tsx: cliente genérico que confirma un hash por POST al abrir.
- Routes /api/account/change-email, /confirm-old-email, /confirm-new-email.
- Páginas /change-email (protegida) y /confirm-old-email, /confirm-new-email (auto).
- Catálogos ChangeEmail, ConfirmOldEmail, ConfirmNewEmail.

Verificado: change-email protegida (401/redirect), confirmaciones con hash inválido
-> invalidLink sin crash. AUTH 100% reimplementado en Next.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:41:42 +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
Inna 548fca16e6 Añade gold y transfer: patrón de pago unificado con fulfill + metadata
- lib/stripe.ts: createCheckoutSession acepta metadata; claimPaidCheckout la
  recupera de la sesión Stripe y la devuelve.
- lib/paid-services.ts: config unificado con fulfill(character, meta) + extraFields.
  gold -> UPDATE characters money (BD directa, no SOAP), transfer -> SOAP
  .char changeaccount. Los 5 simples usan soapFulfill.
- checkout [service]: lee extraFields -> metadata, precio depende de metadata (gold),
  valida precio>0. service-success llama cfg.fulfill(name, metadata).
- prices.ts: getTransferPrice, getGoldOptions/goldPriceFor (home_goldprice).
- GoldForm (personaje + cantidad) y TransferForm (personaje + destino); páginas
  /gold y /transfer. Catálogos Paid.gold/transfer.

Verificado: /gold /transfer redirigen a login, checkout 401 sin sesión, home OK.
NOTA: transfer no valida aún el token de seguridad ni reglas AC (nivel>=55, DK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:32:12 +00:00
Inna 87f9b0b003 Generaliza los servicios de personaje de pago (5) con infra común
- lib/paid-services.ts: config PAID_SERVICES (rename/customize/change-race/
  change-faction/level-up) con comando SOAP, precio y productName por servicio.
- app/api/character/[service]/checkout: ruta de checkout GENÉRICA (valida servicio +
  personaje, crea la sesión Stripe con successUrl /service-success?service=...).
- app/[locale]/service-success: página de éxito ÚNICA que verifica el pago
  (claimPaidCheckout) y ejecuta el comando SOAP del servicio.
- components/ServicePageContent: contenido común (guardas + PaidServiceForm).
- 5 páginas mínimas /rename /customize /change-race /change-faction /level-up.
- Catálogo Paid (por servicio: title/pay/success) sustituye a Rename.

Verificado: 5 páginas redirigen a login, checkout 401 sin sesión, servicio inválido
404, service-success con pago inválido no entrega (muestra error). Pendiente: gold
(cantidad) y transfer (destino+token), variantes del patrón.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:25:57 +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 96e996e9c2 Cliente SOAP + servicios de personaje SOAP (revive/unstuck) en Next.js
- lib/soap.ts: executeSoapCommand (port de ac_soap.py; POST SOAP + basic auth al
  worldserver, timeout 5s, null si inaccesible). AC_SOAP_* en .env.local.
- lib/characters.ts: getGameCharacters / characterIsOfflineAndOwned (acore_characters).
- lib/character-services.ts: soapCharacterAction genérico (valida propiedad+offline,
  cooldown 12h vía home_revive/unstuckhistory, SOAP, registra historial) ->
  reviveCharacter / unstuckCharacter.
- components/CharacterActionForm.tsx (cliente reutilizable: select + botón).
- Routes /api/character/revive|unstuck (guard de sesión) y páginas
  app/[locale]/revive|unstuck (SSR, guardas, i18n). Namespace Services.

Verificado: API sin sesión 401, páginas redirigen a login, cliente SOAP devuelve null
limpio sin worldserver. Patrón base para el resto de servicios de personaje.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:16:32 +00:00
Inna a0a64bc70a Dashboard de /account: info, puntos, créditos, token y personajes
- lib/account.ts: getAccountDashboard(session) lee puntos (home_api_points),
  créditos battlepay (battlenet_accounts), estado del token de seguridad
  (home_securitytoken) y personajes (acore_characters). Resiliente si las BD de
  AzerothCore no están pobladas (devuelve vacíos/0).
- app/[locale]/account: página protegida enriquecida (tarjetas de cuenta + puntos +
  rejilla de personajes con oro), i18n. Namespace Account ampliado.

Verificado: build OK, guarda sigue redirigiendo a login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:11:57 +00:00
Inna 3573b7eb40 Recuperación de cuenta en Next.js (3 flujos + reset), con Turnstile
- lib/recover.ts: requestRecovery(type,email) -> password (crea token en
  home_passwordreset + email), accountname (email con las cuentas), activation
  (reenvía enlace); respuesta genérica anti-enumeración. resetPassword(token,pass)
  re-deriva el verifier SRP6 v2 (bnetMakeRegistration) y actualiza battlenet_accounts,
  marca el token usado.
- Routes /api/auth/recover (Turnstile) y /api/auth/reset-password.
- Páginas app/[locale]/recover (RecoverForm: selector de tipo + email + Turnstile) y
  reset-password (ResetForm con token de la URL). Catálogos Recover, Reset.

Verificado: recover sin captcha -> captchaFailed; reset token inválido -> invalidLink;
páginas 200. Reutiliza home_passwordreset de Django. Crypto de reset ya validada.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:07:16 +00:00
Inna aa61f79a55 Cloudflare Turnstile en login y registro (Next.js)
- lib/turnstile.ts: verifyTurnstile(token, ip) contra siteverify (fail-closed).
- components/Turnstile.tsx: widget cliente (carga el script de Cloudflare, callback
  con el token). Site key por NEXT_PUBLIC_TURNSTILE_SITE_KEY; secreto server-only.
- LoginForm/RegisterForm: widget + token en el POST; submit deshabilitado hasta
  resolver el captcha (si hay site key).
- Routes login/register: verifican el token antes de continuar (error captchaFailed).
- Catálogos: captchaFailed.

Verificado: POST sin token -> captchaFailed en login y registro (enforcement server).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:04:22 +00:00
Inna 4a783d165f Layout visual: cabecera con nav/sesión/idioma + pie, tema Tailwind
- components/Header.tsx (server, lee sesión): marca, nav (Inicio; login/register o
  account/logout según sesión), LanguageSwitcher y LogoutButton.
- components/LanguageSwitcher.tsx (cliente): cambia de idioma conservando la ruta
  (next-intl usePathname/useRouter). components/LogoutButton.tsx (compartido).
- components/Footer.tsx. app/[locale]/layout.tsx envuelve children con Header/Footer
  (columna min-h-screen). Se unifica el LogoutButton (se borra el de account/).
- messages: namespace Nav.

Verificado: home ES/EN con cabecera+nav+idioma+pie; enlaces locale-aware (/en/login);
textos por idioma. Al leer sesión en el header, todas las rutas pasan a dinámicas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:57:00 +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 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