Saltar al contenido

Proteger rutas y funciones de servidor con guardias

Autorizar es responder «¿puede este usuario ver esto?» en el único sitio donde la respuesta no se puede eludir: junto al dato. Esta lección construye guardias reutilizables sobre la sesión —una `query` `getUser` que resuelve al usuario y un `requireUser` que hace `throw redirect('/login')` cuando no hay sesión— y los aplica en las dos superficies protegibles de SolidStart: dentro de una función de servidor, que comprueba antes de devolver, y en el `preload` de una ruta, donde el `throw` dispara la redirección del router. Cierra distinguiendo el guardia real, en el servidor, del `Show` del cliente, que solo esconde interfaz.

La autoridad vive junto al dato

Un guardia es una función de servidor que resuelve al usuario actual desde la sesión y decide. Lo modelamos como una query para heredar su deduplicación y su caché por petición: si varias partes de la página preguntan «¿quién es el usuario?», la sesión se lee una sola vez. getUser devuelve el usuario o null; nunca lanza, porque hay sitios —una barra de navegación— donde «no hay usuario» es un estado legítimo, no un error.

TypeScript
// src/lib/guardias.tsimport { query, redirect } from "@solidjs/router";import { getRequestEvent } from "solid-js/web";import { usarSesion } from "./sesion";import { buscarUsuario } from "./db";export const getUser = query(async () => {  "use server";  const event = getRequestEvent();  // atajo: si el middleware ya lo resolvio, reutilízalo  if (event?.locals.user) return event.locals.user;  const sesion = await usarSesion();  const userId = sesion.data.userId;  if (!userId) return null;  return buscarUsuario(userId);   // tu acceso a datos}, "user");

Un guardia reutilizable: requireUser

Cuando «no hay usuario» sí debe cortar el paso, envuelves getUser en un guardia que lanza. requireUser es también una query, y su throw redirect("/login") es una redirección que el router y las funciones de servidor entienden nativamente: no es una excepción que atrapar, es una respuesta HTTP que se propaga. Como devuelve el usuario ya comprobado, a partir de su llamada el tipo es no-nulo y programas sin interrogantes.

TypeScript
// src/lib/guardias.ts (continuacion)export const requireUser = query(async () => {  "use server";  const user = await getUser();  if (!user) throw redirect("/login");   // corta y redirige  return user;                           // de aqui en adelante, no-nulo}, "require-user");

Definir el guardia una vez y reutilizarlo es la clave de que sea infalible: la comprobación existe en un solo lugar, así que no hay forma de que una ruta nueva «se olvide» de protegerse copiando mal el patrón. Centralizar la política es centralizar la corrección.

Proteger una función de servidor

La primera superficie protegible es la propia función de servidor. Llamas a requireUser en su primera línea; si no hay sesión, el throw redirect aborta antes de que ninguna consulta sensible llegue a ejecutarse. La barrera está literalmente delante del dato.

TypeScript
// src/lib/panel.tsimport { query } from "@solidjs/router";import { requireUser } from "./guardias";import { cargarPanel } from "./db";export const getPanel = query(async () => {  "use server";  const user = await requireUser();     // lanza redirect si no hay sesion  return cargarPanel(user.id);          // solo se alcanza autenticado}, "panel");

Proteger una ruta con preload

La segunda superficie es la ruta. Una ruta de SolidStart puede exportar un objeto route con un preload que arranca la carga de datos antes de renderizar. Si ese preload invoca una query que hace throw redirect, el router captura la redirección y lleva al usuario al login sin pintar la página protegida. El mismo guardia que blinda la función de servidor blinda la ruta.

TSX
// src/routes/panel.tsximport { createAsync, type RouteDefinition } from "@solidjs/router";import { For, Suspense } from "solid-js";import { getPanel } from "~/lib/panel";export const route = {  preload: () => getPanel(),            // dispara el guardia antes de renderizar} satisfies RouteDefinition;export default function Panel() {  const panel = createAsync(() => getPanel());  return (    <Suspense fallback={<p>Cargando panel…</p>}>      <For each={panel()?.items}>{(i) => <li>{i.nombre}</li>}</For>    </Suspense>  );}

El Show del cliente es UX, no seguridad

En el cliente querrás ocultar un enlace de administración a quien no es admin. Eso se hace con Show leyendo getUser, y está bien —mejora la experiencia—. Pero ten clarísimo su estatus: esconder no es proteger. El Show solo decide qué se pinta; cualquiera puede navegar a la URL a mano o llamar a la función de servidor directamente.

TSX
import { Show } from "solid-js";import { createAsync } from "@solidjs/router";import { getUser } from "~/lib/guardias";function EnlaceAdmin() {  const user = createAsync(() => getUser());  // solo OCULTA el enlace; NO protege la ruta /admin ni sus datos  return (    <Show when={user()?.rol === "admin"}>      <a href="/admin">Administracion</a>    </Show>  );}

La ruta /admin y cada función de servidor que sirva sus datos deben volver a comprobar la autoridad con requireUser (y, para roles, un requireAdmin análogo). El Show y el guardia de servidor no compiten: cooperan. Uno pule la experiencia; el otro es la ley.