Insta-Next: Authentication with NextAuth

Insta-Next: Authentication with NextAuth

In this part, let's try to implement authentication for our InstaNext app using NextAuth.js

Featured on Hashnode

In this part, we will be building the authentication part of the web application using NextAuth.js.

Particularly, we will look into the process of login and registration, as well as retrieving the active user information from the backend. Finally, we will also secure the API routes with Middleware.

If you've missed the previous parts, you can continue from where we left off the last part.

Sneak Peek:

NextAuth

Next Auth

Looks like they

Unsurprisingly, NextAuth is the authentication for Next.js.

Unlike the traditional SPAs which needs to maintain session tokens manually on the frontend, Next.js abstracts all these processes, so all we need to do is defining the configurations and it'll be ready to go!

Some other benefits of NextAuth:

  1. To login and logout, we simply call a function signIn or signOut on the client (frontend).

  2. The client can easily get the user's information from the token using getSession.

  3. The server can retrieve the active user's information using getServerSession.

  4. Can integrate a range of third-party providers like Google easily (no need the usual extra libraries)

  5. Integrate well with libraries like Prisma

I hope that's enough to convince you, regardless, this tutorial is going to use NextAuth.

Setting Up

Most of these are taken directly from NextAuth's documentation

Dependency

As usual, let's just install the node package

yarn add next-auth

Database Preparation

However, before we can proceed, we must make some changes to our database schema to fulfill NextAuth's requirements. By default, NextAuth requires the IDs to be in String instead of a number that we're currently implementing. Besides, they also need another Model called Session to store session info.

Since we're changing, we might as well use String for the Post and Story to keep them consistent

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id          String         @id @default(cuid())
  username    String         @unique
  email       String         @unique
  password    String
  description String         @default("")
  created_at  DateTime       @default(now())
  followers   UserFollower[] @relation("followers")
  followings  UserFollower[] @relation("followings")
  posts_liked PostLike[]
  posts       Post[]
  stories     Story[]
  Session     Session[]
}

model Post {
  id         String     @id @default(cuid())
  caption    String
  user_id    String
  user       User       @relation(fields: [user_id], references: [id])
  liked_bys  PostLike[]
  created_at DateTime   @default(now())
}

model Story {
  id         String   @id @default(cuid())
  caption    String
  user_id    String
  user       User     @relation(fields: [user_id], references: [id])
  created_at DateTime @default(now())
}

model Image {
  id            Int    @id @default(autoincrement())
  type          String
  url           String
  associated_id String
  sequence      Int
}

model UserFollower {
  user_id     String
  user        User     @relation("followers", fields: [user_id], references: [id])
  follower_id String
  follower    User     @relation("followings", fields: [follower_id], references: [id])
  created_at  DateTime @default(now())

  @@id([user_id, follower_id])
}

model PostLike {
  user_id    String
  user       User     @relation(fields: [user_id], references: [id])
  post_id    String
  post       Post     @relation(fields: [post_id], references: [id])
  created_at DateTime @default(now())

  @@id([user_id, post_id])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  user_id      String
  expires      DateTime
  user         User     @relation(fields: [user_id], references: [id], onDelete: Cascade)
}

Then, let's run the migration again

yarn migrate

PS: There'll probably be a warning about resetting the database, just enter yes, but we will seed the database later, not now

You might also want to update the types for these files

  1. attach-image.ts

  2. findSinglePost.ts

  3. [post_id]/index.ts

  4. [post_id]/likeds.ts

  5. findPostLikedUsers

  6. modals/types.ts

  7. api/posts.ts

  8. posts/Liked/PostLiked.tsx

Nothing of significant concern here, just some type changes.

NextAuth Configuration

To use NextAuth properly, we will need to set another environment variable, NEXTAUTH_SECRET as a secret key for generating JWT tokens, you can use this UUID generator to create a unique secret for yourself, but here's mine

# .env
DATABASE_URL="postgresql://postgres:secret@localhost:5432/instanext"
NEXTAUTH_SECRET=4411c946-fa73-481a-8c54-f0df76105a57

All auth APIs will be handled by src/pages/api/auth/[...nextauth].ts, which is also where all the NextAuth configurations lie. Let's just copy the example given from the documentation

