No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Navigate y redirects: protegiendo rutas privadas

13/30
Recursos

Aportes 17

Preguntas 5

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Navigate y redirects: protegiendo rutas privadas

Como mencionamos anteriormente, si un usuario intenta acceder a una ruta privada por medio del historial o la barra de búsqueda aún si esta no esta renderizada o no puede acceder lo que va a causar es que nuestra aplicación este en blanco, colapse y ya no se pueda interactuar hasta que recarguemos.

Para solucionar esto lo que haremos es protegerlas de verdad haciendo que si intentan acceder a una ruta parvada haga un redirect al home, a login o a profile dependiendo de la ruta donde intente acceder.

Para esto vamos primero a nuestro Profile.js:

// Importamos el componente navigate
import { useAuth } from '../../auth/auth';

function ProfilePage() {
  const auth = useAuth();

  /* Preguntamos si hay un usuario registrado, si no, redirigimos 
	a la persona */ 
  if (!auth.user) {
    /* Con este componente podemos hacer el redirect hacia donde la 
		lógica de nuestra aplicación lo requiera */ 
    return <Navigate to="/login" />
  }

  return (
    <>
      <h1>Perfil</h1>
      <h1>Welcome {auth.user.username}</h1>
    </>
  );
}

De esta manera si no nos hemos registrado e intentamos acceder a Profile por medio de la barra de búsqueda nos redirigirá a nuestro login.

En caso de que necesitemos redireccionar varias veces a distintos sitios de nuestra aplicación veremos que hacer esto puede ser un poco repetitivo, debemos solucionar esto.

Vamos a auth.js y creemos una función que nos permita hacer esto más sencillo.

import { Navigate } from 'react-router-dom';

...
/* En este compoinente podemos hacer la validación que es la misma 
que utilizamos con anterioridad */
function AuthRoute(props) {
  const auth = useAuth();

  if (!auth.user) {
    return <Navigate to="/login" />
  }

  return props.children
}

export {
	...
  AuthRoute,
}

Luego simplemente implementamos este componente en el ProfilePage.js:

import { AuthRoute, useAuth } from '../../auth/auth';

function ProfilePage() {
  const auth = useAuth();

  return (
    <AuthRoute>
      <h1>Perfil</h1>
      <h1>Welcome {auth.user.username}</h1>
    </AuthRoute>
  );
}

export { ProfilePage }

Si lo ejecutamos tendremos un error debido a que al renderizar el componente aún así lo redireccionemos como auth.user.username no existe y esto nos dará un error ya que en el componente lo estamos solicitando y esto nos genera un erro bloqueante al redireccionar a login.

Pero para que funcione esto en nuestro llamado desde App.js debemos importar a este componente ProfilePage dentro de un componente AuthRoute:

import { AuthProvider, useAuth, AuthRoute } from './Components/auth/auth'
...

function App() {
  return (
    <>
      <HashRouter>
        <AuthProvider>
          <Routes>
            ...
            <Route path='/profile' 
              element={
                <AuthRoute>
                  <ProfilePage /> {/* <-- */}
                </AuthRoute>
              } 
            />
          </Routes>
        </AuthProvider>

      </HashRouter>
    </>
  )
}

Y nuestro componente ProfilePage.js lo podemos dejar como estaba:

function ProfilePage() {
  const auth = useAuth();

  return (
    <>
      <h1>Perfil</h1>
      <h1>Welcome {auth.user.username}</h1>
    </>
  );
}

Ahora haremos lo mismo con el Logout en App.js:

<Route path='/logout' 
  element={
    <AuthRoute>
      <LogoutPage />
    </AuthRoute>
  } 
/>

Lo único que nos queda por hacer es que cuando estemos autenticados no podamos acceder a la página de Login, porque estamos protegiendo las rutas privadas de un usuario público pero no la ruta pública de registro a un usuario privado. ¡Hagamos esto!

LoginPage.js

