Commit Graph

52 Commits

Author SHA1 Message Date
Inna eaae16e33a web-next: change-race/faction/level-up/gold por SumUp + rastreador de misiones
- /change-race-character (3€), /change-faction-character (5€, valida condiciones:
  sin hermandad/correo/subastas/capitán-arena y oro <= cap por nivel),
  /level-up-character (10€), /gold-character (oro por correo SOAP). Cada uno borra
  su ruta antigua y actualiza el link del panel.
- SumUp: hook precheck en PaidServiceConfig (condiciones cambio de facción);
  columna home_stripelog.metadata para campos extra (gold_amount).
- /quest-character: rastreador de estado de misiones/cadenas (clase, Hijos de
  Hodir, misión por ID) en personaje offline; cooldowns por categoría, Turnstile,
  IDs de misión verificados en wotlkdb 3.3.5a. Tabla home_questsearchhistory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:39:28 +00:00
Inna f7a376bd7c web-next: rename-character/customize-character por SumUp + renombrado de rutas
- /rename-character y /customize-character: renombrar/personalizar pagados por SumUp
  (getRenamePrice/getCustomizePrice). SumUp ahora es service-aware: columna
  home_stripelog.service; createSumUpCheckout guarda service+character;
  fulfillSumUpCheckout despacha a la acción del servicio (.char rename/customi) o,
  sin service, acredita PD. [service]/checkout acepta provider:'sumup';
  PaidServiceForm acepta provider+confirmText; service-success maneja el return SumUp.
  Se eliminan las antiguas /rename y /customize (Stripe).
