- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
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>
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>
- '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>
- 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>