skip to Main Content

I am quite new to React, I got a starter kit which has next.js for authentication in React.
I want to restrict access to admin routes when a user accesses them.

I have two roles, admin and user, which gets stored in session after successful login.

The _app.jsx inside pages folder looks like this.

    /**
 * Custom Next.js App
 *
 * @see https://nextjs.org/docs#custom-app
 */

import React from 'react'
import Head from 'next/head'
import { NextAuth } from 'next-auth/client'
import withRedux from 'next-redux-wrapper'
import NextSeo from 'next-seo'
import App, { Container as NextContainer } from 'next/app'
import { Provider as ReduxProvider } from 'react-redux'
import { Favicon, GTMScript, WebFonts } from '../components/head'
import { AuthUserProvider } from '../contexts/AuthUserContext'
import makeSEO from '../lib/seo'
import makeStore from '../lib/store'
// Global CSS from SCSS (compiles to style.css in _document)
import '../styles/globals.scss'

class CustomApp extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {}

    ctx.session = await NextAuth.init({ req: ctx.req })

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx)
    }

    return {
      pageProps,
      seo: makeSEO(ctx.req),
      session: ctx.session,
    }
  }

  componentDidMount() {
    // picturefill for <picture> and srcset support in older browsers
    // eslint-disable-next-line global-require
    if (process.browser) require('picturefill')
  }

  render() {
    const { Component, pageProps, seo, session, store } = this.props

    return (
      <NextContainer>
        {
          // to keep <Head/> items de-duped we should use next/head in _app.jsx
          // @see https://github.com/zeit/next.js/issues/6919
        }
        <Head>
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1, minimal-ui"
          />
          <WebFonts />
          <Favicon />
          <GTMScript />
        </Head>
        <ReduxProvider store={store}>
          <AuthUserProvider session={session}>
            <NextSeo config={seo} />
            <Component {...pageProps} />
          </AuthUserProvider>
        </ReduxProvider>
      </NextContainer>
    )
  }
}

export default withRedux(makeStore)(CustomApp)

Can anyone guide me on what to do in order to make the routes accessible as per the role?

2

