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>
This commit is contained in:
2026-07-12 23:55:34 +00:00
parent 239392b876
commit d5b00a86dc
9 changed files with 256 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { isAdmin } from '@/lib/admin'
import { listNews } from '@/lib/admin-news'
import { AdminNewsManager } from '@/components/AdminNewsManager'
export const dynamic = 'force-dynamic'
export default async function AdminNewsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Admin')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
if (!(await isAdmin(session))) redirect({ href: '/', locale })
const news = await listNews()
return (
<main className="mx-auto max-w-3xl px-4 py-8">
<h1 className="mb-6 text-2xl font-bold text-amber-500">{t('manageNews')}</h1>
<AdminNewsManager initialNews={news} />
</main>
)
}
+28
View File
@@ -0,0 +1,28 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { isAdmin } from '@/lib/admin'
export const dynamic = 'force-dynamic'
export default async function AdminPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Admin')
const session = await getSession()
if (!session.bnetId) redirect({ href: '/login', locale })
if (!(await isAdmin(session))) redirect({ href: '/', locale })
return (
<main className="mx-auto max-w-3xl px-4 py-8">
<h1 className="mb-6 text-2xl font-bold text-amber-500">{t('title')}</h1>
<ul className="space-y-2">
<li>
<Link href="/admin/news" className="text-sky-400 hover:underline">
{t('manageNews')}
</Link>
</li>
</ul>
</main>
)
}
+11
View File
@@ -0,0 +1,11 @@
import { getSession } from '@/lib/session'
import { isAdmin } from '@/lib/admin'
import { deleteNews } from '@/lib/admin-news'
export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await getSession()
if (!(await isAdmin(session))) return Response.json({ success: false, error: 'forbidden' }, { status: 403 })
const { id } = await params
await deleteNews(Number(id))
return Response.json({ success: true })
}
+16
View File
@@ -0,0 +1,16 @@
import { getSession } from '@/lib/session'
import { isAdmin } from '@/lib/admin'
import { createNews } from '@/lib/admin-news'
export async function POST(request: Request) {
const session = await getSession()
if (!(await isAdmin(session))) return Response.json({ success: false, error: 'forbidden' }, { status: 403 })
let b: Record<string, string> = {}
try { b = await request.json() } catch { return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 }) }
const titulo = String(b.titulo ?? '').trim()
const contenido = String(b.contenido ?? '').trim()
const enlace = String(b.enlace ?? '').trim() || null
if (!titulo || !contenido) return Response.json({ success: false, error: 'emptyError' })
const id = await createNews(titulo, contenido, enlace)
return Response.json({ success: true, id })
}