Tag: javascript

  • Protected Routes in Next.js

    Protected Routes in Next.js

    If you are building a SaaS website that has awesome features or a simple website with minimal user functionality, you know Authentication and Authorization are crucial (difference between authentication and authorization). Protected Routes in Next.js help us ensure that unauthenticated users are not able to see routes/pages intended for logged in (authenticated) users. There are a few approaches to to implement Protected Routes in Next.js, i.e., enforce authentication for a page/route.

    But, first of all – why do we love Next.js? Next.js is arguably the most popular and go-to React framework. It packs some cool stuff including file-based routing, incremental static regeneration, and internationalization (i18n). With Next.js 13, we have got even more power – layouts and Turbopack!

    You might be wondering – why bother protecting routes? We are building a SaaS product with a Next.js frontend and Nest.js backend. We have implemented authentication in the backend but we also need to ensure that forced browsing* is prevented and User Experience is enriched. Actual authentication logic should reside inside our back-end logic. All the API calls must be appropriately authenticated. In our app, whenever there is an unauthenticated request it returns 401 Unauthorized. An ACL is also in place so whenever user requests a resource they do not have access to, the backend returns 403 Forbidden.

    Now, let’s create a route protection flow in Next.js.:
    If a user requests a protected route (something that requires authentication), we redirect them to the login page.
    We should not prevent access if a route is public (supposed to be viewed regardless of the users’ authentication state) like a login page.

    At the end, the goals are simple: safety and security.

    Jodi Rell

    Using RouteGuard

    The concept of a RouteGuard is simple. It is a wrapper component that checks whether the user has access to the requested page on every route change. To track the access, we use one states: authorized. If authorized is true, then the user may see the page or else user is redirected to the login page. To update the state, we have a function authCheck() which prevents access (sets authorized to false) if the user does not have access and the page is not public (e.g. landing page, login page, sign-up page).

    import { Flex, Spinner } from '@chakra-ui/react';
    import { useRouter } from 'next/router';
    import publicPaths from '../data/publicPaths';
    import { useAppDispatch, useAppSelector } from '../hooks/storeHooks';
    import { setRedirectLink } from '../redux/AuthSlice';
    import {
      JSXElementConstructor,
      ReactElement,
      useEffect,
      useState,
    } from 'react';
    
    const RouteGuard = (props: {
      children: ReactElement<unknown, string | JSXElementConstructor<unknown>>;
    }) => {
      const { children } = props;
    
      const router = useRouter();
      const [authorized, setAuthorized] = useState(false);
      const user = useAppSelector((state) => state.auth);
    
      const dispatch = useAppDispatch();
    
      useEffect(() => {
        const authCheck = () => {
          if (
            !user.isLoggedIn &&
            !publicPaths.includes(router.asPath.split('?')[0])
          ) {
            setAuthorized(false);
            dispatch(setRedirectLink({ goto: router.asPath }));
            void router.push({
              pathname: '/login',
            });
          } else {
            setAuthorized(true);
          }
        };
    
        authCheck();
    
        const preventAccess = () => setAuthorized(false);
    
        router.events.on('routeChangeStart', preventAccess);
        router.events.on('routeChangeComplete', authCheck);
    
        return () => {
          router.events.off('routeChangeStart', preventAccess);
          router.events.off('routeChangeComplete', authCheck);
        };
      }, [dispatch, router, router.events, user]);
    
      return authorized ? (
        children
      ) : (
        <Flex h="100vh" w="100vw" justifyContent="center" alignItems="center">
          <Spinner size="xl" />
        </Flex>
      );
    };
    
    export default RouteGuard;

    Note: we are using Redux to store the user’s data; authentication is out of the scope of this blog post.

    Implementing the Middleware

    In a scenario where the users’ session expires while they are on a protected page, they will not be able to fetch newer resources (or perform any actions for that matter). That’s, once again, really bad UX. We cannot expect a user to refresh, so we need a way to let them know that their session is no longer valid.

    To implement the same, we will use another awesome Next.js feature – Middlewares! In few words, a middleware sits between your server and the frontend. Middleware allows you to run code before a request is completed, then based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.

    After session expiration, whenever the user makes a request, it will result in 401 Unauthorized. We have implemented a middleware which listens to the response for each request that is being made from the frontend; if the request results in 401 Unauthorized, we dispatch the same action, i.e. log out the user and redirect to the login page.

    import {
      MiddlewareAPI,
      isRejectedWithValue,
      Middleware,
    } from '@reduxjs/toolkit';
    import { logout } from '../redux/AuthSlice';
    import { store } from '../redux/store';
    
    interface ActionType {
      type: string;
      payload: { status: number };
      meta: {};
      error: {};
    }
    
    const unauthenticatedInterceptor: Middleware =
      (_api: MiddlewareAPI) =>
      (next: (action: ActionType) => unknown) =>
      (action: ActionType) => {
        if (isRejectedWithValue(action)) {
          if (action.payload.status === 401 || action.payload.status === 403) {
            console.error('MIDDLEWARE: Unauthorized/Unauthenticated [Invalid token]');
            store.dispatch(logout());
          }
        }
    
        return next(action);
      };
    
    export default unauthenticatedInterceptor;

    Suggested Readings

  • Alfred – Slack Bot to Post Birthday and Anniversary Messages

    Alfred – Slack Bot to Post Birthday and Anniversary Messages

    At rtCamp, the family is expanding. We wanted to automate the process of sending birthday and work anniversary wishes to our Slack workspace, and so Alfred was born. Alfred lets you send birthday and work anniversary messages using the Slack API. It uses Google Apps Script, a cloud-based JavaScript platform, and integrates seamlessly to Google Sheets.

    Alfred uses a single Google Sheet as the database for users’ data and wishes that you want to send out. Using time-based Google Triggers, one can run the Google Apps Script at specific intervals to send out the (random) messages automatically using the Slack API. Alfred is an open-source project, and you can find it on GitHub – https://github.com/danish17/alfred-slack-bot.

    Let’s take a look at how you can set up Alfred for your workspace.

    Creating a Google Sheet

    Google Sheet is where your data will live. The spreadsheet will have two sheets, let’s call them Data and Messages. The Data sheet will store all the details about the users, like their birthdates, anniversary dates, and their names. The Messages sheet will contain a list of the wishes and their type, Birthday or Anniversary.

    The Message spreadsheet should have the following columns:

    1. Text – wish text
    2. Type – whether the wish is for a birthday or an anniversary

    In the Text, you can add text placeholders like <names>; Alfred will automatically replace them with the recipients’ names. For example, if the text is – “I wish <names> a very happy birthday”, Alfred will generate the wish – “I wish John Doe, and Lionel Messi a very happy birthday”.

    If there are mutliple wishes for each type, Alfred will randomly choose one.

    You can download a demo sheet (.xslx) using this link – https://github.com/danish17/alfred-slack-bot/blob/master/example-data/Test-Data.xlsx

    Setting up the script

    Now it is time to add Alfred to your Google Sheet. For that, we need to create a script and import Alfred as a library.

    Open your sheet and click on Tools > Script Editor

    It will open Google Apps Script Console.

    Importing Alfred

    Before Alfred can serve us, we need to add him to the script. To do that, click on the Plus Icon (+) in front of Libraries in the left sidebar.

    Paste the following Script ID: 1u4gU_yqTtdvhckO5JymTXz87MDKerxg8jc2bPeO4x6ATRS8O7cEs7eoj

    Click on Look Up and it should detect Alfred. In Version, choose the latest version and hit Add. You may also choose HEAD (Development Mode) but it may cause breaking changes, so it is not recommended.

    Note: There may be permission issues. Kindly drop a mail on [email protected] so that I can share access with you.

    To check if Alfred is correctly working, you can add the following code and hit Run.

    function main() {
      if (Alfred) Logger.log( 'Added' )
    }

    Configuring Alfred

    Now, we need to configure how Alfred works. The first step to configuring Alfred is to add Alfred to your Slack workspace and obtain an Incoming Webhook URL.

    Adding Alfred to Slack & Getting Webhook URL

    To add Alfred, you can click the button below or visit the URL: https://slack.com/oauth/v2/authorize?scope=incoming-webhook,chat:write&client_id=2618518958503.2630472038933

    Add to Slack

    After clicking, you will be redirected to alfred.danishshakeel.me and you will be shown the Incoming Webhook URL. Copy it, and save it.

    Adding Alfred to your Google Script

    Now that we have obtained the Webhook URL, we can move ahead. Open your Google Script and add the boilerplate code:

    function alfredExample() {
      // Instantiate a new config object with the Slack Webhook URL.
      const config = createConfig(YOUR_SLACK_WEBHOOK_URL_HERE)
    
      // Set parameters.
      config.dataSheet = YOUR_DATA_SHEET_NAME_HERE // Set name of the sheet containing data.
      config.messageSheet = YOUR_WISH_SHEET_NAME_HERE // Set name of the sheet containing messages.
      config.dobColumnKey = YOUR_BIRTHDATE_COLUMN_KEY_HERE // Birthdate column key.
      config.annivColumnKey = YOUR_ANNIVERSARY_COLUMN_KEY_HERE // Joining Date/Anniversary column key.
      config.namesColumnKey = YOUR_NAMES_COLUMN_KEY_HERE // Names column key.
      const date = new Date() // Init a date object.
      // date.setDate(date.getDate() - 1) // Example: match events for yesterday.
      config.dateToMatch = date // Set date.
    
      // Configure messages.
      config.birthdayHeader = YOUR_BIRTHDAY_HEADER_HERE
      config.birthdayImage = YOUR_BIRTHDAY_IMAGE_URL_HERE
      config.birthdayTitle = YOUR_BIRTHDAY_TITLE_HERE
      config.anniversaryHeader = YOUR_ANNIVERSARY_HEADER_HERE
      config.anniversaryImage = YOUR_ANNIVERSARY_IMAGE_URL_HERE
      config.anniversaryTitle = YOUR_ANNIVERSARY_TITLE_HERE
    
      // Run Alfred.
      runAlfred(config);
    }
    

    For our example:

    • config.dataSheet = 'Data' (Sheet name which contains users’ data)
    • config.messageSheet = 'Messages' (Sheet name which contains wishes)
    • config.dobColumnKey = 'DOB' (Column name which contain birthdates)
    • config.annivColumnKey = 'Joining' (Column name which contains anniversary dates)
    • config.namesColumnKey = 'rtCamper' (Column name which contains users’ names)

    For the messages, you can choose almost anything including some markdown and emojis.

    Test

    You can use Alfred.testAlfred(config)to test Alfred.

    Note: It will send a message to the Slack channel that you've given Alfred access to.

    Setup Automatic Messaging (Triggers)

    You would want to make Alfred automatically wish people everyday. For this purpose, we will utilize Google’s Time-Based Triggers. In your Script Console, click on the Timer icon (⏰).

    Now, click on ‘Add Trigger’ button. We will run our function everyday at 8AM – 9AM Indian Standard Time.

    Voila! Alfred is all ready to send out wishes in your Slack workspace every day.

    Future of Alfred

    There are a lot of things that we can add to Alfred. Although Alfred was born to send out birthday and anniversary wishes, we can add the ability to send out important notifications.

    In the short-term, I am planning to add:

    1. Ability to create custom layouts
    2. Use separate Google Spreadsheets for Data and Messages
    3. Add codebase for JavaScript and Python

    I want to see Alfred as an all-in-one Slack bot that behaves like the real ‘Alfred Pennyworth’