9

I want to create basic Next.js HOC for authentication. I searched but I didn't figure it out.

I have an admin page in my Next.js app. I want to fetch from http://localhost:4000/user/me and that URL returns my user. If user data returns, component must be rendered. If data didn't return, I want to redirect to the /admin/login page.

I tried this code but that didn't work. How can I solve this issue? Also can I use useSWR instead of fetch?

const withAuth = (Component, { data }) => {
  if (!data) {
    return {
      redirect: {
        destination: "/admin/login",
      },
    };
  }
  return Component;
};

withAuth.getInitialProps = async () => {
  const response = await fetch("http://localhost:4000/user/me");
  const data = await response.json();
  return { data };
};

export default withAuth;
const AdminHome = () => {
  return ();
};
export default withAuth(AdminHome);
juliomalves
  • 42,130
  • 20
  • 150
  • 146
gorkemgunay
  • 117
  • 1
  • 2
  • 8

3 Answers3

21

Server-side authentication

Based on the answer from Create a HOC (higher order component) for authentication in Next.js, you can create a re-usable higher-order function for the authentication logic.

If the user data isn't present it'll redirect to the login page. Otherwise, the function will continue on to call the wrapped getServerSideProps function, and will return the merged user data with the resulting props from the page.

export function withAuth(gssp) {
    return async (context) => {
        const response = await fetch('http://localhost:4000/user/me');
        const data = await response.json();
        
        if (!data) {
            return {
                redirect: {
                    destination: '/admin/login'
                }
            };
        }

        const gsspData = await gssp(context); // Run `getServerSideProps` to get page-specific data
        
        // Pass page-specific props along with user data from `withAuth` to component
        return {
            props: {
                ...gsspData.props,
                data
            }
        };
    }
}

You can then use it on the AdminHome page to wrap the getServerSideProps function.

const AdminHome = ({ data }) => {
    return ();
};

export const getServerSideProps = withAuth(context => {
    // Your normal `getServerSideProps` code here
    return { props: {} };
});

export default AdminHome;

Client-side authentication

If you'd rather have the authentication done on the client, you can create a higher-order component that wraps the component you want to protect.

const withAuth = (Component) => {
    const AuthenticatedComponent = () => {
        const router = useRouter();
        const [data, setData] = useState()

        useEffect(() => {
            const getUser = async () => {
                const response = await fetch('http://localhost:4000/user/me');
                const userData = await response.json();
                if (!userData) {
                    router.push('/admin/login');
                } else {
                    setData(userData);
                }  
            };
            getUser();
        }, []);

        return !!data ? <Component data={data} /> : null; // Render whatever you want while the authentication occurs
    };

    return AuthenticatedComponent;
};

You can then use it to wrap the AdminHome component directly.

const AdminHome = () => {
  return ();
};