Answers


  1. I was able to get roles working with next-auth and useSession. Hopefully, this helps you as well.

    models/index.js

    import User, { UserSchema } from "./User"
    
    export default {
      User: {
        model: User,
        schema: UserSchema
      }
    }
    

    models/User.js

    import Adapters from "next-auth/adapters"
    
    // Extend the built-in models using class inheritance
    export default class User extends Adapters.TypeORM.Models.User.model {
      constructor(name, email, image, emailVerified, roles) {
        super(name, email, image, emailVerified)
        if (roles) { this.roles = roles}
      }
    }
    
    export const UserSchema = {
      name: "User",
      target: User,
      columns: {
        ...Adapters.TypeORM.Models.User.schema.columns,
        roles: {
          type: "varchar",
          nullable: true
        },
      },
    }
    

    pages/api/auth/[…nextauth].js

    import NextAuth from 'next-auth'
    import Providers from 'next-auth/providers'
    import Adapters from 'next-auth/adapters'
    
    import Models from '../../../models'
    
    export default NextAuth({
      // @link https://next-auth.js.org/configuration/providers
      providers: [
        Providers.Google({
          clientId: process.env.GOOGLE_ID,
          clientSecret: process.env.GOOGLE_SECRET
        })
      ],
      // @link https://next-auth.js.org/tutorials/typeorm-custom-models
      adapter: Adapters.TypeORM.Adapter(
        process.env.DATABASE_URL,
        {
          models: {
            User: Models.User
          }
        }
      ),
      session: { jwt: true },
      callbacks: {
        async jwt(token, user, account, profile, isNewUser) {
          if (account?.accessToken) {
            token.accessToken = account.accessToken
          }
          if (user?.roles) {
            token.roles = user.roles
          }
          return token
        },
        async session(session, token) {
          if(token?.accessToken) {
            session.accessToken = token.accessToken
          }
          if (token?.roles) {
            session.user.roles = token.roles
          }
          return session
        }
      }
    });
    

    Usage – in JSX

    {!session &&  sessionLoading && <p>Loading...</p>}
    {!session && !sessionLoading && <p>Access Denied - Not logged in</p> }
    { session && !sessionLoading && !session.user?.roles?.includes("Verified") && <p>Access Denied - Unverified</p> }
    { session && !sessionLoading && session.user?.roles?.includes("Verified") && <>
      <p>Stuff for verified users</p>
      {session.user.roles && session.user.roles.includes("Admin") && <>
        <button id="doSomething">Admin Only</button>
      </>}
      <p>More stuff for verified users</p>
    </>}
    

    Github Thread

    Login or Signup to reply.
  2. Here’s how I have done it.

    File: _app.js

    ...
    const App = ({ Component, pageProps }) => {
      const [user, setUser] = useState(null);
      const [isAuth, setAuth] = useState(false);
    
      const getUser = async () => {
        try {
          const cognitoUser = await Auth.currentAuthenticatedUser();
    
          const user = //replace with your backend api that fetches user details with role (admin, user, etc.)
    
          if (user && user.data && user.success) {
            setUser(user.data);
          }
        } catch (error) {
          console.log(error);
        }
      }
    
      useEffect(() => {
        Hub.listen('auth', ({ payload: { event, data } }) => {
          switch (event) {
            case 'signIn':
              setAuth(true);
              break;
            case 'signOut':
              setAuth(false);
              break;
            case 'signIn_failure':
              setAuth(false);
              break;
          }
        });
        getUser();
      }, [isAuth]);
    
      Auth.currentSession()
          .then(async (currentSession) => {
            const idTokenExpireTime = currentSession.getIdToken().getExpiration();
            const currentTime = Math.round(+new Date() / 1000);
    
            if (idTokenExpireTime < currentTime) {
              Auth.currentAuthenticatedUser()
                  .then((res) => {
                    const { refreshToken } = res.getSignInUserSession();
    
                    res.refreshSession(refreshToken, (err, session) => {
                      if (err) {
                        //error logic
                      } else {
                        Account.token.set(session.idToken.jwtToken);
                        Account.refreshToken.set(session.refreshToken.token);
                      }
                    });
                  });
            }
    
            store.dispatch(setIsAuth(true));
            setAuth(true);
          })
          .catch((err) => {
            store.dispatch(setIsAuth(false));
            setAuth(false);
          });
    
      if (pageProps.protected && user == null)
          return (
              <div id={'globalLoader'}>
                <div className="loader">
                </div>
              </div>
          );
    
      if (pageProps.protected && user && pageProps.userTypes && pageProps.userTypes.indexOf(user.role) === -1)
          return <Page403 />;
    
      return (<Provider store={store}><Component {...pageProps} /></Provider>);
    
    };
    
    export default App;
    

    The logic is to fetch user with role info and check it and render accordingly. If page props has related keys (protected: true, userTypes: ["Admin"]), it is going to be protected from unauthorized access.

    Page.tsx

    ...
    import { GetServerSideProps } from "next";
    import { withSSRContext } from "aws-amplify";
    ...
    
    const Page = () => {
      ...
    }
    
    export async function authenticatedUsers(context) {
        const { Auth } = withSSRContext(context);
        try {
            await Auth.currentAuthenticatedUser();
        } catch (error) {
            console.log(error)
            return true
        }
        return false
    }
    
    export const getServerSideProps: GetServerSideProps = async (ctx) => {
        let shouldRedirect = await authenticatedUsers(ctx);
    
        if (shouldRedirect) {
            return {
                redirect: {
                    destination: '/sign-in',
                    permanent: false
                }
            }
        }
        return {
            props: {
                protected: true,
                userTypes: ["Admin"]
            }
        }
    }
    
    export default Page;
    

    This page is protected. If you do not use them like props: {}, it will not be protected and normal users will access the page.

    Css file

    #globalLoader{
        position: fixed;
        z-index: 1700;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background-color: #fff;
        display: flex;
        /*left: 0,*/
        right: 0;
        width: 100%;
        height: 100%;
        justify-content: center;
        align-items: center;
    }
    /* From uiverse.io by @satyamchaudharydev */
    .loader {
        --clr: #3498db;
        /* color of spining  */
        width: 50px;
        height: 50px;
        position: relative;
    }
    .loader:before, .loader:after {
        content: "";
        position: absolute;
        top: -10px;
        left: -10px;
        width: 100%;
        height: 100%;
        border-radius: 100%;
        border: 10px solid transparent;
        border-top-color: var(--clr);
    }
    .loader:before {
        z-index: 100;
        animation: spin 1s infinite;
    }
    .loader:after {
        border: 10px solid #ccc;
    }
    @keyframes spin {
        0% {
            -webkit-transform: rotate(0deg);
            -ms-transform: rotate(0deg);
            -o-transform: rotate(0deg);
            transform: rotate(0deg);
        }
        100% {
            -webkit-transform: rotate(360deg);
            -ms-transform: rotate(360deg);
            -o-transform: rotate(360deg);
            transform: rotate(360deg);
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search