// src/pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    // ...add more providers here
  ],
};

export default NextAuth(authOptions);

Prisma Adapter

Next, we must make sure NextAuth can read the user's info from our database. To achieve this, we will rely on NextAuth's Prisma Adapter

yarn add @next-auth/prisma-adapter

And we can proceed to add Prisma Adapter to our configuration, I also deleted the GitHubProvider for now

// src/pages/api/auth/[...nextauth].ts
import prisma from "@/utils/prisma";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
...
export const authOptions: AuthOptions = {
  adapter: PrismaAdapter(prisma),
}

Certainly, we aren't planning to use GitHub as our provider now. Instead, we will look at Credential Authentication. Copying over the codes, and updating to use Prisma in our implementation,

export const authOptions: AuthOptions = {
  adapter: PrismaAdapter(prisma),
  // JWT for credentials
  session: {
    strategy: "jwt",
  },
  // Configure one or more authentication providers
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials, req) {
        const user = await prisma.user.findFirstOrThrow({
          where: {
            email: credentials?.email,
          },
        });
        const userWithImage = await attachImage(user, "user");

        if (user) {
          return {
            id: user.id,
            email: user.email,
            name: user.username,
            image: userWithImage.profile_pic?.url,
          };
        } else {
          return null;
        }
      },
    }),
  ],
};

Password Encryption

Almost there, we still need one last thing, password hashing. Storing passwords in plaintext is the last thing you'd want to do.

We will use the famous bcrypt for this

yarn add bcrypt
yarn add -D @types/bcrypt # to get the type declaration

Let's add some password features

// src/features/password/hashPassword.ts
import { hash } from "bcrypt";

const hashPassword = async (password: string) => {
  return await hash(password, 12);
};

export default hashPassword;

// src/features/password/comparePassword .ts
import { compare } from "bcrypt";

const comparePassword = async (
  plainTextPassword: string,
  hashedPassword: string
) => {
  return await compare(plainTextPassword, hashedPassword);
};

export default comparePassword;

We can now check the password properly!

// src/pages/api/auth/[...nextauth].ts
...
        const user = await prisma.user.findFirstOrThrow({
          where: {
            email: credentials?.email,
          },
        });
        const isValid = await comparePassword(
          credentials?.password ?? "",
          user.password
        );
        const userWithImage = await attachImage(user, "user");

        if (user && isValid) {
          // these are the data that the frontend can acquire
          return {
            id: user.id,
            email: user.email,
            name: user.username,
            image: userWithImage.profile_pic?.url,
          };
        } else {
          return null;
        }

Finally, let's update our user factory to use hashed password instead,

// prisma/factories/user.ts
import { faker } from "@faker-js/faker";
import { Prisma } from "@prisma/client";
import { hashSync } from "bcrypt";

export const fakeUser = (): Prisma.UserCreateInput => ({
  username: faker.name.firstName() + faker.name.lastName(),
  email: faker.internet.email(),
  // using hashSync here so that this function doesn't become async
  password: hashSync("secret", 12),
  description: faker.lorem.paragraph(),
});

We can now seed our database

yarn seed

Client Side

There's only one part where we need to setup the client side, _app.tsx to wrap our components in a SessionProvider to allow us to use useSession in retrieving the active user information

// src/pages/_app.tsx
...
export default function App({ Component, pageProps }: AppPropsWithLayout) {
  const [queryClient] = React.useState(() => new QueryClient());

  const getLayout = Component.getLayout || getDefaultLayout;

  return (
    <>
      <Head>
        <title>Page title</title>
        <meta
          name="viewport"
          content="minimum-scale=1, initial-scale=1, width=device-width"
        />
      </Head>

      <SessionProvider>
        <QueryClientProvider client={queryClient}>
          <Hydrate state={pageProps.dehydratedState}>
            <MantineProvider
              withGlobalStyles
              withNormalizeCSS
              theme={{
                colorScheme: "light",
              }}
            >
              <ModalsProvider modals={modals}>
                {getLayout(<Component {...pageProps} />)}
              </ModalsProvider>
            </MantineProvider>
          </Hydrate>
        </QueryClientProvider>
      </SessionProvider>
    </>
  );
}

and we are done!