export default withAuth(AdminHome);
juliomalves
  • 42,130
  • 20
  • 150
  • 146
  • 2
    Firstly, thank you so much. Thats really nice. I'm using client side rendering in admin component. How can i wrap this code using withAuth() For example: `const posts = useSWR("http://localhost:4000/post", (...args) => fetch(...args).then((res) => res.json()) );` Basically i want to protect whole admin component – gorkemgunay Jan 11 '22 at 08:51
  • In my example, `withAuth` is meant to run on the server as it's a wrapper for `getServerSideProps` - you can't use `useSWR` on server-side code. It's a React hook and can only be used on the client-side. – juliomalves Jan 11 '22 at 08:53
  • Yeah I know useSWR only be used for client-side. Therefore I need to protect whole admin component. If user try to access admin component i need to send him to admin/login page – gorkemgunay Jan 11 '22 at 08:56
  • Yes i need to client-side protection. User can't reach the whole page if not logged in. I need to redirect admin/login page – gorkemgunay Jan 11 '22 at 08:59
  • 1
    If you don't want the user to reach the page, wouldn't my approach done on the server-side be more suitable then? Or is there a reason you need the auth to happen on the client? If you do it client-side, the page will load and _then_ the user will be redirected. – juliomalves Jan 11 '22 at 09:01
  • I'm trying to do personal-blog page. My blog page side is already server-side rendering for seo purposes. But i need to do client-side rendering for admin side. Because i don't need seo here. – gorkemgunay Jan 11 '22 at 09:03
  • You can have the auth logic done on the server, and still do client-side rendering on the admin page, they're not mutually exclusive. Still, I can add a solution for client-side auth to my answer. – juliomalves Jan 11 '22 at 09:04
  • I tried your code and if user not logged in data is not coming but i can still reach the page. I need to wrap whole component in this way if user not logged in automatically redirect to admin/login page – gorkemgunay Jan 11 '22 at 09:09
  • That's not the expected behaviour. If user is not logged in, the app will redirect to the `/admin/login` page. – juliomalves Jan 11 '22 at 09:12
  • `export const getServerSideProps = withAuth(() => { async () => { const postsResponse = await fetch("http://localhost:4000/post"); const postsData = await postsResponse.json(); return { props: { posts: postsData.data, }, }; }; });` Here is the code still component is loading but only posts section is empty. – gorkemgunay Jan 11 '22 at 09:21
  • I tried your client-side code and still it didn't work. I think I didn't figured out. Again thank you so much for helping. I will try to do it again. – gorkemgunay Jan 11 '22 at 10:04
  • 1
    Wow, it's work. Thank you so much this really help to me – gorkemgunay Jan 11 '22 at 10:08
  • Making an additional request every time you need to check auth state, is not performance-wise. It should only check the client state, then let the other requests validate the token, which needs to be made anyway. – GarryOne Oct 21 '22 at 13:48
  • @juliomalves Hi, is it necessary to use both client and server side approach together? Which one is the better approach – ßiansor Å. Ålmerol Jul 05 '23 at 18:55
  • 1
    @ßiansorÅ.Ålmerol No, it's either one or the other. It depends on your requirements. – juliomalves Jul 06 '23 at 07:18
3

If you're looking for the typescript version:

withAuth.ts

export function withAuth(gssp: GetServerSideProps): GetServerSideProps {
  return async (context) => {
    const { user } = (await getSession(context.req, context.res)) || {};

    if (!user) {
      return {
        redirect: { statusCode: 302, destination: "/" },
      };
    }

    const gsspData = await gssp(context);

    if (!("props" in gsspData)) {
      throw new Error("invalid getSSP result");
    }

    return {
      props: {
        ...gsspData.props,
        user,
      },
    };
  };
}

Home.tsx

export const getServerSideProps = withAuth(async (context) => {
  return { props: {} };
});
0

I've created a HOC for protected route:

import { RootState } from "@/redux/features/store";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useSelector } from "react-redux";

const withAuth = <P extends object>(
  WrappedComponent: React.ComponentType<P>
) => {
  const ComponentWithAuth = (props: P) => {
    const router = useRouter();
    const isAuthenticated = useSelector(
      (state: RootState) => state.login.isAuthenticated
    );

    useEffect(() => {
      // Perform authentication check here

      if (!isAuthenticated) {`enter code here`
        router.push("/login");
      }
    }, []);

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

  return ComponentWithAuth;
};

export default withAuth;
and I WRAPPED in app.tsx

import * as React from "react";
import type { AppProps } from "next/app";
import { ThemeProvider, CssBaseline, createTheme } from "@mui/material";
import { Provider } from "react-redux";
import { store } from "../redux/features/store";
import defaultTheme from "../assets/styles/theme/lightThemeOptions";
import "../assets/styles/globalStyles.scss";
import "../assets/styles/iconStyles.scss";
import { Notification } from "../Toastmessage/Snackbar";
import { PersistGate } from "redux-persist/integration/react";
import { persistStore } from "redux-persist";
import Head from "next/head";
import ProtectedRoute from "@/components/ProtectedRoute/protectedroute";
import withAuth from "./ProtectedRoute";

const lightTheme = createTheme(defaultTheme);

const MyApp = ({ Component, pageProps }: AppProps) => {
  let persistor = persistStore(store);

  return (
    <>
      <Head>
        <title>Upshot.ai</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/fav-icon.png" />

        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </Head>
      <Provider store={store}>
        <PersistGate persistor={persistor} loading={null}>
          {() => (
            <ThemeProvider theme={lightTheme}>
            <CssBaseline/>
            {/* <ProtectedRoute> */}
              <Component {...pageProps} />
              {/* </ProtectedRoute> */}
              <Notification />
            </ThemeProvider>
           )}
        </PersistGate> 
      </Provider>
    </>
  );
};

export default withAuth(MyApp);

But my UI is displaying white screen.

devpolo
  • 2,487
  • 3
  • 12
  • 28