EmpiezaTuSaaS

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:

NivelArchivoFuncion
Proxyproxy.tsRedirige usuarios no autenticados antes de renderizar la página
Server ComponentLayout o PageObtiene 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:

proxy.ts
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:

  • getSessionCookie solo 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.
  • callbackUrl guarda 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:

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:

lib/auth/access.ts
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:

app/[locale]/mi-pagina/page.tsx
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:

lib/auth/access.ts
const protectedRoutes = [
  "/dashboard",
  "/settings",
  "/billing",
  "/admin",
  "/mi-pagina",
];

Prueba la protección

  1. 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.
  2. 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):

app/[locale]/(dashboard)/mi-pagina/page.tsx
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:

app/[locale]/(dashboard)/admin-only/page.tsx
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 usuario

El 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 /login está en authRoutes, no en protectedRoutes
  • 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() (con await) en Next.js 16
  • Verifica que BETTER_AUTH_URL coincide con la URL de tu app
  • Confirma que la cookie better-auth.session_token existe en el navegador

La ruta es accesible sin login

  • Verifica que añadiste la ruta al array protectedRoutes en lib/auth/access.ts
  • Recuerda que isProtectedRoute usa startsWith, asi que /mi-pagina cubre subrutas automáticamente
  • Reinicia el servidor de desarrollo después de cambiar lib/auth/access.ts

Resumen

Has aprendido a:

  • Entender como proxy.ts protege 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

On this page