Introducción

Cuando trabajamos creando aplicaciones SPA con React, el router por excelencia es React-Router-DOM, es fácil de usar, potente y versátil.


Uno de los escenarios que nos podemos encontrar en nuestra aplicación es el de manejar rutas de dos tipos:

  • Rutas públicas: es decir páginas en las que el usuario no tiene porque haber introducido su usuario y clave (por ejemplo la propia página de login).

  • Rutas privadas: para poder acceder a estas rutas el usuario ha tenido que hacer login con éxito previamente.


En esta serie de posts vamos a ver cómo manejar esto aplicando una solución simple que nos evite estar copiando y pegando código en cada página.

Nota: los ejemplos en este post están desarrollados usando TypeScript, en la sección de recursos encontrarás la versión ES6 del mismo.

Importante: si está trabajando con la versión 6 de React, hay cambios de calado, en está video puedes ver como implementar un interceptor para esa versión: https://www.lemoncode.tv/curso/react-router-v6/leccion/interceptores

TL;DR;

En este post resolvemos dos problemas:

  • Donde guardar la información de sesión usuario, para ello utilizaremos el contexto de React.

  • Cómo redirigir a la ventana de login si el usuario intenta acceder a una página que necesita previa autenticación, para ello crearemos un wrapper para nuestras rutas.

Aquí tienes un ejemplo que implementa la solución que vamos a detallar en este post:

Agenda

  • Manos a la obra

    • Punto de partida

    • Almacenando información de login.

    • Interceptando la navegación.

  • Siguientes pasos

  • Recursos


Manos a la obra

Punto de partida

Vamos a partir de una aplicación que tiene tres páginas:

  • Página de login: puedes acceder a ella sin estar autenticado.

  • Página de listado: tienes que estar autenticado para poder acceder a ella.

  • Página de detalle: tienes que estar autenticado para poder acceder a ella.

Este punto de partida lo tenéis disponible:

Almacenando la información de login

Cuando un usuario navega a la página de login, introduce su usuario y clave, la aplicación lanza una petición contra la API de servidor, y comprueba que todo es correcto y envía respuesta a nuestra aplicación web, hasta aquí todo bien, pero cuando navego a otra página ¿Cómo puede saber dicha página si el usuario se ha autenticado correctamente?

Lo ideal para almacenar esta información y que este disponible desde cualquier punto de la aplicación es usar el contexto de React, de esta manera:

  • Podemos elegir a qué nivel exponemos la información.

  • En el momento que hay algún cambio, automáticamente, se actualiza en los componentes que la estén consumiendo.

Vamos a definir un context en el que vamos a almacenar el nombre del usuario autenticado:

Flujo, en el contexto añadimos la información de usuario, la exponemos a nivel de aplicación global vía un componente provider, y después la consumimos desde cualquier parte de la aplicación con useContext

Flujo, en el contexto añadimos la información de usuario, la exponemos a nivel de aplicación global vía un componente provider, y después la consumimos desde cualquier parte de la aplicación con useContext

./src/core/authcontext.tsx

import React from "react";

interface Context {
  userInfo: string;
  setUserInfo: (user: string) => void;
}

export const AuthContext = React.createContext<Context>({
  userInfo: "",
  setUserInfo: (user: string) =>
    console.log("Did you forgot to add AuthContext on top of your app?"),
});

export const AuthProvider: React.FunctionComponent = (props) => {
  const { children } = props;
  const [userInfo, setUserInfo] = React.useState<string>("");

  return (
    <AuthContext.Provider value={{ userInfo, setUserInfo }}>
      {children}
    </AuthContext.Provider>
  );
};

En nuestra aplicación supondremos que si el nombre del usuario es una cadena vacía no esta autenticado, en una aplicación real en vez de un string lo suyo sería crear un objeto de sesión en el que almacenáramos el id del usuario, su rol, etc...


Creamos un barrel en core para que sea más cómodo de importar desde otros puntos de la aplicación:


./src/core/index.ts

export * from "./authcontext";

Ese proveedor de contexto lo vamos a poner a nivel de raíz de nuestra aplicación (AuthProvider)


./src/app.tsx

import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { LoginPage, ListPage, DetailPage } from "./pages";
import {AuthProvider} from "./core";

export const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Switch>
          <Route exact path="/">
            <LoginPage />
          </Route>
          <Route path="/list">
            <ListPage />
          </Route>
          <Route path="/detail">
            <DetailPage />
          </Route>
        </Switch>
      </Router>
    </AuthProvider>
  );
};

export default App;

En la página de login, vamos asignaremos el userId del contexto cuando el usuario se valida satisfactoriamente contra el servidor:


- Nos traemos el import de core.

./src/pages/login.tsx

import { AuthContext } from "../core";

- Con _useContext_ nos traemos del context la función para asignar el usuario a nuestro contexto:

./src/pages/login.tsx

