Panel de Administracion
Gestióna usuarios, roles y baneos desde el panel de admin
Panel de Administracion
EmpiezaTuSaaS incluye un panel de administracion para gestiónar usuarios de tu plataforma. El panel tiene sidebar de navegación, página de overview con estadisticas y página de gestión de usuarios con acciones por usuario.
Caracteristicas
- Vista general: Dashboard con 4 tarjetas de estadisticas (Total usuarios, MRR, Conversion, Activos hoy)
- Gestión de usuarios: Lista de usuarios con avatar, email, estado de verificación, suscripcion, rol y acciones
- Roles:
adminyuser(dos roles, sinsuperadmin) - Baneos: Banear y desbanear usuarios
- Acciones por usuario: Menu dropdown con opciones de cambiar rol y banear/desbanear
Estructura de archivos
app/
└── (admin)/
├── layout.tsx # Sidebar de navegación + verificación de rol
└── admin/
├── page.tsx # Overview con tarjetas de estadisticas
└── users/
├── page.tsx # Lista de usuarios con Prisma
└── user-actions.tsx # Componente client con acciones (dropdown)No hay archivos separados en components/admin/. El sidebar está inline en el layout y las acciones de usuario estan en user-actions.tsx junto a la página de usuarios.
Acceso al Panel
El panel está disponible en /admin. La navegación lateral incluye 3 enlaces:
const adminNavItems = [
{ href: "/admin", label: "Overview", icon: BarChart3 },
{ href: "/admin/users", label: "Usuarios", icon: Users },
{ href: "/admin/settings", label: "Configuración", icon: Settings },
];El layout incluye un sidebar oscuro (bg-zinc-950) con el nombre de la app, badge "Admin", links de navegación y un enlace para volver al dashboard.
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">
<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 className="flex-1 overflow-auto bg-background">
<div className="p-8">{children}</div>
</main>
</div>
);
}Crear el primer admin
Al inicio, no tendrás usuarios admin. Puedes crear uno de dos formas:
Opcion 1: Via base de datos
UPDATE "user" SET role = 'admin' WHERE email = 'tu@email.com';Opcion 2: Via Prisma Studio
npx prisma studioAbre Prisma Studio en el navegador, busca tu usuario y cambia el campo role a admin.
Overview (Dashboard de admin)
La página /admin muestra 4 tarjetas de estadisticas con valores placeholder (para que los conectes a datos reales) y una tabla de usuarios recientes.
import { Users, DollarSign, TrendingUp, Activity } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
const adminStats = [
{ title: "Total usuarios", value: "0", icon: Users, trend: "+0%" },
{ title: "MRR", value: "€0", icon: DollarSign, trend: "+0%" },
{ title: "Conversión", value: "0%", icon: TrendingUp, trend: "+0%" },
{ title: "Activos hoy", value: "0", icon: Activity, trend: "+0%" },
];
export default function AdminPage() {
return (
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Panel de Administración</h1>
<p className="text-muted-foreground mt-1">
Vista general del sistema
</p>
</div>
<Badge variant="outline" className="text-xs">
Admin
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{adminStats.map((stat) => (
<Card key={stat.title}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardDescription>{stat.title}</CardDescription>
<stat.icon className="h-4 w-4 text-muted-foreground" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-green-500 mt-1">{stat.trend}</p>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle>Usuarios recientes</CardTitle>
<CardDescription>
Los últimos usuarios registrados en el sistema
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-sm text-center text-muted-foreground py-8">
No hay usuarios registrados todavía
</div>
</CardContent>
</Card>
</div>
);
}Las estadisticas usan valores placeholder ("0", "€0", "0%"). Para conectarlas a datos reales, reemplaza los valores con queries de Prisma.
Gestión de usuarios
La página /admin/users lista todos los usuarios del sistema usando Prisma. Muestra avatar, nombre, email, badges de rol y estado, fecha de registro y un menu de acciones.
import { db } from "@/lib/db";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { AdminUserActions } from "./user-actions";
import { formatDistanceToNow } from "@/lib/utils";
export default async function AdminUsersPage() {
let users = [];
try {
users = await db.user.findMany({
orderBy: { createdAt: "desc" },
take: 100,
select: {
id: true,
name: true,
email: true,
emailVerified: true,
image: true,
createdAt: true,
banned: true,
role: true,
subscription: {
select: { status: true, plan: true },
},
},
});
} catch {
// DB not connected in development
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Gestión de Usuarios</h1>
<p className="text-muted-foreground mt-1">
{users.length} usuarios registrados
</p>
</div>
{/* Tabla de usuarios con acciones */}
</div>
);
}Cada usuario muestra:
- Avatar con imagen o iniciales como fallback
- Nombre con badge "Admin" si
role === "admin"y badge "Baneado" sibanned === true - Email debajo del nombre
- Estado de suscripcion: badge verde si tiene suscripcion activa
- Verificación de email: badge "Verificado" o "Sin verificar"
- Fecha de registro: usando
formatDistanceToNow()(con locale en español) - Menu de acciones: dropdown con opciones
Acciones de usuario
El componente AdminUserActions es un Client Component que usa el dropdown de shadcn/ui y authClient de Better Auth para ejecutar acciones:
"use client";
import { useState } from "react";
import { MoreHorizontal, ShieldCheck, ShieldOff, Ban, UserCheck } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
interface AdminUserActionsProps {
userId: string;
currentRole: string;
isBanned: boolean;
}
export function AdminUserActions({ userId, currentRole, isBanned }: AdminUserActionsProps) {
const [loading, setLoading] = useState(false);
// ... handleAction helper
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" disabled={loading}>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
<DropdownMenuSeparator />
{currentRole !== "admin" ? (
<DropdownMenuItem onClick={setAdmin}>
<ShieldCheck className="h-4 w-4 mr-2" />
Hacer admin
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={setUser}>
<ShieldOff className="h-4 w-4 mr-2" />
Quitar admin
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
{isBanned ? (
<DropdownMenuItem onClick={unbanUser}>
<UserCheck className="h-4 w-4 mr-2" />
Desbanear
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={banUser} className="text-destructive">
<Ban className="h-4 w-4 mr-2" />
Banear usuario
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}Las acciones disponibles son:
| Accion | Metodo | Descripción |
|---|---|---|
| Hacer admin | authClient.admin.setRole({ userId, role: "admin" }) | Promueve a un usuario al rol admin |
| Quitar admin | authClient.admin.setRole({ userId, role: "user" }) | Revierte al rol user |
| Banear | authClient.admin.banUser({ userId }) | Banea al usuario |
| Desbanear | authClient.admin.unbanUser({ userId }) | Desbanea al usuario |
Despues de cada accion, la página se recarga con window.location.reload() para mostrar los datos actualizados.
Personalizacion
Conectar estadisticas reales
Reemplaza los valores placeholder en app/(admin)/admin/page.tsx con queries de Prisma:
const totalUsers = await db.user.count();
const activeSubscriptions = await db.subscription.count({
where: { status: "active" },
});Añadir nuevas páginas admin
- Crea la página en
app/(admin)/admin/tu-pagina/page.tsx - Añade la ruta al array
adminNavItemsenapp/(admin)/layout.tsx:
const adminNavItems = [
{ href: "/admin", label: "Overview", icon: BarChart3 },
{ href: "/admin/users", label: "Usuarios", icon: Users },
{ href: "/admin/tu-pagina", label: "Tu Página", icon: TuIcono },
{ href: "/admin/settings", label: "Configuración", icon: Settings },
];Añadir nuevas acciones de usuario
Extiende el dropdown en user-actions.tsx:
<DropdownMenuItem onClick={() => handleAction(tuAccion, "Mensaje de exito")}>
<TuIcono className="h-4 w-4 mr-2" />
Tu acción personalizada
</DropdownMenuItem>Seguridad
- Las rutas admin deben protegerse verificando
session.user.role === "admin"en el layout o proxy - Las acciones de usuario usan
authClient.adminque verifica el rol en el servidor a traves de Better Auth - Solo existen dos roles:
adminyuser