function LoginPage() {
	...	

  /* Como en este caso solo necesiamos hacer una validación lo 
  haremos directo en el componente preguntando si el usuario ya esta 
  registrado, si lo está lo redirigiremos a la página de Profile 
  usando el Navigate */
  if(auth.user) {
    return <Navigate to="/profile" />
  }

  return (
		...
	)
}

Y hemos terminado de proteger nuestras rutas y tener una mejor lógica de aplicación.

Navigate y redirects: protegiendo rutas privadas

Es muy importante poder proteger rutas que a las que no queremos que el usuario sin autenticar tenga acceso.

Una forma para solucionar esto seria la utilización del componente “Navigate” (Con N mayúscula), por medio de un if, donde evaluamos si el usuario está autenticado o no, vamos a indicarle a nuestro navegador que queremos redireccionar al usuario hacia “/login”

const auth = useAuth()

  if(!auth.user){
    return <Navigate to="/login"/>
  }

Esto ya es una solución, pero existe otra forma más simple de trabajar:

Dentro de nuestro archivo auth vamos a crear un componente, se podría llamar “AutenticadorDeRutas” o “AuthRoute”, y dentro de este vamos a incluir la lógica que aprendimos agregando que, en el caso de que si se pueda ver el contenido, se retorne el “props.children”

function AuthRoute(props) {
  const auth = useAuth();

  if (!auth.user) {
    return <Navigate to="/login" />
  }

  return props.children
}

Por ultimo, tengo que decir que el componente AuthRouter es mejor llamarlo, justamente, en nuestro Router. De esta manera nos ahorramos problemas con valores que, asumimos, ya deberían existir en nuestros componentes si son renderizados.

<Route path='/profile' 
	element={
	  <AuthRoute>
	   <ProfilePage/>
	  </AuthRoute>
}/>

()= parentesis
{}=corchetes
[]=llaves

Es la segunda vez que veo la clase en semanas diferentes y todavia me sorprendo de lo buena que es !!!

Felicitaciones!!

Código de la clase en TypeScript
(Yo separe las partes relacionados al auth en useAuth, AuthStore y AuthRoute)
AuthRoute.tsx

import { Navigate } from "react-router-dom";
import { useAuth } from "./useAuth"
import { FC, PropsWithChildren } from "react";

const AuthRoute:FC<PropsWithChildren> = ({ children }) =>{

    const auth = useAuth();

    if(!auth.user) return <Navigate to="/login" />

    return  children;
}

export { AuthRoute }

LoginPage.tsx

const LoginPage = () => {

    const auth = useAuth();
    const [username, setUsername] = useState('');

    const login = (e:FormEvent<HTMLFormElement>)=>{
        e.preventDefault();
        auth.login({username});
    }

    if(auth.user) return <Navigate to="/profile" />

    return(
        <>
            <h1>Login</h1>

            <form onSubmit={login}>
                <label>Escribe tu nombre de usuario</label>
                <input
                value={username}
                onChange={ e => setUsername(e.target.value)}
                />
                <button type="submit">Entrar</button>
            </form>
        </>
    )
}

App.tsx

function App() {

  return (
    <>
      <HashRouter>
        <AuthProvider>
          <Menu />

          <Routes>
            <Route path='/' element={<HomePage />} />

            <Route path='/blog' element={<BlogPage />}>
              <Route path=':slug' element={<BlogPost />} />
            </Route>
            
            <Route path='/login' element={<LoginPage />} />
            <Route path='/logout' element={
              <AuthRoute>
                <LogoutPage />
              </AuthRoute>
            } />
            <Route path='/profile' element={
              <AuthRoute>
                <ProfilePage />
              </AuthRoute>
            } />


            <Route path='*'  element={<p>Not found</p>} />
          </Routes>
        </AuthProvider>
      </HashRouter>
    </>
  )
}

export default App