Testing

Let's test if everything is working okay. Open up our pages/index.tsx, and add some arbitrary buttons for testing purposes. Remember to update to use email from your own database

import { signIn, useSession } from "next-auth/react";

export default function Home() {
  const posts = useQuery({ queryFn: getAllPosts, queryKey: ["all-posts"] });
  const session = useSession();
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className="space-y-4 flex flex-col items-center">
        {/* Here's the test sign in function */}
        <button
          onClick={() =>
            signIn("credentials", {
              email: "Ila25@hotmail.com",
              password: "secret",
            })
          }
        >
          Sign in
        </button>
        {/* Here's to check the current active session, remember to delete it later */}
        <button onClick={() => console.log(session)}>Current session</button>
        <StoryCarousel />
        {posts.isSuccess &&
          posts.data.posts.map((post, index) => (
            <Post post={post} key={index} />
          ))}
      </div>
    </>
  );
}

Clicking on Current Session should print status "unauthenticated"

Let's click Sign In, and check Current Session again, you should be able to see updated status and the active user object.

Route Protection

Next, let's try to protect our pages with authentication. That is, we will prevent unauthenticated users from browsing certain pages.

How do we do that? There are usually many ways, but my preferred way in Next.js is to set explicitly if the PageComponent is public, and create an AuthGuard which shows the LoginForm if unauthenticated.

So let's first update the type declaration of NextPageWithLayout in _app.tsx to include an attribute isPublic

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode;
} & {
  isPublic?: boolean;
};

Then, we will create an AuthGuard which checks the page's isPublic

// src/components/auth/AuthGuard.tsx
import { NextPageWithLayout } from "@/pages/_app";
import { useSession } from "next-auth/react";
import getDefaultLayout from "../layouts/DefaultLayout";

interface AuthGuardProps {
  Component: NextPageWithLayout;
  pageProps: any;
}

const AuthGuard = ({ Component, pageProps }: AuthGuardProps) => {
  const session = useSession();
  // Notice that I've moded the getLayout from _app.tsx to here
  const getLayout = Component.getLayout || getDefaultLayout;
  const isAuthenticated =
    Component.isPublic || session.status == "authenticated";

  return isAuthenticated ? (
    getLayout(<Component {...pageProps} />)
  ) : (
    <>Login Form</>
  );
};

export default AuthGuard;

And using it in our _app.tsx

// src/pages/_app.tsx
...
              <ModalsProvider modals={modals}>
                <AuthGuard Component={Component} pageProps={pageProps} />
              </ModalsProvider>
...

Is it working? Well we can open incognito browser and check it out, and you should see the text "Login Form" showing!

Splash Screen

So far everything is great, however, if you head back to the authenticated browser and reload the page, you'd notice a short flash of "Login Form" before it shows the main page.

This is because NextAuth needs to make a request to backend to determine the current active user, which is the third status of session, isLoading. To make the flow smoother, we can show a simple splash screen like Instagram's while loading

Here's the logo.svg for you, remember to put it in the /public folder. I did some tailwind magic to get the gradient text at the bottom there,

import Image from "next/image";

const SplashScreen = () => {
  return (
    <div className="relative flex flex-col justify-center items-center w-screen h-screen">
      <Image src="/logo.svg" alt="InstaNext" height={120} width={120} />
      <div className="absolute bottom-4 text-center">
        <p className="text-gray-600 text-sm font-semibold">from</p>
        <p className="text-lg font-semibold text-transparent bg-clip-text bg-gradient-to-br from-[#FF8121] via-[#FF4507] via-20% to-[#E341CC] to-70%">
          Shen Yien
        </p>
      </div>
    </div>
  );
};

export default SplashScreen;

And here's how it'll look

Let's put everything together in our AuthGuard

const AuthGuard = ({ Component, pageProps }: AuthGuardProps) => {
  const session = useSession();
  const getLayout = Component.getLayout || getDefaultLayout;
  // The session might go loading again, so check both data and status
  const isAuthenticated =
    session.status == "authenticated" || session.data != null;
  const canBrowse = Component.isPublic || isAuthenticated;

  // This is so that splash screen is only show once
  const [showSplash, setShowSplash] = useState(true);
  useEffect(() => {
    if (session.status != "loading") {
      setShowSplash(false);
    }
  }, [session, showSplash]);

  if (showSplash) {
    return <SplashScreen />;
  }

  return canBrowse ? getLayout(<Component {...pageProps} />) : <>Login Form</>;
};

