Configurar Panel de Admin
Guia paso a paso para configurar y personalizar el panel de administracion
Configurar Panel de Admin
En este tutorial aprenderas a configurar el panel de administracion para gestiónar usuarios y roles de tu SaaS.
Requisitos previos
- Tener el proyecto configurado con Better Auth
- Base de datos PostgreSQL funcionando
- Al menos un usuario registrado
Paso 1: Verificar Better Auth Admin Plugin
El plugin de admin ya está configurado en lib/auth.ts:
import { multiSession, admin } from "better-auth/plugins";
export const auth = betterAuth({
// ...otras configuraciónes
plugins: [
multiSession({
maximumSessions: 5,
}),
admin({
defaultRole: "user",
adminRoles: ["admin"],
}),
],
});Los roles disponibles son: user y admin. Solo los usuarios con rol admin pueden acceder al panel.
Paso 2: Crear tu primer administrador
npx prisma studio- Abre Prisma Studio en
http://localhost:5555 - Navega a la tabla
users - Encuentra tu usuario y cambia el campo
roleaadmin - Guarda los cambios
UPDATE users
SET role = 'admin'
WHERE email = 'tu@email.com';Crea un script temporal:
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
async function createAdmin() {
const email = process.argv[2];
if (!email) {
console.error("Uso: npx tsx scripts/create-admin.ts tu@email.com");
process.exit(1);
}
const user = await db.user.update({
where: { email },
data: { role: "admin" },
});
console.log(`Usuario ${user.email} ahora es admin`);
await db.$disconnect();
}
createAdmin();npx tsx scripts/create-admin.ts tu@email.comPaso 3: Protección de rutas
El acceso al panel de admin está protegido en dos niveles:
Proxy (proxy.ts)
El proxy usa getSessionCookie de better-auth/cookies para una verificación optimista de la cookie de sesión. Como el proxy en Next.js 16 se ejecuta en Node.js runtime (no en Edge Runtime), no necesita hacer llamadas HTTP internas.
import { getSessionCookie } from "better-auth/cookies";
// Optimistic cookie check — no HTTP call needed
const sessionCookie = getSessionCookie(request);
const isAuthenticated = !!sessionCookie;La verificación del rol de admin no se hace en el proxy, ya que getSessionCookie solo verifica la existencia de la cookie, no su contenido (no contiene información de rol). En su lugar, la verificación de rol se hace en el layout del admin como Server Component con auth.api.getSession.
Layout del admin (app/(admin)/layout.tsx)
El layout del panel de admin incluye un sidebar con navegación:
import Link from "next/link";
import { Shield, Users, BarChart3, Settings, Zap } from "lucide-react";
import { siteConfig } from "@/config/site";
const adminNavItems = [
{ href: "/admin", label: "Overview", icon: BarChart3 },
{ href: "/admin/users", label: "Usuarios", icon: Users },
{ href: "/admin/settings", label: "Configuración", icon: Settings },
];
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen flex">
{/* Admin Sidebar */}
<aside className="w-64 border-r border-border bg-zinc-950 hidden md:flex flex-col">
<div className="p-6 border-b border-border">
<Link href="/admin" className="flex items-center gap-2 font-bold">
<Zap className="h-5 w-5" />
<span>{siteConfig.name}</span>
<span className="text-xs bg-primary text-primary-foreground px-1.5 py-0.5 rounded ml-1">
Admin
</span>
</Link>
</div>
<nav className="flex-1 p-4 space-y-1">
{adminNavItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
))}
</nav>
<div className="p-4 border-t border-border">
<Link
href="/dashboard"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<Shield className="h-4 w-4" />
Volver al dashboard
</Link>
</div>
</aside>
{/* Main */}
<main className="flex-1 overflow-auto bg-background">
<div className="p-8">{children}</div>
</main>
</div>
);
}Paso 4: Página principal del admin
La página /admin (app/(admin)/admin/page.tsx) muestra una vista general con tarjetas de estadisticas:
- Total usuarios con porcentaje de crecimiento
- MRR (Monthly Recurring Revenue)
- Conversion (tasa de conversion)
- Activos hoy
Y una tabla de usuarios recientes registrados en el sistema.
Las estadisticas vienen con valores placeholder (0). Conectalas a tus queries de Prisma para mostrar datos reales.
Paso 5: Gestión de usuarios
Página de usuarios (/admin/users)
En app/(admin)/admin/users/page.tsx encontraras una lista de todos los usuarios registrados. Para cada usuario se muestra:
- Avatar y nombre con badges de rol (
Admin) y estado (Baneado) - Email del usuario
- Estado de suscripcion (plan activo si lo tiene)
- Verificación de email (verificado o sin verificar)
- Fecha de registro (en formato relativo en español: "Hace 2 dias")
- Menu de acciones
Acciones disponibles
El componente AdminUserActions (app/(admin)/admin/users/user-actions.tsx) ofrece estas acciones a traves de un dropdown menu:
| Accion | Descripción |
|---|---|
| Hacer admin | Promover un usuario al rol admin |
| Quitar admin | Revertir un admin al rol user |
| Banear usuario | Bloquear el acceso del usuario |
| Desbanear | Restaurar el acceso de un usuario baneado |
Las acciones se ejecutan a traves del plugin admin de Better Auth:
import { authClient } from "@/lib/auth-client";
// Promover a admin
const setAdmin = () =>
handleAction(
() => authClient.admin?.setRole?.({ userId, role: "admin" }),
"Usuario promovido a admin"
);
// Revertir a usuario
const setUser = () =>
handleAction(
() => authClient.admin?.setRole?.({ userId, role: "user" }),
"Rol revertido a usuario"
);
// Banear
const banUser = () =>
handleAction(
() => authClient.admin?.banUser?.({ userId }),
"Usuario baneado"
);
// Desbanear
const unbanUser = () =>
handleAction(
() => authClient.admin?.unbanUser?.({ userId }),
"Usuario desbaneado"
);Paso 6: Añadir nuevas páginas al admin
Crea la página dentro de app/(admin)/admin/:
import type { Metadata } from "next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export const metadata: Metadata = {
title: "Mi Página - Admin",
};
export default function MiPáginaAdmin() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Mi Página</h1>
<p className="text-muted-foreground mt-1">
Descripción de la página
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Contenido</CardTitle>
</CardHeader>
<CardContent>
{/* Tu contenido */}
</CardContent>
</Card>
</div>
);
}Añade el enlace al sidebar editando app/(admin)/layout.tsx:
import { FileText } from "lucide-react";
const adminNavItems = [
{ href: "/admin", label: "Overview", icon: BarChart3 },
{ href: "/admin/users", label: "Usuarios", icon: Users },
{ href: "/admin/mi-pagina", label: "Mi Página", icon: FileText }, // Nueva página
{ href: "/admin/settings", label: "Configuración", icon: Settings },
];Mejores practicas
Seguridad
- Nunca expongas acciones de admin sin verificar el rol en el servidor
- El proxy protege las rutas a nivel de request, pero siempre válida también en tus API routes
- Las acciones de ban y cambio de rol se ejecutan a traves de la API de Better Auth, que válida permisos internamente
Resumen
Has configurado:
- Panel de admin con sidebar de navegación y estadisticas
- Gestión de usuarios con acciones: hacer admin, quitar admin, banear y desbanear
- Protección de rutas a nivel de proxy y layout
- Estructura para añadir nuevas páginas fácilmente
Siguiente paso
- Crear una página estática - Añadir nuevas páginas públicas a tu app