remix-run / react-router

Declarative routing for React
https://reactrouter.com
MIT License
53.06k stars 10.3k forks source link

[Docs]: Protected routes example using createBrowserRouter #10637

Closed ratoi-crysty closed 1 year ago

ratoi-crysty commented 1 year ago

Describe what's incorrect/missing in the documentation

Having that currently, this is the recommended way of using react-router, we need an example of how to protect the routes created with createBrowserRouter.

Currently, the only example for auth available, is the old one using JSX <BrowserRouter/> and <Route/>.

timdorr commented 1 year ago

This is still in progress, depending on the Middleware API: https://github.com/remix-run/react-router/discussions/10327#discussioncomment-5581679

ericledonge commented 1 year ago

It's strange to recommend a new way of doing things that doesn't yet seem feasible.

onattech commented 1 year ago

There is a great example here by Bob Ziroll from freecodecamp. https://www.youtube.com/watch?v=nDGA3km5He4&t=1747s

philipp-serfling commented 1 year ago

@onattech Are you sure the link/timestamp is correct? I could not find anything regarding createBrowserRouter

frin457 commented 1 year ago

I was looking into this and wanted to leave an example for simple folk like myself. I was able to implement a <ProtectedRoute/> workflow onto the 'createBrowserRouter' react-router-remix.


const root = ReactDOM.createRoot(container!); // createRoot(container!) if you use TypeScript
const dummyUser = {loggedIn: true, token: ''}
const router = createBrowserRouter([
    {
        path: '/',
        element: <AppLayout><ProtectedRoute user={dummyUser}></ProtectedRoute></AppLayout>,
        children: [
        {
            path: '/',
            element: <p>hello '/home'</p>
        },
        {
            path: '/page1',
            element: <p>hello '/page1'</p>
        },
        {
            path: '/page2',
            element: <p>hello '/page2'</p>
        },]
    },
    {
        path: '/login',
        element: <AppLayout><LoginPage/></AppLayout>
    }
])

root.render(
    <React.StrictMode>
        <RouterProvider router={router} ></RouterProvider>
    </React.StrictMode>
);
jheisondev commented 11 months ago

I also had a lot of doubts, as it is not clear in the documentation. I implemented it as follows

Component:

import React from "react";

import { Navigate, Outlet } from "react-router-dom";

const ProtectedRoutes = () => {
    // TODO: Use authentication token
    const localStorageToken = localStorage.getItem("token");

    return localStorageToken ? <Outlet /> : <Navigate to="/login"  replace />;
};

export default ProtectedRoutes;

Routes:

export const router = createBrowserRouter(
[
    {
        path: "/",
        element: <Home />,
    },
    {
    path: "/login",
    element: <Login />,
    },
    {
    element: <ProtectedRoutes />,
    children: [
          {
              path: "/route1",
              element: <Screen1 />,
          },
          {
              path: "/route2",
              element: <Screen2 />,
          },
          {
              path: "/route3",
              element: <Screen3 />,
          },
    ],
    },
  ], { basename: "/app" },
);

Provider:

<RouterProvider router={router} />

awreese commented 11 months ago

https://stackoverflow.com/a/66289280/8690857

It doesn't matter if you are using a routes configuration array with the useRoutes hook, or if you pass the same object to one of the Data router creation functions, the routing code is the same.

In other words, given:

const PrivateRoutes = () => {
  const location = useLocation();
  const { authLogin } = /* some auth state provider */;

  if (authLogin === undefined) {
    return null; // or loading indicator/spinner/etc
  }

  return authLogin 
    ? <Outlet />
    : <Navigate to="/login" replace state={{ from: location }} />;
}
const routes = [
  {
    path: "/",
    element: <PrivateRoutes />,
    children: [
      ... protected routes ...
    ],
  },
  {
    path: "/login",
    element: <Login />,
  },
  {
    path: "*",
    element: <PageNotFound />
  },
];

You can do either of the following:

const appRoutes = useRoutes(routes);

return <BrowserRouter>{appRoutes}</BrowserRouter>;

or

const router = createBrowserRouter(routes);

return <RouterProvider router={router} />;
matija2209 commented 11 months ago

@awreese My auth state provider is nested under RouterProvider so there is no context for it.