Nuestra principal estrategia en este caso fue el usar validaciones para permitir o no que una persona pueda acceder a diferentes paginas dependiendo de si está logeada o no

esta clase es mágica

Navigate y redirects: protegiendo rutas privadas

.
Vamos a proteger las rutas de manera que si un usuario trata de acceder directamente por la url, entonces se le redirija al home, o la página que corresponda.
.
En primer lugar se podría pensar en condicionar el renderizado del componente y mostrar un mensaje que diga que no se le permite ver dicho contenido, pero no es realmente la mejor forma de hacerlo.
.

function ProfilePage() {
  const auth = useAuth();
  
  if (auth.user) {
	  return (
		  <>
	      <h1>Perfil</h1>
	      <p>Welcome, {auth.user.username}</p>
	    </>
	  );
  } else {
	  return (
		  Por favor vete
	  );
  }
}

.
Otra forma es utilizar el componente Navigate de React Router DOM para navegar automáticamente al home en el caso de que no se hayan logueado.
.

function ProfilePage() {
  const auth = useAuth();
  if (!auth.user) {
	  return <Navigate to="/login" />;
  }
  
  return (
    <>
      <h1>Perfil</h1>
      <p>Welcome, {auth.user.username}</p>
    </>
  );
}

.
Si bien funciona bien, no es muy conveniente hacer esto cuando tenemos demasiadas rutas privadas; puede volverse tedioso.
.
Entonces, la solución es crear un componente AuthRoute en el archivo auth.js. Este componente verifica si el usuario está logueado y si lo está entonces lo hace navegar al login, y sino retorna a props.children.
.

...

function AuthRoute(props) {
  const auth = useAuth();

  if (!auth.user) {
    return <Navigate to="/login" />;
  }

  return props.children;
}

export {
  AuthProvider,
  AuthRoute,
  useAuth,
};

.
Utilizamos a AuthRoute para envolver los elementos de la página de perfil. Sin embargo, esto no va a funcionar porque auth.user es null, y por ende no se puede obtener la propiedad username.
.
Aún si AuthRoute nos debería hacer navegar al home, lo que pasa es que React antes de entrar al código de AuthRoute revisa primero que todas los elementos y variables de ProfilePage sean accesibles. Por lo cual al no poder acceder a username la aplicación no responde, y siquiera estaríamos entrando al código de AuthRoute para navegar al home.
En otras palabras, cuando React intenta renderizar ProfilePage, primero ejecuta todo el código dentro de él. Esto incluye la llamada a useAuth y el intento de acceder a auth.user.username.
.
Si auth.user no está definido (es null), el intento de acceder a username causará un error que detendrá la ejecución de ProfilePage. Esto significa que React no continuará ejecutando el código dentro de ProfilePage después de ese punto. Como resultado, el código dentro de AuthRoute nunca se alcanza y, por lo tanto, la redirección a la página de inicio no se activa.
.

function ProfilePage() {
  const auth = useAuth();
  
  return (
    <AuthRoute>
      <h1>Perfil</h1>
      <p>Welcome, {auth.user.username}</p>
    </AuthRoute>
  );
}

.
Una solución rápida sería utilizar el optional chaining, sin embargo sigue sin ser la mejor solución.
.

<p>Welcome, {auth.user?.username}</p>

.
La mejor forma para solucionar esto es asumir que auth.user.username siempre estará disponible, y no utilizar el AuthRoute en ProfilePage.
.
Entonces, desde App vamos a envolver el componente que va renderizar la propiedad element mediante el componente AuthRoute.
.

<Routes>
<Route path="/" element={<HomePage />} />
	...
  <Route
	  path="/logout"
    element={
	    <AuthRoute>
	      <LogoutPage />
      </AuthRoute>
    }
  />
  <Route
	  path="/profile"
	  element={
		  <AuthRoute>
			  <ProfilePage />
		  </AuthRoute>
	  }
  />
  ...
</Routes>

