From 72e8000b26cc6622ff54bb2669f2590170d50d05 Mon Sep 17 00:00:00 2001 From: adevopg Date: Tue, 14 Jul 2026 14:22:05 +0000 Subject: [PATCH] =?UTF-8?q?A=C3=B1adir=20/ban-history=20(Historial=20de=20?= =?UTF-8?q?sanciones):=20baneos=20y=20muteos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- web-next/app/[locale]/ban-history/page.tsx | 139 +++++++++++++++++++++ web-next/lib/ban-history.ts | 117 +++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 web-next/app/[locale]/ban-history/page.tsx create mode 100644 web-next/lib/ban-history.ts diff --git a/web-next/app/[locale]/ban-history/page.tsx b/web-next/app/[locale]/ban-history/page.tsx new file mode 100644 index 0000000..a2e574f --- /dev/null +++ b/web-next/app/[locale]/ban-history/page.tsx @@ -0,0 +1,139 @@ +import type { Metadata } from 'next' +import { setRequestLocale } from 'next-intl/server' +import { redirect, Link } from '@/i18n/navigation' +import { getSession } from '@/lib/session' +import { getSanctions, type Sanction } from '@/lib/ban-history' +import { PageShell } from '@/components/PageShell' +import { ServiceBox } from '@/components/ServiceBox' + +export const dynamic = 'force-dynamic' + +export const metadata: Metadata = { title: 'Historial de sanciones' } + +/** Fecha en formato DD-MM-YYYY HH:MM:SS. */ +function fmt(d: Date): string { + const p = (n: number) => String(n).padStart(2, '0') + return `${p(d.getDate())}-${p(d.getMonth() + 1)}-${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` +} + +function StatusCell({ s }: { s: Sanction }) { + return {s.status} +} + +/** Caja con la tabla de sanciones (baneos o muteos), o el vacío del diseño. */ +function SanctionBox({ + title, + emptyText, + rows, + showScope, +}: { + title: string + emptyText: string + rows: Sanction[] + showScope: boolean +}) { + return ( +
+
+

{title}

+
+
+ {rows.length === 0 ? ( + + + + + + +
+ {emptyText} +
+ ) : ( + + + + {showScope && } + + + + + + + {rows.map((s, i) => ( + + {showScope && ( + + )} + + + + + + + ))} + +
AlcanceMotivoSancionado porFechaExpiraEstado
+ {s.scope} + + {s.reason} + + {s.by} + + {fmt(s.date)} + + {s.expires ? fmt(s.expires) : 'Permanente'} + + +
+ )} +
+
+ ) +} + +export default async function BanHistoryPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params + setRequestLocale(locale) + + const session = await getSession() + if (!session.bnetId) redirect({ href: '/login', locale }) + if (!session.username) redirect({ href: '/select-account', locale }) + + const { bans, mutes } = await getSanctions(session.accountId!, session.bnetId) + + return ( + + +
+

Aquí podrás ver todas las sanciones que ha tenido la cuenta.

+

+ Si tienes una sanción vigente, verás que su estado indica Activo. +

+

Para evitar que tu cuenta sea sancionada, recuerda respetar las normas de nuestro servidor.

+
+
+

+ Importante: +

+

+ Si tu cuenta aparece como sancionada o cerrada al intentar ingresar al reino, pero tu historial no tiene + ninguna sanción activa, significa que has tenido un bloqueo automático temporal por demasiados intentos de + inicio de sesión fallidos. +

+

+ Espera al menos 10 minutos para volver a intentar y asegúrate de usar nombre de cuenta y contraseña + correctas. +

+
+
+

+ Para realizar un reclamo de sanción, por favor ingresa en nuestro foro y visita la + sección Reclamaciones. +

+
+ + + +
+ ) +} diff --git a/web-next/lib/ban-history.ts b/web-next/lib/ban-history.ts new file mode 100644 index 0000000..cfdb540 --- /dev/null +++ b/web-next/lib/ban-history.ts @@ -0,0 +1,117 @@ +import type { RowDataPacket } from 'mysql2' +import { db, DB } from './db' + +/** Una sanción (baneo o muteo) de la cuenta. */ +export interface Sanction { + scope: string // 'Cuenta' | 'Battle.net' | 'Personaje: Nombre' + reason: string + by: string + date: Date + expires: Date | null // null = permanente + active: boolean + status: string // etiqueta: Activo / Activo (permanente) / Expirado +} + +/** Estado de un baneo a partir de sus timestamps UNIX (segundos) y el flag active. */ +function banState(bandate: number, unbandate: number, active?: number): { active: boolean; status: string; expires: Date | null } { + const now = Math.floor(Date.now() / 1000) + const permanent = unbandate === 0 || unbandate <= bandate + const flagActive = active === undefined ? true : active === 1 + const stillActive = flagActive && (permanent || unbandate > now) + return { + active: stillActive, + status: stillActive ? (permanent ? 'Activo (permanente)' : 'Activo') : 'Expirado', + expires: permanent ? null : new Date(unbandate * 1000), + } +} + +/** + * Historial de sanciones de una cuenta desde las BD de AzerothCore: + * - Baneos: `acore_auth.account_banned` (cuenta de juego), `battlenet_account_bans` + * (cuenta Battle.net) y `acore_characters.character_banned` (personajes de la cuenta). + * - Muteos: `acore_auth.account_muted` (por cuenta de juego). + * Cada consulta es tolerante a fallos (devuelve [] si la tabla no existe / AC no + * está poblado) para no romper la página. Ordenado por fecha descendente. + */ +export async function getSanctions(accountId: number, bnetId?: number): Promise<{ bans: Sanction[]; mutes: Sanction[] }> { + const bans: Sanction[] = [] + const mutes: Sanction[] = [] + if (!accountId) return { bans, mutes } + + // 1) Baneos de la cuenta de juego. + try { + const [rows] = await db(DB.auth).query( + 'SELECT bandate, unbandate, bannedby, banreason, active FROM account_banned WHERE id = ? ORDER BY bandate DESC', + [accountId], + ) + for (const r of rows) { + const s = banState(Number(r.bandate), Number(r.unbandate), Number(r.active)) + bans.push({ scope: 'Cuenta', reason: String(r.banreason || '—'), by: String(r.bannedby || '—'), date: new Date(Number(r.bandate) * 1000), ...s }) + } + } catch { + /* AC no poblado */ + } + + // 2) Baneos de la cuenta Battle.net (sin columna active). + if (bnetId) { + try { + const [rows] = await db(DB.auth).query( + 'SELECT bandate, unbandate, bannedby, banreason FROM battlenet_account_bans WHERE id = ? ORDER BY bandate DESC', + [bnetId], + ) + for (const r of rows) { + const s = banState(Number(r.bandate), Number(r.unbandate)) + bans.push({ scope: 'Battle.net', reason: String(r.banreason || '—'), by: String(r.bannedby || '—'), date: new Date(Number(r.bandate) * 1000), ...s }) + } + } catch { + /* AC no poblado */ + } + } + + // 3) Baneos de personaje (de los personajes de la cuenta). + try { + const [chars] = await db(DB.characters).query('SELECT guid, name FROM characters WHERE account = ?', [accountId]) + if (chars.length > 0) { + const nameByGuid = new Map(chars.map((c) => [Number(c.guid), String(c.name)])) + const guids = [...nameByGuid.keys()] + const [rows] = await db(DB.characters).query( + `SELECT guid, bandate, unbandate, bannedby, banreason, active FROM character_banned WHERE guid IN (${guids.map(() => '?').join(',')}) ORDER BY bandate DESC`, + guids, + ) + for (const r of rows) { + const s = banState(Number(r.bandate), Number(r.unbandate), Number(r.active)) + bans.push({ scope: `Personaje: ${nameByGuid.get(Number(r.guid)) ?? '?'}`, reason: String(r.banreason || '—'), by: String(r.bannedby || '—'), date: new Date(Number(r.bandate) * 1000), ...s }) + } + } + } catch { + /* AC no poblado */ + } + + // 4) Muteos de la cuenta de juego (mutetime = fin del muteo). + try { + const [rows] = await db(DB.auth).query( + 'SELECT mutedate, mutetime, mutedby, mutereason FROM account_muted WHERE guid = ? ORDER BY mutedate DESC', + [accountId], + ) + const now = Math.floor(Date.now() / 1000) + for (const r of rows) { + const end = Number(r.mutetime) + const active = end > now + mutes.push({ + scope: 'Cuenta', + reason: String(r.mutereason || '—'), + by: String(r.mutedby || '—'), + date: new Date(Number(r.mutedate) * 1000), + expires: end > 0 ? new Date(end * 1000) : null, + active, + status: active ? 'Activo' : 'Expirado', + }) + } + } catch { + /* AC no poblado */ + } + + bans.sort((a, b) => b.date.getTime() - a.date.getTime()) + mutes.sort((a, b) => b.date.getTime() - a.date.getTime()) + return { bans, mutes } +}