awreese commented 11 months ago

@awreese My auth state provider is nested under RouterProvider so there is no context for it.

@matija2209 It's fine for context providers to be rendered within the RouterProvider component, the same React rules apply regarding Context providers and consumers, e.g. providers need only be rendered higher in the ReactTree than consumers.

t-bello7 commented 10 months ago

The example below shows how to create protected routes using createBroweserRouter and using Loader to pass the authenticated data. https://github.com/remix-run/react-router/tree/main/examples/auth-router-provider

VariabileAleatoria commented 9 months ago

The example below shows how to create protected routes using createBroweserRouter and using Loader to pass the authenticated data. https://github.com/remix-run/react-router/tree/main/examples/auth-router-provider

I do not understand the need for the root loader to check for user authentication if I can get the same infos from the AuthProvider object?

alxvallejo commented 9 months ago

@awreese My auth state provider is nested under RouterProvider so there is no context for it.

@matija2209 It's fine for context providers to be rendered within the RouterProvider component, the same React rules apply regarding Context providers and consumers, e.g. providers need only be rendered higher in the ReactTree than consumers.

But in your example, your parent component PrivateRoutes is also your consumer component. Shouldn't your parent component have the auth provider wrapped around PrivateRoutes?

frin457 commented 9 months ago

I have refined my script a little more since I originally posted on this thread, using the following components:

interface AuthContextProps { isAuthenticated: boolean; updateAuthentication: (value: boolean) => void; authEndpoint : UrlObject; } const AuthContext = createContext<AuthContextProps | undefined>(undefined); export const login = (obj : UrlObject ) => { if(!obj.hostname) throw new Error('Login failed, please provide an authentication url to the application config file.') else window.open(obj.hostname , '_blank') } export const AuthProvider: React.FC<{ children: React.ReactNode, authUrl:string, isDev:boolean }> = ({ children, authUrl, isDev }) => { //Ignore authentication requirement in development mode const [isAuthenticated, setIsAuthenticated] = useState(isDev); const authEndpoint = {hostname:authUrl} const updateAuthentication = (value: boolean) => { setIsAuthenticated(value); }; const contextValue: AuthContextProps = { isAuthenticated, updateAuthentication, authEndpoint, };

return {children}</AuthContext.Provider>; };

export const useAuthContext = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuthContext must be used within an AuthProvider'); } return context; };

// - - - - - ProtectedRoute.tsx - - - - - import React, { useEffect, useState } from "react"; import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useAuthContext } from "./AuthContext"; //import AppLayout //import getCookie

//wraps navigation actions with a check for the application auth cookie. type ProctectRouteProps ={ children?: React.ReactNode isDev: boolean; cookieName: string, landingPath: string, errorPath: string } const ProtectedRoute = ({ children , isDev, cookieName, errorPath, landingPath } : ProctectRouteProps) => { const location = useLocation(); const navigate = useNavigate() const [hasLoaded, setHasLoaded] = useState(false) const { isAuthenticated, updateAuthentication } = useAuthContext();

if(!cookieName) navigate(errorPath, {state:{ error: 'Authentication cookie identifier was not provided. Please review .env variables.' }})

useEffect(() => { // Check app cookie const cookieValue = getCookie() const exists = !!cookieValue; if(isDev){ console.log((Application started in development mode. Bypassing 'ProtectedRoute'.)) updateAuthentication(true) } else updateAuthentication(exists) setHasLoaded(true) }, [location.pathname, cookieName, updateAuthentication, isDev]); // Run the check on every route change if (!isAuthenticated) { //Direct user to home page on first login. if(!hasLoaded) return <Navigate to={landingPath} replace state={{error: 'No authentication cookie was found, please complete the login process.'}}/>; else return <Navigate to={errorPath} replace state={{error: 'No authentication cookie was found, please complete the login process.'}}/>; } return (

{children ? children : }
)

};

export default ProtectedRoute;

// - - - - - ErrorPage.tsx - - - - - import React from "react"; import { useLocation, useNavigate } from "react-router-dom"; // import Button

const isDev = process.env.REACT_APP_NODE_ENV === 'development' const hostname = ${isDev ? process.env.REACT_APP_DEV_API_URL : process.env.REACT_APP_PROD_API_URL}

