Skip to content

Instantly share code, notes, and snippets.

@whoisryosuke
Created June 26, 2018 22:24
Show Gist options
  • Save whoisryosuke/d034d3eaa0556e86349fb2634788a7a1 to your computer and use it in GitHub Desktop.
Save whoisryosuke/d034d3eaa0556e86349fb2634788a7a1 to your computer and use it in GitHub Desktop.
ReactJS - NextJS - A HOC for wrapping NextJS pages in an authentication check. Checks for getInitialProps on the child component and runs it, so you still get SSR from the page. Also includes a user agent for Material UI.
import React, {Component} from 'react'
import Router from 'next/router'
import AuthService from './AuthService'
export default function withAuth(AuthComponent) {
const Auth = new AuthService('http://localhost')
return class Authenticated extends Component {
static async getInitialProps(ctx) {
// Ensures material-ui renders the correct css prefixes server-side
let userAgent
if (process.browser) {
userAgent = navigator.userAgent
} else {
userAgent = ctx.req.headers['user-agent']
}
// Check if Page has a `getInitialProps`; if so, call it.
const pageProps = AuthComponent.getInitialProps && await AuthComponent.getInitialProps(ctx);
// Return props.
return { ...pageProps, userAgent }
}
constructor(props) {
super(props)
this.state = {
isLoading: true
};
}
componentDidMount () {
if (!Auth.loggedIn()) {
Router.push('/')
}
this.setState({ isLoading: false })
}
render() {
return (
<div>
{this.state.isLoading ? (
<div>LOADING....</div>
) : (
<AuthComponent {...this.props} auth={Auth} />
)}
</div>
)
}
}
}
@othneildrew
Copy link

Ive been having trouble getting a HOC to work like this for pages that use getStaticProps. Any suggestions ?

Thanks for this! Do you have an example that uses the new getServerSideProps() implementation?

Haven't tested this yet but would it be just like dealing with getInitialProps ?

static async getServerSideProps(ctx) {
  // Check if Page has a `getServerSideProps`; if so, call it.
  const pageProps = AuthComponent.getServerSideProps && await AuthComponent.getServerSideProps(ctx);
  // Return props.
  return { ...pageProps }
}

Unfortunately, this doesn't work because new versions of next now require that getServerSideProps and getStaticProps be exported like so:

export const getStaticProps = () => ({
  props: {
    hello: 'world',
  },
})

The solution proposed would result in an error similar to this -> Error: page /profile getServerSideProps can not be attached to a page's component and must be exported from the page. See more info here: https://err.sh/next.js/gssp-component-member. I'm also in need of a solution and am working on one. Will let you'll know if I come up with something.

@ImranAhmed
Copy link

@othneildrew Did you ever resolve this? I have hit a similar problem here. auth0/nextjs-auth0#148

@nhanpdnguyen
Copy link

Hi, I've just realized, why don't we use this withAuth as a HOC on the component that the page renders, not on the page itself. This way we could use getStaticProps and getServerSideProps as normal.
The withAuth HOC will then only be responsible for authentication check and redirect stuff, no need for getIntitalProps inside since we could declare that on the page.
For example, a page at pages/products.js

import React from 'react'
import ProductList from '@components/ProductList'
import withAuth from '@utils/withAuth'

function ProductsPage(props) {
  const AuthenticatedProductList = withAuth(ProductList);
  return <AuthenticatedProductList {...props} />
}

export async function getStaticProps(context) {
 // getStaticProps stuff
}

export async function getServerSideProps(context) {
 // getServerSideProps stuff
}

ProductsPage.getInitialProps = async (ctx) => {
  // getInitialProps stuff
}

export default ProductsPage;

@moelashmawy
Copy link

Hi, I've just realized, why don't we use this withAuth as a HOC on the component that the page renders, not on the page itself. This way we could use getStaticProps and getServerSideProps as normal.
The withAuth HOC will then only be responsible for authentication check and redirect stuff, no need for getIntitalProps inside since we could declare that on the page.
For example, a page at pages/products.js

import React from 'react'
import ProductList from '@components/ProductList'
import withAuth from '@utils/withAuth'

function ProductsPage(props) {
  const AuthenticatedProductList = withAuth(ProductList);
  return <AuthenticatedProductList {...props} />
}

export async function getStaticProps(context) {
 // getStaticProps stuff
}

export async function getServerSideProps(context) {
 // getServerSideProps stuff
}

ProductsPage.getInitialProps = async (ctx) => {
  // getInitialProps stuff
}

export default ProductsPage;