Login Form

This is Instagram's Login Form, let's try to copy over

While Instagram built their own mobile phone with phone frames & changing images, I'll just use a simple screenshot I took of the phones and add in the form, download them here

// src/components/auth/LoginForm.tsx
import { Button, Image, PasswordInput, TextInput } from "@mantine/core";
import Link from "next/link";

const LoginForm = () => {
  return (
    <div className="pt-12">
      <div className="mx-auto max-w-[800px] grid grid-cols-2">
        <div className="flex justify-center items-center">
          <Image
            alt="InstaNext Promo Picture"
            src="/login-phone.png"
            width={420}
            height="auto"
          />
        </div>
        <div className="flex justify-center items-center">
          <div className="min-w-[340px]">
            <form className="border-gray-200 border-solid border-2 px-10 py-12">
              <Link href="/">
                <Image
                  src="/brand.svg"
                  alt="InstaNext"
                  width="200"
                  className="mx-auto"
                />
              </Link>
              <div className="mt-10 space-y-1 ">
                <TextInput placeholder="Email" />
                <PasswordInput placeholder="Password" />
              </div>
              <Button
                fullWidth
                className="mt-2 bg-blue-400 hover:bg-blue-500 rounded-md"
              >
                Log in
              </Button>
            </form>
            <div className="text-center border-gray-200 border-solid border-2 py-4 mt-3">
              Don{"'"}t have an account?{" "}
              <Link
                href="/auth/sign-up"
                className="text-blue-400 font-semibold"
              >
                Sign up
              </Link>
            </div>
          </div>
        </div>
      </div>
      <div className="mt-12 text-center text-sm text-gray-400">
        InstaNext - Instagram Clone by{" "}
        <Link
          href="https://shenyien.cyou"
          target="_blank"
          className="text-blue-400"
        >
          Shen Yien
        </Link>
      </div>
    </div>
  );
};

export default LoginForm;

And plugging in the LoginForm into our AuthGuard

...
  return canBrowse ? getLayout(<Component {...pageProps} />) : <LoginForm />;
...

And now, entering the page without being authenticated, it looks nice!

However, notice that the page doesn't do much now? Let's add some form logics, you can use Mantine's useForm for it.

We will make the button type submit, and for the form's onSubmit, we let Mantine's useForm handle it, and pass the values to NextAUth's signIn