- /revive-character (comparte ReviveServiceContent); se elimina /revive.
- Renombres de ruta + todos sus enlaces:
  /account -> /my-account, /recruit -> /recruit-a-friend, /unstuck -> /unstuck-character.
  (/select-account y /api/account/* intactos.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:56 +00:00
Inna 6ca94c2803 web-next: selector de personaje coloreado + rediseño unstuck + fixes vote/account-info
- CharacterSelect: componente reutilizable que colorea las opciones por clase
  (class="priest big-font"…) y el <select> con la clase seleccionada. Usado en
  unstuck, revive, gold, transfer, trade-points, servicios de pago y recruit.
  getGameCharacters ahora devuelve classCss.
- /unstuck (+revive): diseño del original (info + NOTA, prompt, opciones coloreadas,
  botón "Desbloqueado"/"Revivido" con refresh).
- /vote-points: seed de los 4 sitios con logos y URLs (sql/seed_votesites.sql);
  botón voted-button para sitios en cooldown; etiqueta "Nota:" separada.
- account-info.png (imagen nueva) y CSS de los paneles de cuenta: se añade
  background-blend-mode: saturation y background-size en #account-settings y
  .account-fieldset para que la imagen salga como el original.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:55:43 +00:00
Inna b9803adeb9 web-next: rediseño de security-token, recover, vote-points + rename-guild
- /security-token: diseño completo del original (info + advertencia + NOTA 7 días),
  fecha en formato HH:MM:SS DD-MM-YYYY, botón "Token enviado", enlace a /recover.
- /recover: 4 opciones (contraseña por usuario, nombre de cuenta, token de seguridad
  y enlace de activación por correo); recuperación de token nueva; Turnstile por envío.
- /vote-points: caja de info con las 7 secciones Q&A + panel "Sitios de votación" con
  tarjetas inline-div (imagen, PV, último voto), botón refrescar; abre el sitio y acredita
  PV al volver. lib/vote.ts + getVoteSitesForAccount.
- /rename-guild: renombra una hermandad de la que la cuenta es Maestro por 1000 PD +
  token de seguridad; SOAP .guild rename con comillas; reembolsa los PD si SOAP falla.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:06:49 +00:00
Inna 66d056c04b web-next: herramientas de PD (transferir, comercio, códigos de promoción)
- /transfer-d-points: transferir PD a la cuenta dueña de un personaje
  (requiere token de seguridad; movimiento atómico con FOR UPDATE).
- /trade-points: comercio PD<->oro mediante códigos de 1 solo uso
  (contraseña + token + Turnstile; oro al creador por SOAP; expira 1h;
  lock de 5s tras canjear; rollback total si falla la entrega).
- /promo-code: canje de códigos por PD/PV (sensibles a mayúsculas/minúsculas,
  usos limitados, 1 canje por cuenta).
- /admin/promo: panel para crear / editar / activar / desactivar / borrar
  códigos de promoción.
- Tablas nuevas (sql/): home_tradecode, home_promocode, home_promoredemption.

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:37:13 +00:00
Inna 42fc15de59 d-points: reconciliación SumUp (sin webhooks) para asegurar la entrega de PD
SumUp eliminó los webhooks, así que la acreditación no puede depender de que
el navegador vuelva al return_url. Se añade reconciliación consultando el
estado en la API de SumUp de los checkouts pendientes (fulfilled=0):

- reconcileSumUpCheckouts() en lib/sumup.ts: recorre los pendientes recientes
  y acredita los que estén PAID (reclamo atómico idempotente).
- /d-points y /account reconcilian los pendientes de la cuenta al cargar, para
  reflejar el saldo aunque el usuario no pasara por la página de éxito.
- /api/dpoints/reconcile: backstop global protegido con CRON_SECRET, pensado
  para un systemd timer que lo llame periódicamente.

Verificado E2E con tarjeta de test: pago SumUp sin volver a la web -> el
backstop acredita los PD (idempotente). Credenciales y CRON_SECRET viven en
.env.local (fuera de git).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 20:22:44 +00:00
Inna 89f6e5689f d-points: dejar sólo Stripe y SumUp con compra de PD funcional
Elimina las pestañas de PayPal/dLocal/Crypto/Skrill y por país; deja
Información + Stripe + SumUp (las dos únicas pasarelas que se usarán).

- Stripe: totalmente funcional reutilizando createCheckoutSession + webhook
  + página de éxito. Nueva ruta /api/dpoints/checkout crea la sesión con
  importe elegido por el usuario (1 unidad = 100 PD).
- SumUp: integración real (lib/sumup.ts) mediante checkout hospedado,
  habilitada al definir SUMUP_API_KEY/SUMUP_MERCHANT_CODE; si no, avisa.
- Nuevo servicio de entrega `dpoints` (lib/paid-services.ts) que acredita
  PD en home_api_points; entrega idempotente vía reclamo atómico.
- Página /d-points-success entrega ambos proveedores (session_id/ref).
- Iconos SVG generados para cada pasarela (stripe/sumup, marca + wordmark).
- Moneda configurable con DP_CURRENCY (por defecto EUR).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:14:29 +00:00
Inna c3e57ff25e web-next: /recruit según el markup (Información + Panel RAF con recompensas)
- Reescrita la página: box «Información» con la guía completa de Recluta a un
  amigo (marca → nombre del reino) + box «Panel de Recluta a un amigo».
- Nuevo RecruitPanel: estadísticas (cuenta reclutada, amigos reclutados, amigos
  al nivel 80, recompensas reclamadas X/6) + tabla de recompensas (objetivo,
  objeto con icono y color de calidad, estado) con botón Reclamar para las
  disponibles + sección «Recompensas disponibles». Sustituye a RecruitClaim.
- lib/recruit-claim: getRecruitStats (isRecruited + recruitedCount).
- Colores de calidad de objeto .q0-.q5 en globals.css.
- Sembradas las 6 recompensas en home_recruitreward (seed en sql/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:14:10 +00:00
Inna ce39d0582c web-next: rango de cuenta dinámico según PD en /account
La insignia de rango (antes fija a 1_Newbie/Nivel 1) ahora se calcula por los
PD de la cuenta: 14 niveles (getAccountRank en lib/account.ts) con umbrales
0/100/200…/900/1000/5000/10000/>10000 → imágenes nw-ranks 1_Newbie…14_GrandMaster
y título «Nivel N». Verificado end-to-end: dp=5500 → 12_Mentor «Nivel 12».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:56:01 +00:00
Inna acd3305a3f web-next: paginar /changelogs (10 por página + navegación)
getCommits ahora recibe page/limit y devuelve {commits, totalPages} (del
header X-PageCount de Gitea). La página lee ?page, muestra 10 commits y usa
el componente Pagination (Anterior/1 2 3…/Siguiente).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:23:38 +00:00
Inna fdd360684a web-next: página /changelogs desde la API de Gitea
Ruta que lista los últimos 50 commits del repo vía la API pública de Gitea
(lib/changelog.ts getCommits → /api/v1/repos/Inna/NovaWoW/commits). Cada
entrada: título (1ª línea), fecha, autor · sha (enlace al commit) y cuerpo
del mensaje (sin trailers Co-Authored-By). Estilo del tema (como noticias).
GITEA_URL/GITEA_REPO configurables por env (defaults al repo actual).
revalidate 300s para no golpear Gitea en cada visita.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:16:52 +00:00
Inna 0affbb2ced web-next: top de logros por PUNTOS reales + fix logro Nivel 80
- «Top de logros» ahora suma los PUNTOS de logros (no el nº): JOIN a
  acore_world.achievement_points (id→points), poblada desde el DB2 Achievement
  del build 3.4.3.54261 (wago.tools). Columna «Puntos de logros». Seed en
  sql/achievement_points.sql (1912 logros).
- Fix: el logro «Nivel 80» es el id 13 (logros de nivel WotLK van de +10:
  6=Nv10,7=Nv20…12=Nv70,13=Nv80), no el 20. «Primeros del reino» ahora se
  puebla correctamente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:10:55 +00:00
Inna 5d724afb8d web-next: página de jugadores /<slug>-players con datos reales de la BD
Extiende la ruta dinámica [realm] para servir también /<slug>-players (slug
del reino en realmlist). PlayersBoard (server) consulta acore_characters:
- Personajes creados por clase (conteo por clase y facción) + totales.
- Top de muertes con honor (characters.totalKills/todayKills, LIMIT 10).
- Top de logros (nº de logros por personaje vía character_achievement;
  los «puntos» no existen en la BD del servidor, vienen del DBC del cliente).
- Primeros del reino - Nivel 80 (primer personaje por fecha del logro 20 =
  Nivel 80, overall/por clase/por raza; vacío si aún no hay nivel 80).
Imágenes reales del tema (nw-classes/nw-races/nw-icons). lib/players.ts con
los mapas de clase/raza/facción. RealmInfo extraído a su componente. Enlace
JUGADORES de la cabecera ahora dinámico (/<slug>-players), como REINO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:53:50 +00:00
Inna a9ae6d4506 web-next: ruta del reino dinámica /<slug>-realm (según realmlist)
La página del reino pasa de /novawow-realm (fija) a una ruta dinámica
app/[locale]/[realm] cuyo slug es <nombre-del-reino>-realm, con el nombre
tomado de acore_auth.realmlist (LIMIT 1). Ej.: realm «Trinity» → /trinity-realm.
realmSlug() normaliza el nombre (minúsculas, sin espacios). Si el slug no
coincide con el reino actual → notFound(). Convive con el catch-all [...rest]
y las rutas estáticas mantienen prioridad (verificado: /login, /account OK;
/foo y /novawow-realm → 404).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:39:08 +00:00
Inna 8da22ba70f web-next: página /novawow-realm (información del reino)
Ruta con el markup del tema: título «Reino - <NOMBRE>» (nombre tomado de
acore_auth.realmlist con LIMIT 1, por si hay varios reinos; lib/realm.ts
getRealmName) y dos columnas realm-information-half con Rates y Horarios.
force-dynamic + generateMetadata con el nombre del reino.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:35:06 +00:00
Inna a93527cef7 web-next: estado del reino como Online/Offline (puerto 8085) en vez del conteo
En el recuadro de estado de la home, la fila del reino mostraba el conteo de
personajes online (0). Ahora muestra Online/Offline según un TCP check al
worldserver (puerto 8085), con color verde/rojo como la fila Login.
lib/home.ts añade world_status (checkServerStatus address:8085).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:16:39 +00:00
Inna 6337de73ad web-next: /account idéntico al my-account de Django
La página de cuenta no se parecía a la de Django (oro descuadrado, info como
tabla, herramientas en grupos abiertos). Reescrita para replicar el
partials/my-account.html + isla AccountDashboard de Django:

- Información en DOS fieldsets (.account-fieldset): «Datos básicos» (cuentas,
  correos, fecha de registro, IPs) y «Estado de la cuenta» (insignia de rango
  nw-rank + PD/PV/créditos/token + baneo + reclutamiento). lib/account.ts
  ampliado para traer reg_mail/email/joindate/last_ip/last_attempt_ip, estado
  de baneo (account_banned) y nº de reclutados.
- Personajes en char-box con el markup exacto de Django: retrato flotante +
  oro EN LÍNEA (monedas width=10). Antes las monedas salían a tamaño nativo
  (descuadre) y apiladas.
- Herramientas en paneles plegables (acordeón) «OPCIONES DE CUENTA / OPCIONES
  DE PERSONAJE» como Django (componente cliente AccountTools), con los
  servicios ya implementados en Next y las etiquetas/descripciones exactas.
- globals.css: revertir otro reset del preflight de Tailwind — img{display:
  block} apilaba verticalmente los iconos en línea (monedas); el tema los usa
  inline como la web Django. Volvemos a img{display:inline}.

Verificado con Playwright (login real con cuenta de prueba + captura): info,
personajes con oro en línea y paneles que expanden con sus iconos; home sin
regresión por el cambio de img.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 10:58:36 +00:00
Inna 8bb18838ff Admin: gestión de foros (categorías y foros)
- lib/admin-forum.ts: listCategoriesWithForums, createCategory/deleteCategory
  (solo si vacía), createForum/deleteForum (borrado lógico), setForumVisibility.
- API: /api/admin/forum(+/[id] DELETE/PATCH visibilidad) y
  /api/admin/forum/category(+/[id]) (403 sin admin).
- UI: /admin/forum (AdminForumManager) — alta de categorías, alta/borrado de foros
  por categoría, mostrar/ocultar; tarjeta "Gestionar foros" en el índice del admin.
- i18n es/en (Admin): 13 claves nuevas.

Verificado: build OK, /admin/forum 307->login, APIs 403 sin admin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:49:52 +00:00
Inna 1a45d48412 Admin: corregir columnas de account_access + enlace al panel en la cuenta
- Este build de AzerothCore usa account_access(AccountID, SecurityLevel), no el
  clásico (id, gmlevel). Se corrigen las 4 consultas (admin.ts, forum-perm.ts,
  forum.ts, admin-users.ts) -> isAdmin/forumIsModerator/gmlevel ahora funcionan.
- account page: enlace destacado al Panel de administración, visible solo si isAdmin.
- i18n es/en: Account.adminPanel.

Verificado: build OK; cuenta de prueba con SecurityLevel 3 (>= ADMIN_GMLEVEL) es admin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:42:04 +00:00
Inna f721aac7e5 Tienda Battlepay: pago de órdenes pendientes vía Stripe
Reutiliza la infraestructura Stripe (sin depender de SumUp/credenciales externas):
- lib/battlepay.ts: getPendingOrders/getPendingOrder/markOrderPaid sobre
  acore_auth.battlepay_orders (PENDING->PAID, el mod-battlepay entrega en juego).
- fulfill.ts: rama battlepay (metadata.battlepay_reference -> markOrderPaid),
  idempotente vía el claim atómico compartido con el webhook.
- API POST /api/battlepay/pay (valida orden PENDING de la cuenta, crea checkout).
- UI: /battlepay (lista + pagar) y /battlepay-success (entrega compartida),
  enlace "Tienda" en la cabecera para usuarios logueados.
- i18n es/en: namespace Battlepay + Nav.store.

Verificado: build OK, /battlepay 307->login, API 401 sin sesión, success 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:12:12 +00:00
Inna 00c03d84e1 Foro: perfil de usuario
- lib/forum.ts: getForumProfile (post_count, topic_count, joindate, gmlevel->status
  admin/gm, y últimos 5 temas), portado de forum/views.py profile().
- Página /forum/user/[username] con contadores, antigüedad, rango y temas recientes.
- Los nombres de autor en la vista del tema enlazan al perfil.
- i18n es/en: Forum.memberSince, recentTopics, roleAdmin, roleGm.

Verificado: build OK, /forum/user 200.

Foro COMPLETO: índice, foro/tema, crear, responder, editar/borrar, moderación
(lock/pin/mover/borrar/restaurar), búsqueda, paginación, editor rico y perfiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:08:49 +00:00
Inna 8a3ba8ff40 Foro: restaurar posts y temas borrados (moderadores)
- lib/forum.ts: getTopic/getPosts/countPosts aceptan includeDeleted (mods ven lo
  borrado); Post.deleted y TopicFull.deleted.
- API: PUT /api/forum/post restaura post (mod-only); moderate action 'restore'
  restaura tema (usa includeDeleted al leer el tema).
- UI: posts borrados atenuados con badge y botón Restaurar (PostActions);
  TopicModBar muestra banner + Restaurar tema cuando el tema está borrado.
- i18n es/en: Forum.restore, restoreTopic, deletedMark.

Verificado: build OK, PUT post y moderate restore 401 sin sesión, /forum 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:07:18 +00:00
Inna 7c1cd195ff Foro: mover tema entre foros (moderadores)
- lib/forum-write.ts: moveTopic (forum_id + moved=1). lib/forum.ts: listForumsForMove.
- API /api/forum/moderate: acción 'move' con forumId, validando foro destino visible
  y distinto del actual.
- TopicModBar: selector «Mover a» con los foros visibles (excl. el actual); tras mover,
  redirige al foro destino.
- i18n es/en: Forum.moveTopic.

Verificado: build OK, move 401 sin sesión, /forum 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 01:01:48 +00:00
Inna 0247c6cc38 Personajes: zona, raza/clase e imagen en el panel de cuenta
- lib/data/zone-names.json: ZONE_NAMES (9156 zonas) exportado de zone_definitions.py.
- lib/character-info.ts: getZoneName, getClassCss/getClassName, getRaceName y
  getCharacterImage (raza/clase/género -> /races/big-*.webp, con validación de
  compatibilidad clase-raza y fallback unknown), portado de account.py.
- lib/account.ts: Character enriquecido (race, class, zone, imageUrl, class/raceName).
- account page: tarjetas con imagen de raza, color de clase WoW, nivel, raza/clase,
  📍 zona y oro. globals.css: colores de clase (.class-*).
- public/races: 20 imágenes de raza del tema + unknown.webp.

Verificado: build OK, imágenes 200, /account 307→login. El JSON de zonas se usa
solo en servidor (no infla el bundle cliente).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:59:26 +00:00
Inna 389dac68c4 Stripe: webhook que entrega el servicio aunque el usuario no vuelva
- app/api/stripe/webhook: verifica firma (constructEventAsync) y en
  checkout.session.completed registra la IP de Stripe (como Django) y ENTREGA
  el servicio. Idempotente frente a la página de éxito.
- lib/stripe.ts: claimPaidCheckout ahora es ATÓMICO (UPDATE ... WHERE fulfilled=0
  + affectedRows) para evitar doble entrega entre webhook y página de éxito;
  nuevo recordStripeIp.
- lib/fulfill.ts: fulfillCheckoutSession compartido (reclama + resuelve servicio
  desde metadata.service + ejecuta). checkout guarda `service` en metadata.
- service-success: usa la entrega compartida y distingue entregado/ya-procesado/error.
- i18n: Paid.alreadyProcessed.

Requiere configurar STRIPE_WEBHOOK_SECRET en .env.local (endpoint apuntando a
/api/stripe/webhook, evento checkout.session.completed).

Verificado: build OK, webhook 400 sin firma / firma inválida.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:55:18 +00:00
Inna 7534b9cbbb Reclutamiento: página de usuario para reclamar recompensas (recruit-a-friend)
Portado de home/views/recruit.py:
- lib/recruit-claim.ts: getRecruitRewards (con marca de reclamadas),
  getRecruitedLevel80Count, y claimRecruitReward con las mismas validaciones que
  Django (recompensa existe, no reclamada, amigos reclutados a nivel 80 suficientes,
  anti-abuso por IP compartida), entrega por SOAP `.send items` (éxito si "Mail sent to")
  y registro en home_claimedreward. Extra de seguridad: valida que el personaje sea de la cuenta.
- API POST /api/recruit/claim (401 sin sesión).
- UI /recruit (pública): rejilla de recompensas con estado reclamada/elegible/bloqueada,
  selector de personaje y contador de amigos a nivel 80. Enlace en el footer.
- i18n es/en: namespace RecruitClaim + Nav.recruit.

Verificado: build OK, /recruit 200, claim 401 sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:45:29 +00:00
Inna c6c56c3d95 Admin: recompensas de reclutamiento (recruit-a-friend)
- lib/admin-recruit.ts: list/create/delete sobre home_recruitreward
  (required_friends, reward_name, item_id, item_quantity, item_link, icon_class).
- API: POST /api/admin/recruit, DELETE /api/admin/recruit/[id] (403 sin admin).
- UI: /admin/recruit con AdminRecruitManager (alta ordenada por amigos requeridos),
  enlace desde el índice del admin.
- i18n es/en (Admin): 10 claves nuevas.

Verificado: build OK, /admin/recruit 307→login, APIs 403 sin admin,
columnas confirmadas en la migración 0001_initial.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:41:31 +00:00
Inna d382adcd59 Admin: gestión de usuarios (buscar, banear/desbanear)
- lib/admin-users.ts: searchAccounts (acore_auth.account + account_banned + gmlevel),
  banAccount/unbanAccount por BD (autoritativo) con kick best-effort por SOAP.
  Baneo días<=0 = permanente (unbandate=bandate, convención AzerothCore); desactiva
  baneo activo previo antes de insertar el nuevo.
- API: GET /api/admin/users?q=, POST {accountId, action:ban|unban, reason, days} (403 sin admin).
- UI: /admin/users con AdminUsersManager (buscar por usuario/email, badges GM/online/baneado,
  banear con motivo+días o desbanear), enlace desde el índice del admin.
- i18n es/en (Admin): 14 claves nuevas.

Verificado: build OK, /admin/users 307→login, APIs 403 sin admin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:39:08 +00:00
Inna d9e6ff1496 Admin: gestión de opciones de oro (home_goldprice)
- lib/admin-gold.ts: list/create/delete sobre home_goldprice (id, gold_amount, price).
- API: POST /api/admin/gold, DELETE /api/admin/gold/[id] (403 sin admin).
- UI: /admin/gold con AdminGoldManager (alta ordenada por cantidad + borrado),
  enlace desde el índice del admin.
- i18n es/en (Admin): manageGold, gold, goldAmount, goldPrice, goldUnit, noGold.

Verificado: build OK, /admin/gold 307→login sin sesión, APIs 403 sin admin,
columnas confirmadas en la migración Django.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:35:13 +00:00
Inna 36a4354ff7 Foro: paginación de temas (20/pág) y de posts (15/pág)
- lib/forum.ts: countTopics/countPosts + offset/limit en getTopics/getPosts
  (mismos límites que Django: FORUM_TOPICS_PER_PAGE=20, FORUM_POSTS_PER_PAGE=15).
- components/Pagination.tsx: paginación reutilizable (ventana ±2, primera/última,
  prev/next) con enlaces ?page=N; no renderiza si solo hay una página.
- Foro y tema aceptan ?page=N (clamp al rango válido).
- El formulario de respuesta solo aparece en la última página; enlace a ella si no.
- i18n: namespace Common (prev/next/page) + replyOnLastPage.

Build OK. Sin datos de foro en acore_web (estado vacío correcto).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:32:16 +00:00
Inna 1de1002938 Foro: edición/borrado de posts, moderación de temas y búsqueda
Portado desde forum/db.py + forum/permissions.py de Django:
- lib/forum-perm.ts: forumIsModerator (gmlevel >= FORUM_MOD_GMLEVEL) y
  canEditPost (autor o moderador).
- Edición y borrado lógico de posts (autor o mod), respetando tema bloqueado,
  con validación de longitud mínima (plainLength).
- Moderación de temas (solo mod): bloquear/desbloquear, fijar/no fijar, borrar.
- Búsqueda de temas por título/contenido (searchTopics/countSearchTopics)
  con página /forum/search y buscador en el índice.
- API: PATCH/DELETE /api/forum/post, POST /api/forum/moderate (401/403 correctos).
- UI: PostActions, TopicModBar, ForumSearchBox; marca "editado" y "[Fijado]".
- i18n: 22 claves nuevas en es/en.

Verificado: build OK, /forum y /forum/search 200, APIs 401 sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:28:39 +00:00
Inna f9609aad99 Página de votación de usuario (/vote-points) + enlace en cabecera
- lib/vote.ts: getVoteSites (home_votesite), registerVote (cooldown 12h30 vía
  home_votelog, acredita PV en home_api_points, registra el voto).
- Route /api/vote (POST {url}, guard sesión). Página /vote-points (VotePanel:
  abre el sitio + registra el voto, muestra PV/cooldown). Enlace "Votar" en cabecera.
- Cierra el ciclo con el admin de sitios de voto.

Verificado: /vote-points 200, POST 401 sin sesión; lógica probada: voto -> +PV,
segundo voto -> cooldown 12h29m.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:06:57 +00:00
Inna 934bee2aec Admin: gestión de sitios de voto (home_votesite)
- lib/admin-votes.ts: listVoteSites / createVoteSite / deleteVoteSite.
- Routes /api/admin/votes (POST) y /api/admin/votes/[id] (DELETE), guard isAdmin.
- Página /admin/votes (AdminVotesManager: crear/listar/borrar) + enlace en dashboard.
- Conecta con la página de votación (/vote-points) ya existente.

Verificado: guardas (307/403), CRUD del write layer OK (con env cargado).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:03:06 +00:00
Inna 0f71e1f4d9 Admin: gestión de precios de los servicios de personaje
- lib/admin-prices.ts: PRICE_TABLES (rename/customize/change-race/change-faction/
  level-up/transfer -> home_*price), getAllPrices, setPrice (fila única: DELETE+INSERT).
- Route /api/admin/prices (POST, guard isAdmin). Página /admin/prices con
  AdminPricesManager (editar cada precio). Enlace en el dashboard admin.
- Catálogo Admin (precios + nombres de servicio).

Verificado: /admin/prices redirige sin permiso, API 403; setPrice escribe en BD
correctamente (probado con env cargado). Pendiente admin: sitios de voto, gold options,
moderación del foro.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:59:45 +00:00
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 f332fc39ca Registro + activación por email en Next.js (SRP6 + nodemailer)
- lib/mail.ts: transporte SMTP (nodemailer, mismas creds Gmail que Django).
- lib/register.ts: registerAccount (valida, comprueba email existente, crea fila en
  home_accountactivation reutilizando la tabla de Django, envía email de activación);
  activateAccount (crea battlenet_accounts con SRP6 v2 + account con SRP6 Grunt, como
  activate_account_view: expansion=2, battlenet_index=1; borra la activación).
- Route handlers /api/auth/register y /api/auth/activate.
- Páginas app/[locale]/register (RegisterForm cliente) y app/[locale]/activate-account
  (ActivateClient auto-POST al abrir el enlace). Textos en messages (Register, Activate).

Validado: validaciones (missingFields/passwordMismatch/invalidEmail/passwordTooLong);
ciclo completo activación->crea bnet+account->login OK con la cuenta creada
(needsSelection=false), password mala->invalidCredentials. Todo en TS, crypto validada.

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

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

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

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

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