diff --git a/README.md b/README.md index 7a17735..0cd349c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ npm run dev # http://127.0.0.1:3000 | `ADMIN_EMAILS` / `ADMIN_GMLEVEL` | Acceso al panel de administración | | `CRON_SECRET` | Protege el endpoint de reconciliación de SumUp | | `NEXT_PUBLIC_GA_ID` | Google Analytics 4 (p. ej. `G-XXXXXXXXXX`). **Vacío o sin poner = analítica desactivada**, no se carga nada. Además, aunque esté puesta, solo se carga si el visitante acepta la categoría «Analíticas» del banner de cookies. Al ser `NEXT_PUBLIC_*` se incrusta en el build: activar o desactivar exige `npm run build` + reinicio | +| `ADSENSE_CLIENT_ID` | ID de editor de Google AdSense (`ca-pub-...`). Emite `` en el `` de todas las páginas. **Vacío o sin poner = etiqueta desactivada**. No la ven ni los admins ni las cuentas de rango 2+ o que hayan pagado alguna vez (ver `components/AdSense.tsx`). Se lee en el servidor en cada petición: activar o desactivar es tocar el `.env` + reinicio, **sin rebuild** | ## 3. Producción diff --git a/web-next/app/[locale]/layout.tsx b/web-next/app/[locale]/layout.tsx index 012ce7e..15b02b5 100644 --- a/web-next/app/[locale]/layout.tsx +++ b/web-next/app/[locale]/layout.tsx @@ -10,6 +10,7 @@ import { Social } from '@/components/Social' import { Footer } from '@/components/Footer' import { CookieBanner } from '@/components/CookieConsent' import { Analytics } from '@/components/Analytics' +import { AdSense } from '@/components/AdSense' import '../globals.css' export function generateStaticParams() { @@ -52,6 +53,9 @@ export default async function LocaleLayout({ return ( + {/* Verificación de la cuenta de Google AdSense: solo si hay ADSENSE_CLIENT_ID + y el visitante no es admin. Ver components/AdSense.tsx. */} + {/* Iconos del sitio (idénticos a la web Django) */} diff --git a/web-next/components/AdSense.tsx b/web-next/components/AdSense.tsx new file mode 100644 index 0000000..ef61b23 --- /dev/null +++ b/web-next/components/AdSense.tsx @@ -0,0 +1,42 @@ +import { getSession } from '@/lib/session' +import { isAdmin } from '@/lib/admin' +import { getAccountRankInfo } from '@/lib/account' + +/** + * Etiqueta de verificación de Google AdSense, con TRES interruptores: + * + * 1. `ADSENSE_CLIENT_ID` en el .env. Vacío o sin poner = no se emite nada. Es el + * interruptor del administrador. No lleva prefijo NEXT_PUBLIC_ a propósito: + * esto se renderiza en el servidor (el layout ya es dinámico porque
+ * lee la sesión), así que no hace falta exponerlo al bundle del cliente. + * 2. Admin (allowlist 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, ver getAccountRank en lib/account.ts) y pagos: + * solo ve anuncios el rango 1 (Newbie, el de por defecto) que ADEMÁS nunca ha + * pagado. Son dos cortes distintos a propósito: cualquier pago confirmado por + * Stripe o SumUp los quita por mínimo que sea, y el mínimo de SumUp (0,20 € = + * 20 PD) no llega a los 100 PD del rango 2, así que mirar solo el rango dejaría + * con anuncios a alguien que ya ha pagado. + * + * Ambas comprobaciones cortan en seco sin tocar la BD si no hay sesión, así que el + * visitante anónimo (el caso normal) no paga ninguna consulta extra. + * + * Ojo: esto es solo la etiqueta de verificación de la cuenta, no carga adsbygoogle.js + * ni escribe cookies, por eso no pasa por el banner de consentimiento. El día que se + * añada el script de anuncios sí tendrá que ir detrás de la categoría «Marketing» + * (useConsent('marketing'), ver components/Analytics.tsx). + */ +export async function AdSense() { + const client = process.env.ADSENSE_CLIENT_ID + if (!client) return null + + const session = await getSession() + if (await isAdmin(session)) return null + + if (session.accountId) { + const { level, donatedPD } = await getAccountRankInfo(session.accountId) + if (level >= 2 || donatedPD > 0) return null + } + + return +} diff --git a/web-next/lib/account.ts b/web-next/lib/account.ts index fa18d60..8e9f003 100644 --- a/web-next/lib/account.ts +++ b/web-next/lib/account.ts @@ -54,39 +54,57 @@ export interface AccountDashboard { characters: Character[] } -/** Datos del panel de cuenta. Resiliente si las BD de AzerothCore no están pobladas. */ -export async function getAccountDashboard(session: SessionData, locale = 'es'): Promise { - const accountId = session.accountId ?? 0 - - let dp = 0 - let vp = 0 +/** 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( 'SELECT dp, vp FROM api_points WHERE accountID = ?', [accountId], ) - if (p[0]) { - dp = p[0].dp - vp = p[0].vp - } + 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. - let donatedPD = 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 { try { const [d] = await db(DB.default).query( "SELECT COALESCE(SUM(amount), 0) AS total FROM stripelog WHERE account_id = ? AND fulfilled = 1 AND mode IN ('sumup', 'test', 'live')", [accountId], ) - donatedPD = Math.round(Number(d[0]?.total ?? 0) * PD_PER_UNIT) + 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 { + const accountId = session.accountId ?? 0 + + const { dp, vp } = await getPoints(accountId) + const donatedPD = await getDonatedPD(accountId) let battlepayCredits = 0 try {