Saltar al contenido

Un flujo de auth completo: login, sesión, middleware y logout

La síntesis del nivel: montar la autenticación de punta a punta con las piezas ya vistas. Un `login` modelado como `action` que verifica credenciales, siembra la sesión con `session.update` y hace `throw redirect`; un formulario con mejora progresiva que muestra el error devuelto vía `useSubmission`; un middleware que puebla `event.locals.user` desde la sesión como acelerador de la petición; el guardia `requireUser` que decide junto al dato sin depender del middleware; y un `logout` que hace `session.clear` y redirige. Cinco piezas, un solo bucle: la autoridad se acuña una vez y se verifica en todas partes.

El mapa del flujo

Antes del código, la coreografía. El usuario envía sus credenciales; la acción de servidor las verifica y, si son válidas, escribe su identidad en la sesión sellada y lo redirige a la zona privada. En cada petición posterior, el middleware lee esa sesión y deja el usuario resuelto en event.locals, y el guardia de la ruta vuelve a confirmar junto al dato. El logout limpia la sesión y devuelve al login.

Login: una action que verifica y siembra la sesión

El login es una mutación: cambia el estado del servidor, así que se modela con action. Verifica las credenciales; si fallan, devuelve un Error para que el formulario lo pinte; si aciertan, siembra la sesión y lanza una redirección. Esa asimetría es deliberada: devolver comunica un resultado que la interfaz muestra; lanzar un redirect cambia de página.

TypeScript
// src/lib/auth.tsimport { action, redirect } from "@solidjs/router";import { usarSesion } from "./sesion";import { verificarCredenciales } from "./db";export const loginAction = action(async (formData: FormData) => {  "use server";  const email = String(formData.get("email"));  const password = String(formData.get("password"));  const user = await verificarCredenciales(email, password);  if (!user) return new Error("Credenciales invalidas"); // el form lo muestra  const sesion = await usarSesion();  await sesion.update({ userId: user.id });   // acuna autoridad: siembra la sesion  throw redirect("/panel");                   // exito: cambia de pagina}, "login");export const logoutAction = action(async () => {  "use server";  const sesion = await usarSesion();  await sesion.clear();       // revoca la autoridad de este cliente  throw redirect("/login");}, "logout");

El formulario ata la acción con action={loginAction} y method="post", lo que le da mejora progresiva: funciona incluso sin JavaScript, porque es un envío de formulario real. useSubmission expone el envío en vuelo —su pending para deshabilitar el botón, su result para leer el Error devuelto—.

TSX
// src/routes/login.tsximport { useSubmission } from "@solidjs/router";import { Show } from "solid-js";import { loginAction } from "~/lib/auth";export default function Login() {  const enviando = useSubmission(loginAction);  return (    <form action={loginAction} method="post">      <input name="email" type="email" required />      <input name="password" type="password" required />      <button disabled={enviando.pending}>Entrar</button>      <Show when={enviando.result instanceof Error}>        <p role="alert">{(enviando.result as Error).message}</p>      </Show>    </form>  );}

El middleware que puebla el usuario

Con la sesión sembrada, cada petición de servidor puede resolver al usuario una sola vez y dejarlo listo para todos. Ese es el trabajo del middleware: leer la sesión y depositar el resultado en event.locals.user. Es un acelerador, no un guardia; su función es ahorrar relecturas, no autorizar.

TypeScript
// src/middleware/index.tsimport { createMiddleware } from "@solidjs/start/middleware";import { usarSesion } from "~/lib/sesion";import { buscarUsuario } from "~/lib/db";export default createMiddleware({  onRequest: async (event) => {    const sesion = await usarSesion();    const userId = sesion.data.userId;    event.locals.user = userId ? await buscarUsuario(userId) : null;  },});

Recuerda el getUser de la lección anterior: usaba event.locals.user como atajo pero volvía a leer la sesión si el atajo faltaba. Ahí está la robustez del diseño: si el middleware corrió, el guardia va rápido; si no corrió —una navegación de cliente—, el guardia lee la sesión él mismo. La corrección nunca cuelga del middleware; el middleware solo la abarata.

Logout y la ruta protegida: el círculo se cierra

El logout es la acción inversa del login: session.clear revoca la autoridad y redirect devuelve al login. Y la ruta protegida reutiliza requireUser en su preload, de modo que solo un usuario con sesión válida la ve. El bucle está cerrado: se entra sembrando la sesión, se permanece porque el guardia la valida, se sale limpiándola.

TSX
// src/routes/panel.tsximport { createAsync, type RouteDefinition } from "@solidjs/router";import { logoutAction } from "~/lib/auth";import { requireUser } from "~/lib/guardias";export const route = {  preload: () => requireUser(),   // guardia: sin sesion, redirige a login} satisfies RouteDefinition;export default function Panel() {  const user = createAsync(() => requireUser());  return (    <>      <h1>Hola, {user()?.email}</h1>      <form action={logoutAction} method="post">        <button>Salir</button>      </form>    </>  );}