can you provide a full implementation please?

@Sergioamjr
Copy link

Sergioamjr commented Apr 1, 2021

I couldn't use the getServerSideProps method in a HOC, because it is only available on pages. Neither use getInitialProps inside a getServerSideProps, I got a warning in my console.

In my case. I needed a generic function to validate the user's access to a private page with an async call. I solved this by creating a function that receives the context and a callback.

//util.js
const validateRoute = (context) => async (callback) => {
  const { id, pathname } = context.query;
  const paths = await getAllowedPaths(id);
  if (!paths.includes(pathname)) {
    return {
      redirect: "/home",
      permanente: false,
    };
  }
  return callback();
};

In all my private pages.

export const getServerSideProps = (context) => {
  return validateRoute(context)(async () => {
    // my custom callback for each page.
    const { id } = context.query;
    const props = await getUserData(id);
    return {
      props,
    };
  });
};

@mtoranzoskw
Copy link

mtoranzoskw commented Apr 19, 2021

I couldn't use the getServerSideProps method in a HOC, because it is only available on pages. Neither use getInitialProps inside a getServerSideProps, I got a warning in my console.

In my case. I needed a generic function to validate the user's access to a private page with an async call. I solved this by creating a function that receives the context and a callback.

//util.js
const validateRoute = (context) => async (callback) => {
  const { id, pathname } = context.query;
  const paths = await getAllowedPaths(id);
  if (!paths.includes(pathname)) {
    return {
      redirect: "/home",
      permanente: false,
    };
  }
  return callback();
};

In all my private pages.

export const getServerSideProps = (context) => {
  return validateRoute(context)(async () => {
    // my custom callback for each page.
    const { id } = context.query;
    const props = await getUserData(id);
    return {
      props,
    };
  });
};

would you help port this to typescript?

I was able to cast it like this:

import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'
import Cookies from 'cookies'

type EmptyProps = {
  props: Record<string, unknown>
}

// HOC to only be executed when user is not logged in, otherwise redirect to portal
const withoutAuth = (context: GetServerSidePropsContext) => async (
  callback: () => any
): Promise<GetServerSidePropsResult<EmptyProps>> => {
  const cookies = new Cookies(context.req, context.res)

  if (cookies.get('user')) {
    return {
      redirect: {
        destination: '/protected-source',
        permanent: false,
      },
    }
  }

  return callback()
}

export default withoutAuth

I still can't figure the typescript definition for the callback... I've ended up doing callback: () => any

@Sergioamjr
Copy link

I couldn't use the getServerSideProps method in a HOC, because it is only available on pages. Neither use getInitialProps inside a getServerSideProps, I got a warning in my console.
In my case. I needed a generic function to validate the user's access to a private page with an async call. I solved this by creating a function that receives the context and a callback.

//util.js
const validateRoute = (context) => async (callback) => {
  const { id, pathname } = context.query;
  const paths = await getAllowedPaths(id);
  if (!paths.includes(pathname)) {
    return {
      redirect: "/home",
      permanente: false,
    };
  }
  return callback();
};

In all my private pages.

export const getServerSideProps = (context) => {
  return validateRoute(context)(async () => {
    // my custom callback for each page.
    const { id } = context.query;
    const props = await getUserData(id);
    return {
      props,
    };
  });
};

would you help port this to typescript?

I was able to cast it like this:

import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'
import Cookies from 'cookies'

type EmptyProps = {
  props: Record<string, unknown>
}

// HOC to only be executed when user is not logged in, otherwise redirect to portal
const withoutAuth = (context: GetServerSidePropsContext) => async (
  callback: () => any
): Promise<GetServerSidePropsResult<EmptyProps>> => {
  const cookies = new Cookies(context.req, context.res)

  if (cookies.get('user')) {
    return {
      redirect: {
        destination: '/protected-source',
        permanent: false,
      },
    }
  }

  return callback()
}

export default withoutAuth

I still can figure the typescript definition for the callback... I've ended up doing callback: () => any

@mtoranzoskw callback has no parameter and might return the same values of withoutAuth. Try this out, let me know if it works.