type ErrorPageProps = { error?: string; } const ErrorPage = (props:ErrorPageProps) => { const navigate = useNavigate(); const location = useLocation();

const navMsg = location.state?.error
const msg = navMsg ? <p className="text-lg">{navMsg}</p> : 
    props.error ? <p className="text-lg">{props.error}</p>:
    <p className="text-lg">The targeted page <b>"{location.pathname}"</b> was not found, please confirm the spelling and try again.</p>
return (
        <section id="Error" className="w-full h-full flex flex-col items-center">
            {msg}
            <span className="flex flex-row justify-center items-center space-x-8 my-10 ">

            <Button id='backButton' onClick={()=>navigate(-1)}>
                Return to Previous Page
            </Button>
            <Button id='homeButton' onClick={()=>navigate('/home')}>
                Return to Home Page
            </Button>
            <Button id='logout' onClick={()=>{
                const endpoint = `${hostname}/logout`
                window.open(endpoint, '_blank')
            }}>
                Reset Authentication
            </Button>
            </span>
        </section>
)

}

export default ErrorPage;

// - - - - - index.tsx - - - - - import React from "react"; import ReactDOM from 'react-dom/client'; //import your application pages //import & & &

const isDev = process.env.REACT_APP_NODE_ENV === 'development' const authUrl = isDev ? process.env.REACT_APP_DEV_AUTH_URL : process.env.REACT_APP_PROD_AUTH_URL const container = document.getElementById('root'); const root = ReactDOM.createRoot(container!); // createRoot(container!) if you use TypeScript

const router = createBrowserRouter([ {
path: '/', element: <ProtectedRoute isDev={isDev} cookieName={process.env.REACT_APP_COOKIE_NAME || ''} landingPath="/home" errorPath="/error"/>, children: [ { path: '/somePage', element: <>page</> }, ] }, { path: '/home', element: }, {// Keep this last path: '*', element: }

])

root.render(

); ```
awreese commented 9 months ago

@awreese My auth state provider is nested under RouterProvider so there is no context for it.

@matija2209 It's fine for context providers to be rendered within the RouterProvider component, the same React rules apply regarding Context providers and consumers, e.g. providers need only be rendered higher in the ReactTree than consumers.

But in your example, your parent component PrivateRoutes is also your consumer component. Shouldn't your parent component have the auth provider wrapped around PrivateRoutes?

@alxvallejo This is a very basic and trivial code example. The assumption here is that the context provider component just exists somewhere higher in the ReactTree... whether it is directly wrapping the PrivateRoutes component, the RouterProvider component, or some other ancestor component is a bit irrelevant, it just needs to exist as an ancestor component to any component consuming it's value, e.g. the normal React Context usage.

Fahad29 commented 6 months ago

Could you kindly confirm whether this private route is functioning correctly? Additionally, could you provide guidance on how to utilize this private route, as it is currently displaying an error? A Route is only ever to be used as the child of element, never rendered directly. Please wrap your in a Routes.


const PrivateRoute = ({ component: Component,  ...rest }) => {
  return (
    <Route
      {...rest}
      render={(props) =>
        isAuthenticated() ? (
          <Component {...props} />
        ) : (
          <Navigate to="/Login" />
        )
      }
    />
  );
};
export const pageRoutes = [
  { path: "/",  element: <PrivateRoute component={Home}  />, title: "Home" },
  { path: "/PurchaseCreate", element: <PurchaseCreate />, title: "Purchase Create" },
  { path: "/PurchaseList", element: <PurchaseList />, title: "Purchase List" },
  // other mappings ...
]
export const loginRoutes = [
  { path: "/Login", element: <Login /> },
  { path: "/Register", element: <Register /> },
  { path: "/ResetPassword", element: <ForgotPassword /> },
]
export const emptyRoutes = [
  { path: "*", element: <Page404 /> },
]

export const pageRouter = createBrowserRouter([

  {
    element: <DashboardLayout />,
    children: pageRoutes,
  },
  {
    element: <LoginLayout />,
    children: loginRoutes,
  },
  {
    element: <EmptyLayout />,
    children: emptyRoutes,
  },
])