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.
// 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—.
// 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.
// 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.
// 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> </> );}