export const LoginPage: React.FC = () => {
  const history = useHistory();
  const { setUserInfo } = React.useContext(AuthContext);

- En la función en la que navegamos a la página de listado si el login es correcto, asignamos al contexto el nombre de usuario logado (setUserInfo):

./src/pages/login.tsx

  const handleNavigation = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Just a mock / dummy password check
    if (username === "admin" && password === "test") {
      setUserInfo(username); // That's the important part
      history.push("/list");

Interceptando la navegación

Ya que tenemos el nombre del usuario autenticado accesible desde cualquier punto de la aplicación, vamos a hacer un wrapper sobre el componente Router de React-Router ¿ Qué narices es eso de un wrapper?

  • Tu tienes el componente Route de react-router, te va casí perfecto pero te hace falta añadir una comprobación: ¿Está el usuario autenticado?

  • Te creas otro componente, que puedes llamar AuthRouteComponent y en ese componente añades la comprobación de que existe usuario (si no rediriges a login).

  • En el return pintas el componente de Route de _react-router_.

  • Es decir estás "envolviendo el Router en papel de regalo" ;).

Veamos como queda esto en código:

/src/core/authroute.tsx

import React from "react";
import { Route, RouteProps, useHistory } from "react-router-dom";
import { AuthContext } from "../core";

export const AuthRouteComponent: React.FunctionComponent<RouteProps> = (
  props
) => {
  const { userInfo } = React.useContext(AuthContext);
  const history = useHistory();

  React.useEffect(() => {
    if (!userInfo) {
      history.push("/");
    }
  }, [props?.location?.pathname]);

  return <Route {...props} />;
};

Añadimos este componente al index de nuestra carpeta core:


./src/core/index.ts

export * from "./authcontext";
export * from "./authroute";

¿Qué hacemos aquí?

  • Este componente va a sustituir a Route en el switch de React Router en las rutas que necesiten que el usuario este autenticado.

  • Cuando se cambia de ruta y se vaya a activar esta página se ejecutara la función que vemos en el useEffect.

  • Comprobamos si el usuario se ha autenticado previamente, y si no redirigimos a la página de login.

Ya sólo tenemos que sustituir las rutas que necesitan que el usuario este autenticado, en vez de usar _Route_ usamos el wrapper que hemos creado previamente _AuthRouteComponent_

Route para rutas publicas, AuthRouteComponent para rutas que necesiten que el usuario esté autenticado

Route para rutas publicas, AuthRouteComponent para rutas que necesiten que el usuario esté autenticado

./src/app.tsx

import React from "react";
import "./App.css";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { LoginPage, ListPage, DetailPage } from "./pages";
import { AuthProvider, AuthRouteComponent } from "./core";

export const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Switch>
          <Route exact path="/">
            <LoginPage />
          </Route>
          <AuthRouteComponent path="/list">
            <ListPage />
          </AuthRouteComponent>
          <AuthRouteComponent path="/detail">
            <DetailPage />
          </AuthRouteComponent>
        </Switch>
      </Router>
    </AuthProvider>
  );
};

export default App;

Para ver como funciona, puedes probar el "happy path":

  • Vas a la página de login.

  • Te logas satisfactoriamente.

  • Puedes navegar a la páginas protegidas sin problemas.

Si el usuario introduce su login y password satisfactoriamente puede navegar a cualquier ventana

Si el usuario introduce su login y password satisfactoriamente puede navegar a cualquier ventana


Vamos ahora por el caso "malo":

  • Voy a la página de login.

  • No me logo.

  • Pego una ruta protegida.

  • Me redirige a la página de login.

El usuario intenta saltarse la ventana de login y directamente acceder a una ruta automáticamente se le redirige a la página de login

El usuario intenta saltarse la ventana de login y directamente acceder a una ruta automáticamente se le redirige a la página de login

Siguientes Pasos

Ahora que hemos resuelto este caso, nos encontramos con nuevos desafíos:

  • ¿Qué pasa si el usuario tiene su token de sesión en una cookie o en storage? Es decir que si pega el enlace en un navegador ya esta autenticado, aunque no haya pasado por el login, no deberíamos de redirigir a la página de login en ese caso.

  • ¿Qué pasa si el usuario se autentica, está trabajando con la aplicación y le expira la sesión? Aquí tendríamos que detectar la respuesta de 401 y redirigir a la página de login.

  • ¿Qué pasa si el usuario pega un enlace a una página privada y no está autenticado? ... Por un lado redirigirlo a la página de login, pero recordar la última página a la que fue para una vez validado su usuario y clave redirigirle a esa página de destino inicial.

  • ¿Cómo podemos manejar roles?

Si os ha gustado este post iremos avanzando en la serie resolviendo estos problemas.

Recursos

El ejemplo básico implementado en TypeScript:

El ejemplo básico en ES6:

Si queréis ver el mismo proyecto pero con Material UI y más elaborado:

¿Front, Devops o Back?


Si tienes ganas de ponerte al día ¿Te apuntas a alguno de nuestros Másters Online o Bootcamps?


- Máster Front End Online Lemoncode

- Bootcamp Backend Online Lemoncode

- Bootcamp Devops Online Lemoncode