import { Button, Image, PasswordInput, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { signIn } from "next-auth/react";
import Link from "next/link";

const LoginForm = () => {
  const form = useForm<{ email: string; password: string }>();
  return (
    <div className="pt-12">
      <div className="mx-auto max-w-[800px] grid grid-cols-2">
        <div className="flex justify-center items-center">
          <Image
            alt="InstaNext Promo Picture"
            src="/login-phone.png"
            width={420}
            height="auto"
          />
        </div>
        <div className="flex justify-center items-center">
          <div className="min-w-[340px]">
            <form
              className="border-gray-200 border-solid border-2 px-10 py-12"
              onSubmit={form.onSubmit((values) =>
                signIn("credentials", values)
              )}
            >
              <Link href="/">
                <Image
                  src="/brand.svg"
                  alt="InstaNext"
                  width="200"
                  className="mx-auto"
                />
              </Link>
              <div className="mt-10 space-y-1 ">
                <TextInput
                  placeholder="Email"
                  {...form.getInputProps("email")}
                />
                <PasswordInput
                  placeholder="Password"
                  {...form.getInputProps("password")}
                />
              </div>
              <Button
                fullWidth
                className="mt-2 bg-blue-400 hover:bg-blue-500 rounded-md"
                type="submit"
              >
                Log in
              </Button>
            </form>
            <div className="text-center border-gray-200 border-solid border-2 py-4 mt-3">
              Don{"'"}t have an account?{" "}
              <Link
                href="/auth/sign-up"
                className="text-blue-400 font-semibold"
              >
                Sign up
              </Link>
            </div>
          </div>
        </div>
      </div>
      <div className="mt-12 text-center text-sm text-gray-400">
        InstaNext - Instagram Clone by{" "}
        <Link
          href="https://shenyien.cyou"
          target="_blank"
          className="text-blue-400"
        >
          Shen Yien
        </Link>
      </div>
    </div>
  );
};

export default LoginForm;

You can test signing in now, it will work!

Updating Backend Logic

Finally, we can do some minor updates to our logic for /api/posts and /api/stories so that only those that the users are following will show.

Retrieving Active User

NextAuth provides a convenient getServerSession to retrieve current active user. However, the session object does not contain the current user's id, which is a little annoying to work with. Let's update our src/pages/api/auth[...nextauth].ts to include it!

// src/pages/api/auth[...nextauth].ts
export const authOptions: AuthOptions = {
...
  callbacks: {
    session: async ({ session, token }) => {
      if (session?.user) {
        // no idea why jwt token stores the user id in an attribute called sub
        session.user.id = token.sub ?? "";
      }
      return session;
    },
  },
}

At this point, the IDE is probably showing an error that id does not exist in User object, so to correct the type system, you can create a type declaration file at the root

// next-auth.d.ts
import "next-auth";

declare module "next-auth" {
  interface User {
    id: string;
  }

  interface Session {
    user: User;
  }
}

And it's fixed!

Posts

For posts, we can simply update findManyPosts.ts to include the user id in our where parameter.

PS: I also rename it to findFollowingPosts to better describe the function

// src/features/posts/findFollowingPosts.ts
import attachImage from "../images/attach-image";
import prisma from "@/utils/prisma";

const findFollowingPosts = async (userId: string) => {
  const posts = await prisma.post.findMany({
    include: {
      user: true,
      _count: {
        select: {
          liked_bys: true,
        },
      },
    },
    orderBy: { created_at: "desc" },
    // the user must the follower of the posters
    where: {
      user: {
        followers: {
          some: {
            follower_id: userId,
          },
        },
      },
    },
  });

  return await Promise.all(
    posts.map(async (post) => {
      const postsWithImages = await attachImage(post, "post");
      const postsWithAuthorWithImages = {
        ...postsWithImages,
        user: await attachImage(post.user, "user"),
      };
      return postsWithAuthorWithImages;
    })
  );
};

export default findFollowingPosts;

Then, we can pass the user id from the API route

// src/pages/api/posts/index.ts
...
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<AllPostsData>
) {
  const currentSession = await getServerSession(req, res, authOptions);
  const posts = await findFollowingPosts(currentSession?.user.id ?? "");
  res.status(200).json({ posts });
}

And we have filtered the posts successfully based on the follower logic

Stories

The implementation of stories would be almost equivalent to the posts, so I'll leave it to you. You can refer to my codes on GitHub.

Updating UI

Finally, let's go back to our index page

I just noticed that the StoryCarousel is a bit off, let's fix it.

// src/components/carousel/StoryCarousel.tsx
// 640px to make sure all items will be displayed nicely
...
    <Carousel
      height={120}
      maw={640}
      classNames={{
        control:
          "p-0 border-0 text-gray-600 data-[inactive=true]:invisible data-[inactive=true]:cursor-default bg-white",
        controls: "top-[20px]",
        viewport: "rounded-sm w-[640px]",
      }}
      previousControlIcon={<BiChevronLeft size={24} />}
      nextControlIcon={<BiChevronRight size={24} />}
      slideSize={70}
      slidesToScroll={4}
      draggable={false}
      align={"start"}
      w="640"
      slideGap={"sm"}
      // this is to prevent over-scrolling, which is default behaviour
      containScroll="trimSnaps"
    >
...

Much better.

Logout

We will tackle the easier one, currently, we have a Logout button in our SideBar.tsx which does nothing. We can use NextAuth's signOut for that purpose

// src/components/sidebar/SideBar.tsx
...
        <SideBarButton
          text="Logout"
          Icon={FiLogOut}
          onClick={() => signOut()}
        />
...

User Profile

Likewise, let's add in the current user's id to the user profile button. I decided to move out profile button as it is more