.
De esta forma ya no es necesario modificar el ProfilePage o ningún otro componente envuelto en AuthRoute desde App.
.

function ProfilePage() {
  const auth = useAuth();
  
  return (
    <>
      <h1>Perfil</h1>
      <p>Welcome, {auth.user.username}</p>
    </>
  );
}

.
Por último, queda proteger a la página de login cuando estemos autenticados. Entonces, es suficiente crear un condicional dentro de LoginPage para navegar al profile del usuario. Lo hacemos de esta manera solo porque es un caso particular de esta ruta.
.

function LoginPage() {
	...
  if(auth.user){return<Navigate to="/profile"/>}
  
  return (
    ...
  );
}

Nota:
A los que estaban usando BrowserRouter no les va a salir la solicion del profe, tendran que cambiar por HashRouter para que salga.
Si desean que funcione con BrowserRouter tendria que hacer uso de localStorage

el problema del minuto 6:32 creo que es porque en ves de llamar a auth.user le agrega una propiedad undefined en este caso auth.user.username

Mi solucion fue esta:

Mi solucion fue esta: 
<ProtectRoute>
  {!!userName ? (
    <h2>Bienvenido {userName}</h2>
  ) : (
    <p>Porfavor logueate!</p>
  )}
</ProtectRoute>

incluso una render function me parecería mas limpia para este caso

Que bueno, este error me toco en algunas paginas, ahora no recuerdo bien cuales, pero al estar logeado tenia la opcion de login

el componente de AuthRoute junto a los demas tambien pudo hacerse hecho con react higher order components?? supongo
En mi caso, sí funcionó desde la primera iteración. Este fue mi código (construido con TS - TypeScript): ```js //auth.tsx import React, { createContext, ReactNode, useContext, useState } from 'react'; import { useNavigate, Navigate } from 'react-router-dom'; // aqui se define que tipo de dato es children type AuthProviderProps = { children: ReactNode; } // aqui se definen los tipos de datos que reciben las funciones y useState de Auth Provider type AuthContextType = { // el tipo de dato es un objeto con un tipo string o un valor nulo user: {username: string} | null; // login recibe una funcion funcion con "credenciales" que son un objeto con un dato tipo string login: (credentials: { username: string }) => void; // logout recibe un valor del tipo funcion logout: () => void; } // aqui dice que createContext puede recibr los tupos definidos en AuthContextType o puede ser indefinido, y por default lo haces indefinido const AuthContext = createContext<AuthContextType | undefined>(undefined); // React FC ayuda en typescript a definir un Function Component y permite pasar props prededifinas, en este caso es necesario para definir a "children" const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { const navigate = useNavigate(); const [ user, setUser ] = useState<{username: string} | null>(null); const login = (credentials: { username: string }) => { setUser({ username: credentials.username }) navigate('/profile'); } const logout = () => { setUser(null) navigate('/'); } const auth = { user, login, logout } return( <AuthContext.Provider value={auth}> {children} </AuthContext.Provider> ) } const useAuth = () => { const auth = useContext(AuthContext); return auth; } const ProtectedRoute = (props: any) => { const auth = useAuth(); if (!auth?.user) { return <Navigate to='/login'/> } return(props.children) } export { AuthProvider, useAuth, ProtectedRoute } // ProfilePage.tsx import React from 'react' import { useAuth, ProtectedRoute } from './hooks/auth' const ProfilePage: React.FC = () => { const auth = useAuth(); return( <ProtectedRoute>

Welcome to ProfilePage {auth?.user?.username}

{/* aqui dices que auth? y user? puede ser undefined y por eso puedes renderizar el componente aunque no reciba valores de useAuth */} </ProtectedRoute> ) } export { ProfilePage } ```

No funciona porque a pesar de que si hace el redireccionamiento hacia login, igual sigue leyendo el resto del codigo del componente y cuando un usuario no se ha logeado, ese auth.user es null y en la linea 10 está intentando leer una propiedad que es nula