3361f2e9e9
Se añade `<meta name="google-adsense-account">` al <head> de todas las páginas, que es lo que pide Google para verificar la propiedad del sitio. OJO: esto NO muestra anuncios ni carga adsbygoogle.js todavía; solo permite que Google confirme que el dominio es nuestro y arranque la revisión. TRES interruptores (ver components/AdSense.tsx): 1. `ADSENSE_CLIENT_ID` en el .env (no versionado). Vacío o sin poner = no se emite nada. Documentada en el README. NO lleva prefijo NEXT_PUBLIC_ a propósito: la etiqueta se pinta en el servidor, así que no hace falta exponerla al bundle ni rebuildear para activarla/desactivarla, al revés que NEXT_PUBLIC_GA_ID. 2. Admin (ADMIN_EMAILS o gmlevel de AzerothCore): sin etiqueta, para que la navegación del staff no cuente como tráfico con anuncios. 3. Rango de la cuenta (ns-ranks) Y pagos: solo ve anuncios el rango 1 (Newbie, el de por defecto) que ADEMÁS nunca ha pagado. Los dos cortes del punto 3 son distintos a propósito y no basta con mirar el rango: el mínimo de SumUp son 0,20 €, que a 100 PD/unidad son 20 PD, y eso sigue siendo rango 1. Mirando solo el rango, quien pagara el mínimo seguiría viendo anuncios, que es justo lo contrario de lo que queremos. Para no duplicar el SQL, las consultas de saldo y de total donado salen de getAccountDashboard a dos helpers (`getPoints`, `getDonatedPD`) que reutiliza el nuevo `getAccountRankInfo`, con las dos consultas mínimas en vez del panel entero (personajes, baneos, correo…). El criterio de rango es el mismo que ya usaba /my-account: el máximo entre lo donado y el saldo actual. Verificado contra la web real: anónimo y rango 1 sin pagos SÍ reciben la etiqueta; rango 1 con 99 PD también; 100 PD de saldo (rango 2) no; rango 1 tras pagar 0,20 € tampoco; admin tampoco; y con la variable vacía no la recibe nadie. Los dos últimos casos no existían en la BD, así que se probaron con datos temporales en la cuenta 14 (fila en stripelog y en api_points), ya borrados. /my-account sigue pintando el rango correcto tras mover las consultas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
227 lines
6.7 KiB
TypeScript
227 lines
6.7 KiB
TypeScript
import type { RowDataPacket } from 'mysql2'
|
|
import { db, DB } from './db'
|
|
import type { SessionData } from './session'
|
|
import { getZoneName, getClassCss, getCharacterImage, getRaceName, getClassName } from './character-info'
|
|
import { PD_PER_UNIT } from './dpoints'
|
|
|
|
// Rango de la cuenta según los PD (14 niveles). Umbral = PD mínimos por nivel.
|
|
const RANK_FILES = [
|
|
'1_Newbie', '2_Rookie', '3_Apprentice', '4_Explorer', '5_Contributor', '6_Enthusiast',
|
|
'7_Collaborator', '8_Regular', '9_RisingStar', '10_Proficient', '11_Experienced',
|
|
'12_Mentor', '13_Veteran', '14_GrandMaster',
|
|
]
|
|
const RANK_MIN = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 5000, 10000, 10001]
|
|
|
|
/** Nivel de rango (1-14) e imagen según los PD (dp). */
|
|
export function getAccountRank(dp: number): { level: number; image: string } {
|
|
let idx = 0
|
|
for (let i = 0; i < RANK_MIN.length; i++) if (dp >= RANK_MIN[i]) idx = i
|
|
return { level: idx + 1, image: `/ns-themes/ns-ryu/ns-images/ns-ranks/${RANK_FILES[idx]}.svg` }
|
|
}
|
|
|
|
export interface Character {
|
|
name: string
|
|
level: number
|
|
gold: number
|
|
silver: number
|
|
copper: number
|
|
race: number
|
|
classId: number
|
|
className: string
|
|
raceName: string
|
|
classCss: string
|
|
zone: string
|
|
imageUrl: string
|
|
}
|
|
|
|
export interface AccountDashboard {
|
|
bnetEmail: string
|
|
gameAccount: string
|
|
regMail: string
|
|
email: string
|
|
joindate: string
|
|
lastIp: string
|
|
lastAttemptIp: string
|
|
dp: number
|
|
vp: number
|
|
donatedPD: number
|
|
battlepayCredits: number
|
|
securityTokenDate: string | null
|
|
isBanned: boolean
|
|
unbanDate: string | null
|
|
isRecruited: boolean
|
|
recruitedCount: number
|
|
characters: Character[]
|
|
}
|
|
|
|
/** Saldo de puntos (PD y PV) de una cuenta. 0/0 si no tiene fila en api_points. */
|
|
async function getPoints(accountId: number): Promise<{ dp: number; vp: number }> {
|
|
try {
|
|
const [p] = await db(DB.default).query<RowDataPacket[]>(
|
|
'SELECT dp, vp FROM api_points WHERE accountID = ?',
|
|
[accountId],
|
|
)
|
|
if (p[0]) return { dp: p[0].dp, vp: p[0].vp }
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return { dp: 0, vp: 0 }
|
|
}
|
|
|
|
/**
|
|
* Total donado (en PD) para el rango: suma de TODOS los pagos confirmados
|
|
* (Stripe mode test/live + SumUp mode sumup), convertidos a PD. Cuenta todas
|
|
* las pasarelas y donaciones mixtas, y NO baja al gastar PD (a diferencia del
|
|
* saldo). El rango usará el máximo entre esto y el saldo actual.
|
|
*/
|
|
async function getDonatedPD(accountId: number): Promise<number> {
|
|
try {
|
|
const [d] = await db(DB.default).query<RowDataPacket[]>(
|
|
"SELECT COALESCE(SUM(amount), 0) AS total FROM stripelog WHERE account_id = ? AND fulfilled = 1 AND mode IN ('sumup', 'test', 'live')",
|
|
[accountId],
|
|
)
|
|
return Math.round(Number(d[0]?.total ?? 0) * PD_PER_UNIT)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* Rango (1-14) y total donado de una cuenta, con las dos consultas mínimas y sin
|
|
* traerse el panel entero (personajes, baneos, correo…). Mismo criterio de rango
|
|
* que /my-account: el máximo entre lo donado y el saldo actual.
|
|
*/
|
|
export async function getAccountRankInfo(
|
|
accountId: number,
|
|
): Promise<{ level: number; donatedPD: number }> {
|
|
const [{ dp }, donatedPD] = await Promise.all([getPoints(accountId), getDonatedPD(accountId)])
|
|
return { level: getAccountRank(Math.max(donatedPD, dp)).level, donatedPD }
|
|
}
|
|
|
|
/** Datos del panel de cuenta. Resiliente si las BD de AzerothCore no están pobladas. */
|
|
export async function getAccountDashboard(session: SessionData, locale = 'es'): Promise<AccountDashboard> {
|
|
const accountId = session.accountId ?? 0
|
|
|
|
const { dp, vp } = await getPoints(accountId)
|
|
const donatedPD = await getDonatedPD(accountId)
|
|
|
|
let battlepayCredits = 0
|
|
try {
|
|
const [c] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT battlePayCredits FROM battlenet_accounts WHERE id = ?',
|
|
[session.bnetId ?? 0],
|
|
)
|
|
if (c[0]?.battlePayCredits != null) battlepayCredits = c[0].battlePayCredits
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Datos de la tabla account (correos, fecha, IPs, reclutador).
|
|
let regMail = ''
|
|
let email = ''
|
|
let joindate = ''
|
|
let lastIp = ''
|
|
let lastAttemptIp = ''
|
|
let isRecruited = false
|
|
try {
|
|
const [a] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT reg_mail, email, last_ip, last_attempt_ip, joindate, recruiter FROM account WHERE id = ?',
|
|
[accountId],
|
|
)
|
|
if (a[0]) {
|
|
regMail = a[0].reg_mail || ''
|
|
email = a[0].email || ''
|
|
lastIp = a[0].last_ip || ''
|
|
lastAttemptIp = a[0].last_attempt_ip || ''
|
|
isRecruited = Boolean(a[0].recruiter)
|
|
if (a[0].joindate) joindate = new Date(a[0].joindate).toLocaleString('es-ES')
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Estado de baneo (account_banned activo).
|
|
let isBanned = false
|
|
let unbanDate: string | null = null
|
|
try {
|
|
const [bn] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT unbandate FROM account_banned WHERE id = ? AND active = 1 LIMIT 1',
|
|
[accountId],
|
|
)
|
|
if (bn[0]) {
|
|
isBanned = true
|
|
unbanDate = new Date(Number(bn[0].unbandate) * 1000).toLocaleString('es-ES')
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
// Nº de amigos reclutados.
|
|
let recruitedCount = 0
|
|
try {
|
|
const [rc] = await db(DB.auth).query<RowDataPacket[]>(
|
|
'SELECT COUNT(*) AS n FROM account WHERE recruiter = ?',
|
|
[accountId],
|
|
)
|
|
if (rc[0]) recruitedCount = Number(rc[0].n)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
let securityTokenDate: string | null = null
|
|
try {
|
|
const [s] = await db(DB.default).query<RowDataPacket[]>(
|
|
'SELECT created_at FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
[accountId],
|
|
)
|
|
if (s[0]) securityTokenDate = new Date(s[0].created_at).toISOString()
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
let characters: Character[] = []
|
|
try {
|
|
const [ch] = await db(DB.characters).query<RowDataPacket[]>(
|
|
'SELECT name, level, money, race, class, zone, gender FROM characters WHERE account = ?',
|
|
[accountId],
|
|
)
|
|
characters = ch.map((r) => ({
|
|
name: r.name,
|
|
level: r.level,
|
|
gold: Math.floor(r.money / 10000),
|
|
silver: Math.floor((r.money % 10000) / 100),
|
|
copper: r.money % 100,
|
|
race: r.race,
|
|
classId: r.class,
|
|
className: getClassName(r.class, locale),
|
|
raceName: getRaceName(r.race, locale),
|
|
classCss: getClassCss(r.class),
|
|
zone: getZoneName(r.zone, locale),
|
|
imageUrl: getCharacterImage(r.class, r.race, r.gender),
|
|
}))
|
|
} catch {
|
|
/* la BD de personajes puede no estar poblada */
|
|
}
|
|
|
|
return {
|
|
bnetEmail: session.bnetEmail ?? '',
|
|
gameAccount: session.username ?? '',
|
|
regMail,
|
|
email,
|
|
joindate,
|
|
lastIp,
|
|
lastAttemptIp,
|
|
dp,
|
|
vp,
|
|
donatedPD,
|
|
battlepayCredits,
|
|
securityTokenDate,
|
|
isBanned,
|
|
unbanDate,
|
|
isRecruited,
|
|
recruitedCount,
|
|
characters,
|
|
}
|
|
}
|