Tres tablas: actividad (solicitudes de token, home_securitytoken), conexiones
al reino (logs_ip_actions + account.last_ip/last_login como conexión actual),
y conexiones web (home_loginattempt, estado Exito/Fracaso -> Conexión exitosa/
Contraseña incorrecta). lib/security-history.ts (getSecurityHistory), tolerante
si falta la tabla. Django era stub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lee las BD de AzerothCore: baneos de account_banned (cuenta),
battlenet_account_bans (Battle.net) y character_banned (personajes de la
cuenta); muteos de account_muted. Estado Activo/Activo (permanente)/Expirado
según bandate/unbandate/active. Dos tablas (baneos con alcance, muteos) + info.
lib/ban-history.ts (getSanctions), tolerante si AC no está poblado. Django stub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dos cajas por pasarela (Stripe mode test/live, SumUp mode sumup) desde
home_stripelog, con logo, nota y tabla ID/Estado/Concepto/Fecha/Cantidad.
Estado PAID (fulfilled=1) / PENDING. Se omiten dLocal, Crypto y PayPal del
diseño original. lib/trans-history.ts (getPaymentTransactions). Django era stub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Historial unificado por cuenta que combina las fuentes reales: home_stripelog
(pagos Stripe mode test/live + SumUp mode sumup), home_votelog (PV de votos) y
home_promoredemption (PD/PV por código). Detecta compras de PD ("N PD") vs
servicios pagados en €. lib/points-history.ts (getPointsHistory + getPointsBalances)
y página con el diseño (info + tabla Fecha/Concepto/Método/PD/PV/Importe/Estado
+ saldo actual). El Django original solo tenía un stub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Envía ítems de la tienda por correo al personaje de un amigo, pagado por
SumUp. lib/gift.ts (catálogo por categoría, priceCart con precios reales de
BD, sendGiftByMail vía SOAP .send items troceado a 12/correo + anti-inyección);
servicio send-gift en PAID_SERVICES; ruta dedicada /api/gift/checkout (origen
propio, destino existe, token de seguridad, carrito); SendGiftForm + página con
el diseño; i18n Paid[send-gift]; seed_giftitems.sql (4 cat, 8 ítems).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Nuevo servicio de pago `restore-item` (getRestoreItemPrice default 1€, fulfill
SOAP `.item restore <recover_id> <char>`). Al pulsar "Recuperar" se crea un
checkout de SumUp y al pagar se ejecuta el restore.
- Se quita el descuento de 100 PD y el gate "No tienes PD suficientes".
- La búsqueda sigue igual (gratis, cooldown 8h, Turnstile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /restore-character: recupera personajes borrados por BD (como .character deleted
restore). Condiciones: nivel>60, borrado <30 días, DK no si ya hay DK activo,
nombre libre, cooldown 12h/personaje. Gratis. Tabla home_restorehistory.
- /restore-items: recupera ítems borrados vía SOAP del core (.item restore list /
.item restore); el core aplica calidad/equipable/BoP/<7días. Cada ítem 100 PD
(reembolso si SOAP falla), consulta con cooldown 8h/personaje + Turnstile.
Tabla home_itemsearchhistory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Nueva ruta /transfer-character (10€ SumUp); borra /transfer (Stripe) y actualiza
el link del panel.
- lib/transfer-character.ts: precheck con todas las condiciones — personaje offline,
contraseña, token de seguridad, 2FA activo, cuenta destino válida (≠ propia) y,
para Caballeros de la Muerte, la cuenta destino debe tener un personaje nivel >=55
y no tener ya un DK; + bloqueo de 5s tras transferencia reciente.
- El hook precheck de PaidServiceConfig ahora recibe {accountId,email,character,body}.
- TransferForm: contraseña + token (con ojos), provider sumup, confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /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>
- /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>
- 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>
- /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>
- /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>
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>
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>
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>
- 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>
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>
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>
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>
- «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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
- 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>