import { AiOutlineHome, AiFillHome, AiOutlineSearch } from "react-icons/ai";
import { ImSearch } from "react-icons/im";
import { BsPlusSquare, BsFillPlusSquareFill } from "react-icons/bs";
import { RiUser3Line, RiUser3Fill } from "react-icons/ri";
import { FiLogOut } from "react-icons/fi";
import Link from "next/link";
import { useRouter } from "next/router";
import Image from "next/image";
import SideBarButton from "./SideBarButton";
import { signOut, useSession } from "next-auth/react";

const links = [
  { name: "Home", route: "/", IconLine: AiOutlineHome, IconFilled: AiFillHome },
  {
    name: "Search",
    route: "/search",
    IconLine: AiOutlineSearch,
    IconFilled: ImSearch,
  },
  {
    name: "Create",
    route: "/new",
    IconLine: BsPlusSquare,
    IconFilled: BsFillPlusSquareFill,
  },
  // we will update the route to use user name in next part
  {
    name: "Profile",
    route: "/profile",
    IconLine: RiUser3Line,
    IconFilled: RiUser3Fill,
  },
];

const SideBar = () => {
  const router = useRouter();
  const currentPath = router.asPath;
  const session = useSession();

  return (
    <div className="w-[244px] fixed left-0 top-0 h-full px-4 py-6 border-r-[1px] border-solid border-gray-300">
      <div className="flex flex-col justify-between h-full">
        <div className="flex flex-col items-center">
          <Link href="/">
            <Image src="/brand.svg" alt="InstaNext" height="120" width="140" />
          </Link>
          <div className="space-y-2 w-full mt-12">
            {links.map((link, index) => {
              const isActive = link.route == currentPath;
              return (
                <SideBarButton
                  key={index}
                  Icon={isActive ? link.IconFilled : link.IconLine}
                  text={link.name}
                  href={link.route}
                  isActive={isActive}
                />
              );
            })}
            {/* This is the profile button */}
            <SideBarButton
              Icon={
                currentPath == `/users/${session.data?.user.name}`
                  ? RiUser3Fill
                  : RiUser3Line
              }
              text={"Profile"}
              href={`/users/${session.data?.user.name}`}
              isActive={currentPath == `/users/${session.data?.user.name}`}
            />
          </div>
        </div>
        <SideBarButton
          text="Logout"
          Icon={FiLogOut}
          onClick={() => signOut()}
        />
      </div>
    </div>
  );
};

export default SideBar;

Profile Picture

Lastly, Instagram will show the active user's username and profile picture on the top right, we will add that too

To do that, you can use a flex for the index.tsx page and add in the avatar just like that, here's the new index.tsx

// src/pages/index.tsx
import Head from "next/head";
import { useQuery } from "@tanstack/react-query";
import { getAllPosts } from "@/api/posts";
import Post from "@/components/posts/Post";
import StoryCarousel from "@/components/carousel/StoryCarousel";
import { signIn, useSession } from "next-auth/react";
import { Avatar, Text } from "@mantine/core";
import Link from "next/link";

export default function Home() {
  const posts = useQuery({ queryFn: getAllPosts, queryKey: ["all-posts"] });
  const session = useSession();
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className="flex flex-row items-start justify-center space-x-12">
        <div className="space-y-4 flex flex-col items-center">
          <StoryCarousel />
          {posts.isSuccess &&
            posts.data.posts.map((post, index) => (
              <Post post={post} key={index} />
            ))}
        </div>
        <div className="w-[240px]">
          <Link href={`/users/${session?.data?.user.name}`}>
            <div className="flex items-center space-x-2">
              <Avatar
                src={session?.data?.user.image ?? ""}
                alt={session?.data?.user.name ?? ""}
                size="64px"
                classNames={{ root: "rounded-full" }}
              />
              <Text className="font-semibold">{session?.data?.user.name}</Text>
            </div>
          </Link>
        </div>
      </div>
    </>
  );
}

And here's the final output

Summary

Got a little too lengthy here, but that's basically the end.

In this part, we have built an authentication system using NextAuth, and updated both frontend and backend so that they are aware of the current user. There's also a new sign-in page for the user to login.

In the next part, we will look into making POST requests in Next.js!

As usual, here's the link to the completed codes for this part on GitHub.