const withoutAuth = (context: GetServerSidePropsContext) => async (
  callback: () => GetServerSidePropsResult<EmptyProps>
): Promise<GetServerSidePropsResult<EmptyProps>> => {

@mtoranzoskw
Copy link

I couldn't use the getServerSideProps method in a HOC, because it is only available on pages. Neither use getInitialProps inside a getServerSideProps, I got a warning in my console.
In my case. I needed a generic function to validate the user's access to a private page with an async call. I solved this by creating a function that receives the context and a callback.

//util.js
const validateRoute = (context) => async (callback) => {
  const { id, pathname } = context.query;
  const paths = await getAllowedPaths(id);
  if (!paths.includes(pathname)) {
    return {
      redirect: "/home",
      permanente: false,
    };
  }
  return callback();
};

In all my private pages.

export const getServerSideProps = (context) => {
  return validateRoute(context)(async () => {
    // my custom callback for each page.
    const { id } = context.query;
    const props = await getUserData(id);
    return {
      props,
    };
  });
};

would you help port this to typescript?
I was able to cast it like this:

import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'
import Cookies from 'cookies'

type EmptyProps = {
  props: Record<string, unknown>
}

// HOC to only be executed when user is not logged in, otherwise redirect to portal
const withoutAuth = (context: GetServerSidePropsContext) => async (
  callback: () => any
): Promise<GetServerSidePropsResult<EmptyProps>> => {
  const cookies = new Cookies(context.req, context.res)

  if (cookies.get('user')) {
    return {
      redirect: {
        destination: '/protected-source',
        permanent: false,
      },
    }
  }

  return callback()
}

export default withoutAuth

I still can figure the typescript definition for the callback... I've ended up doing callback: () => any

@mtoranzoskw callback has no parameter and might return the same values of withoutAuth. Try this out, let me know if it works.

const withoutAuth = (context: GetServerSidePropsContext) => async (
  callback: () => GetServerSidePropsResult<EmptyProps>
): Promise<GetServerSidePropsResult<EmptyProps>> => {

Not working... I've tried that but I have an error when implementing it
image

Got an error using Promise as well (callback: () => Promise<GetServerSidePropsResult<EmptyProps>>)
image

@Sergioamjr
Copy link

I couldn't use the getServerSideProps method in a HOC, because it is only available on pages. Neither use getInitialProps inside a getServerSideProps, I got a warning in my console.
In my case. I needed a generic function to validate the user's access to a private page with an async call. I solved this by creating a function that receives the context and a callback.

//util.js
const validateRoute = (context) => async (callback) => {
  const { id, pathname } = context.query;
  const paths = await getAllowedPaths(id);
  if (!paths.includes(pathname)) {
    return {
      redirect: "/home",
      permanente: false,
    };
  }
  return callback();
};

In all my private pages.

export const getServerSideProps = (context) => {
  return validateRoute(context)(async () => {
    // my custom callback for each page.
    const { id } = context.query;
    const props = await getUserData(id);
    return {
      props,
    };
  });
};

would you help port this to typescript?
I was able to cast it like this:

import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'
import Cookies from 'cookies'

type EmptyProps = {
  props: Record<string, unknown>
}

// HOC to only be executed when user is not logged in, otherwise redirect to portal
const withoutAuth = (context: GetServerSidePropsContext) => async (
  callback: () => any
): Promise<GetServerSidePropsResult<EmptyProps>> => {
  const cookies = new Cookies(context.req, context.res)

  if (cookies.get('user')) {
    return {
      redirect: {
        destination: '/protected-source',
        permanent: false,
      },
    }
  }

  return callback()
}

export default withoutAuth

I still can figure the typescript definition for the callback... I've ended up doing callback: () => any

@mtoranzoskw callback has no parameter and might return the same values of withoutAuth. Try this out, let me know if it works.

const withoutAuth = (context: GetServerSidePropsContext) => async (
  callback: () => GetServerSidePropsResult<EmptyProps>
): Promise<GetServerSidePropsResult<EmptyProps>> => {

Not working... I've tried that but I have an error when implementing it
image

Got an error using Promise as well (callback: () => Promise<GetServerSidePropsResult<EmptyProps>>)
image

@mtoranzoskw try to make your props optional.

type EmptyProps = {
  props?: Record<string, unknown>
}

// or

type EmptyProps = {
  props: Partial<Record<string, unknown>>
}

@mtoranzoskw
Copy link

mtoranzoskw commented Apr 20, 2021

Thanks for the help @Sergioamjr

I've tried both, but making props optional was the only one that worked! I guess Partial would go 1 level up so that props become optional.

type EmptyProps = {
  props?: Record<string, unknown>
}

💪

@Mood-al
Copy link

Mood-al commented Aug 4, 2021

anyone done this without getInialProps ? currently im using next 11 and i want to add auth to my app but so far i couldnt understand the logic around protected routes in next js

@thuyit1996
Copy link

Thank a lot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment