4

Essentially I created a HOC for two pages in my Next.js app (i.e. profile and dashboard) two prevent users from accessing them if they're not authorized.

Example: pages/profile.js

import withAuth from "../components/AuthCheck/index";

function Profile() {
  return (
    <>
      <h1>Profile</h1>
    </>
  )
}


export default withAuth(Profile);

My Auth component/HOC:

import { useRouter } from 'next/router'
import { useUser } from '../../lib/hooks'
import { PageLoader } from '../Loader/index'

const withAuth = Component => {
  const Auth = (props) => {
    const { isError } = useUser(); //My hook which is calling /api/user see if there is a user
    const router = useRouter()


    if (isError === 'Unauthorized') {
      if (typeof window !== 'undefined' && router.pathname === '/profile' || router.pathname === 'dashboard') router.push('/login')

      return <PageLoader />
    }
    return (
      <Component {...props} />
    );

  };

  if (Component.getInitialProps) {
    Auth.getInitialProps = Component.getInitialProps;
  }

  return Auth;
};

export default withAuth;

Now what is happening is if you happen to enter /profile or /dashboard in the browser URL bar, before the redirect you'll see the page for a second i.e. flash.

Any idea why that is happening?

Antonio Pavicevac-Ortiz
  • 7,239
  • 17
  • 68
  • 141
  • 4
    Because the redirect happens on the client-side - you'll briefly see the page generated on server first before it happens. The only way to prevent it is to redirect on the server-side. – juliomalves Dec 10 '21 at 00:01
  • @juliomalves Interesting. And thanks. So where would you put that logic to do that? – Antonio Pavicevac-Ortiz Dec 10 '21 at 00:07
  • I'd recommend a read through [Authenticating Server-Rendered Pages](https://nextjs.org/docs/authentication#authenticating-server-rendered-pages) and [Next.js Middleware](https://nextjs.org/docs/middleware). – juliomalves Dec 10 '21 at 13:43

2 Answers2

4

Based on what Juliomalves and Adrian mentioned I re-read the Next.js docs based on what they included, Always good to get a refresh.

That being said I tried what Adian posted.

In the _app.js file I did the following:

import dynamic from "next/dynamic";
import { useRouter } from 'next/router'

import { useEffect } from 'react';

import { PageLoader } from '../components/Loader/index'

import { useUser } from '../lib/hooks'

import Login from '../pages/login'

const Layout = dynamic(() => import('../components/Layout'));

function MyApp({ Component, pageProps }) {
  const { user, isLoading } = useUser();
  const router = useRouter();

  useEffect(() => {
    router.replace(router.pathname, undefined, { shallow: true })
  }, [user])

  function AuthLogin() {
    useEffect(() => {
      router.replace('/login', undefined, { shallow: true })
    }, [])
    return <Login />
  }

  return (
          <Layout>
            {isLoading ? <PageLoader /> :
              pageProps.auth && !user ? (
                <AuthLogin />
              ) : (
                <Component {...pageProps} />
              )
            }
          </Layout>

  );
}

export default MyApp

So the isLoading prop from the SWR hook useUser() is a part of the first conditional ternary, While true you get the <Loader/>, when false you get the next ternary to kick of;

If the both the auth and !user props are true, the AuthLogin get rendered!

This is how I did it. I went into the pages I wanted private and used the async function getStaticProps and created the prop auth and set it to true.

/pages/dashboard.js Or whatever you want to be private;

export default function Dashboard() {
  return (
    <>
      <h1>Dashboard</h1>
    </>
  )
}

export async function getStaticProps() {
  return {
    props: {
      auth: true
    },
  }
}

So back in _app.js when the pages are getting rendered the getStaticProps will, as said docs say:

Next.js will pre-render this page at build time using the props returned by getStaticProps.

So when pageProps.auth && !user is reached in _app, that is where auth comes from.

Last two things:

You need this useEffect function in the MyApp component with the user prop from the hook in its dependency. Because that will keep the URL in sync/correct, between the redirects.

In /pages/_app, MyApp add:

 useEffect(() => {
    router.replace(router.pathname, undefined, { shallow: true })
  }, [user]);

In the AuthLogin component add:

 useEffect(() => {
      router.replace('/login', undefined, { shallow: true })
    }, []);

This ensures when component gets rendered, the URL would be right.

I am sure if your page is changing frequently you'll have to look into getServerSideProps but for this solved my use case for static pages!

Thanks Juliomalves and Adrian!

Antonio Pavicevac-Ortiz
  • 7,239
  • 17
  • 68
  • 141
  • Minor issue, I'd refactor and pull your AuthLogin function definition outside of your MyApp definition. Never a good idea to define a component inside of another component. – mccambridge May 01 '23 at 17:41
3

I'd consider making use of getServerSideProps on pages that need to be authed, have a look at getServerSideProps . It'll run server side on a page per request.

Alternatively, it might make sense (depending on your project setup - especially if you have access to auth state in _app.tsx_) to render the auth component in your _app. More specifically, you could add a protected:true prop to the pages that are behind auth wall (using static props). Then in app you can check if a particular page has protected===true and redirect to auth component if the user isn't authed, for example:

            {pageProps.protected && !user ? (
                <LoginComponent />
            ) : (
              <Component {...pageProps} />
            )}
adrian
  • 2,786
  • 2
  • 18
  • 33