Página privada
Crea rutas protegidas que requieren autenticación
Página privada
Crea páginas que solo usuarios autenticados puedan ver. Al terminar tendrás:
- Una ruta protegida con redirección automatica al login
- Un Server Component que muestra datos del usuario
- Entendimiento de
proxy.ts(el reemplazo del middleware en Next.js 16)
Prerequisito: haber completado Crear endpoints de API (autenticación y endpoints funcionando).
Como funciona la protección de rutas
El boilerplate protege las rutas en dos niveles:
| Nivel | Archivo | Funcion |
|---|---|---|
| Proxy | proxy.ts | Redirige usuarios no autenticados antes de renderizar la página |
| Server Component | Layout o Page | Obtiene la sesión completa y válida permisos |
En Next.js 16, proxy.ts reemplaza al antiguo middleware.ts. Se ejecuta en Node.js runtime (no Edge), lo que permite usar cualquier libreria de Node sin restricciones.
Paso 1: Entiende proxy.ts
El archivo proxy.ts en la raiz del proyecto es el primer filtro de protección:
import { NextRequest, NextResponse } from "next/server";
import { getSessionCookie } from "better-auth/cookies";
import { isAuthRoute, isProtectedRoute } from "@/lib/auth/access";
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
const protectedPath = isProtectedRoute(pathname);
const authPath = isAuthRoute(pathname);
if (!protectedPath && !authPath) {
return NextResponse.next();
}
// Verificación optimista de cookie — sin llamada HTTP
const sessionCookie = getSessionCookie(request);
const isAuthenticated = !!sessionCookie;
// Si no está autenticado y la ruta es protegida -> login
if (!isAuthenticated && protectedPath) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
// Si está autenticado y va a login/register -> dashboard
if (isAuthenticated && authPath) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}Puntos clave:
getSessionCookiesolo verifica si la cookie existe, no si es valida. Es una verificación rapida ("optimista").- La validación real de la sesión se hace en el Server Component con
auth.api.getSession. callbackUrlguarda la ruta original para redirigir despues del login.
Paso 2: Registra tu nueva ruta protegida
Las rutas protegidas se definen en lib/auth/access.ts:
const protectedRoutes = ["/dashboard", "/settings", "/billing", "/admin"];
const authRoutes = ["/login", "/register", "/forgot-password"];
export function isProtectedRoute(pathname: string): boolean {
return protectedRoutes.some((route) => pathname.startsWith(route));
}
export function isAuthRoute(pathname: string): boolean {
return authRoutes.some((route) => pathname.startsWith(route));
}Para proteger una nueva ruta, añádela al array protectedRoutes:
const protectedRoutes = [
"/dashboard",
"/settings",
"/billing",
"/admin",
"/mi-pagina", // Tu nueva ruta protegida
];isProtectedRoute usa startsWith, asi que /mi-pagina también protege /mi-pagina/subruta, /mi-pagina/otra, etc.
Paso 3: Crea tu página protegida
Crea el archivo de la página
Vamos a crear una página de perfil en /mi-pagina:
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
export const metadata = {
title: "Mi Página",
};
export default async function MiPágina() {
// Obtener sesión real (no solo la cookie)
const session = await auth.api.getSession({
headers: await headers(),
});
// Doble verificación: si no hay sesión valida, redirigir
if (!session?.user) {
redirect("/login");
}
// Consultar datos del usuario
const user = await db.user.findUnique({
where: { id: session.user.id },
select: {
name: true,
email: true,
role: true,
createdAt: true,
emailVerified: true,
subscription: {
select: { plan: true, status: true },
},
},
});
return (
<div className="max-w-2xl mx-auto py-10 px-4 space-y-6">
<h1 className="text-2xl font-bold">Mi Página</h1>
<Card>
<CardHeader>
<CardTitle>Datos de la cuenta</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Nombre</span>
<span className="font-medium">{user?.name}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Email</span>
<span className="font-medium">{user?.email}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Rol</span>
<Badge variant="outline">{user?.role}</Badge>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Plan</span>
<span className="font-medium">
{user?.subscription?.plan || "Sin plan"}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Email verificado</span>
<span className="font-medium">
{user?.emailVerified ? "Si" : "No"}
</span>
</div>
</CardContent>
</Card>
</div>
);
}Añade la ruta a protectedRoutes
Edita lib/auth/access.ts:
const protectedRoutes = [
"/dashboard",
"/settings",
"/billing",
"/admin",
"/mi-pagina",
];Prueba la protección
- Sin sesión: Abre una ventana de incógnito y ve a
http://localhost:3000/mi-pagina. Deberías ser redirigido a/login?callbackUrl=/mi-pagina. - Con sesión: Inicia sesión y ve a
/mi-pagina. Deberías ver tus datos.
Paso 4: Protección dentro del Dashboard layout
Si tu página debe estar dentro del dashboard (con sidebar), colócala en el grupo (dashboard):
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
export default async function MiPáginaDashboard() {
const session = await auth.api.getSession({
headers: await headers(),
});
const user = session?.user;
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Mi Página</h1>
<p className="text-muted-foreground">
Bienvenido, {user?.name}
</p>
{/* Tu contenido */}
</div>
);
}El layout de (dashboard) ya obtiene la sesión y renderiza el sidebar con la navegación. Solo necesitas crear tu page.tsx dentro de la carpeta.
Paywall activado por defecto: si requirePayment está en true en config/site.ts, el usuario no verá el dashboard (ni tu nueva página) hasta que tenga una suscripción activa. En su lugar ve la página de pricing. Los admins se saltan el paywall. Consulta Paywall para más detalles.
Paso 5: Página solo para admins
Para crear una página exclusiva de administradores, combina la verificación de sesión con la de rol:
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
export default async function AdminOnlyPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) {
redirect("/login");
}
if (session.user.role !== "admin") {
redirect("/dashboard");
}
return (
<div>
<h1 className="text-2xl font-bold">Solo Admins</h1>
<p>Esta página solo es visible para administradores.</p>
</div>
);
}Para una implementacion completa del panel de admin con sidebar, usuarios y acciones, consulta Panel de Admin.
Patron completo: proxy + server component
Este diagrama muestra el flujo completo cuando un usuario visita una ruta protegida:
Usuario visita /mi-pagina
|
proxy.ts
|
isProtectedRoute("/mi-pagina") -> true
|
getSessionCookie(request)
|
+-- Cookie NO existe -> Redirect a /login?callbackUrl=/mi-pagina
|
+-- Cookie existe -> NextResponse.next()
|
page.tsx (Server Component)
|
auth.api.getSession(headers)
|
+-- Sesión invalida/expirada -> redirect("/login")
|
+-- Sesión válida -> Renderizar página con datos del usuarioEl proxy es la primera línea de defensa (rapida, solo cookie check). El Server Component es la segunda (validación completa contra la DB).
Errores comunes
La página redirige infinitamente entre login y mi ruta
- Verifica que
/loginestá enauthRoutes, no enprotectedRoutes - Asegúrate de que el proxy no redirige la ruta de callback de autenticación
auth.api.getSession devuelve null aunque estoy logueado
- Asegúrate de pasar
await headers()(conawait) en Next.js 16 - Verifica que
BETTER_AUTH_URLcoincide con la URL de tu app - Confirma que la cookie
better-auth.session_tokenexiste en el navegador
La ruta es accesible sin login
- Verifica que añadiste la ruta al array
protectedRoutesenlib/auth/access.ts - Recuerda que
isProtectedRouteusastartsWith, asi que/mi-paginacubre subrutas automáticamente - Reinicia el servidor de desarrollo después de cambiar
lib/auth/access.ts
Resumen
Has aprendido a:
- Entender como
proxy.tsprotege rutas con verificación optimista de cookies - Registrar nuevas rutas protegidas en
lib/access.ts - Crear páginas que verifican la sesión con
auth.api.getSession - Redirigir usuarios no autenticados
- Proteger páginas por rol (admin)
Siguiente paso
- Pagos con Stripe - Configura pagos únicos con Stripe Checkout