Back to Blog

Building a custom login page with Next.js, Auth.js & MongoDB

Auth.js is a great library to handle authentication in Next.js. It's easy to use and it's also easy to customize. Let's see how to build a custom login page with Auth.js + ShipFast.

Posted by

Supabase and ShipFast logo combined

Introduction

Auth.js is a great library that handles authentication in Next.js from magic links to OAuth providers. It's easy to use and customize. We will be going over how to setup Magic links, OAuth providers, and email the magic link to the user.

Auth.js is used by many companies like OpenAI's ChatGPT and many others, it is a scalable solution for authentication. We will be using Next.js App router version 14 & the Shipfast starter template.

1. Follow Shipfast User Auth

First, go to ShipFast User Auth and ensure you connect your database, setup Magic Links and OAuth.

2. Modify your NextAuth Config Settings

Ensure auth in config.ts has the following settings:

Ensuring Proper Config

Your custom login page must be set to the directory you intend to use in your app.

congig.ts
  auth: {
    // REQUIRED — the path to log in users. It's use to protect private routes (like /dashboard). It's used in apiClient (/libs/api.js) upon 401 errors from our API
    loginUrl: "/login",
    // REQUIRED — the path you want to redirect users after successfull login (i.e. /dashboard, /private). This is normally a private page for users to manage their accounts. It's used in apiClient (/libs/api.js) upon 401 errors from our API & in ButtonSignin.js
    callbackUrl: "/dashboard",
  },

3. Using Mailgun API + Editing next-auth.ts

Create a custom login page in npm i mailgun.js form-data and add the following code to handle the email templates we will build next and send the magic link to the user.

Setting up next-auth.ts

Add the following form-data, mailgun API and hbs handlebars for template running server side

libs/next-auth.ts
import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import EmailProvider from "next-auth/providers/email";
import { MongoDBAdapter } from "@auth/mongodb-adapter";
import Mailgun from "mailgun.js";
import formData from "form-data";
import config from "@/config";
import connectMongo from "./mongo";
import emailTemplate from "@/emails/confirm-email.hbs";
import welcomeTemplate from "@/emails/welcome.hbs";

interface NextAuthOptionsExtended extends NextAuthOptions {
  adapter: any;
}

interface VerificationRequest {
  identifier: string;
  url: string;
}

interface User {
  email?: string;
  // add other properties of user if needed
}

const mailgun = new Mailgun(formData);
const client = mailgun.client({
  username: "api",
  key: process.env.MAILGUN_API_KEY,
});

const sendVerificationRequest = async ({
  identifier,
  url,
}: VerificationRequest) => {
  try {
    const template = emailTemplate({
      signin_url: url,
      email: identifier,
      base_url: process.env.NEXTAUTH_URL.replace("/api/auth", ""),
    });
    await client.messages.create(process.env.MAILGUN_DOMAIN, {
      from: `Login to Buena AI ${process.env.EMAIL_FROM}`,
      to: identifier,
      subject: "Your sign-in link for Login Buena AI",
      html: template,
    });
  } catch (err) {
    console.log(`❌ Unable to send verification email to user (${identifier})`);
    console.log("because: ", err);
  }
};

const sendWelcomeEmail = async ({ user }: { user: User }) => {
  const { email } = user;

  try {
    const template = welcomeTemplate({
      base_url: process.env.NEXTAUTH_URL.replace("/api/auth", ""),
      support_email: "angel@aibusinessfirst.com",
    });
    await client.messages.create(process.env.MAILGUN_DOMAIN, {
      from: `Login to Buena AI ${process.env.EMAIL_FROM}`,
      to: email,
      subject: "Welcome from Buena AI",
      html: template,
    });
  } catch (error) {
    console.log(`❌ Unable to send welcome email to user (${email} )`);
  }
};
export const authOptions: NextAuthOptionsExtended = {
  // Set any random key in .env.local
  secret: process.env.NEXTAUTH_SECRET,
  providers: [
    GoogleProvider({
      // Follow the "Login with Google" tutorial to get your credentials
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
      async profile(profile) {
        return {
          id: profile.sub,
          name: profile.given_name ? profile.given_name : profile.name,
          email: profile.email,
          image: profile.picture,
          createdAt: new Date(),
        };
      },
    }),

    // Requires a MongoDB database. Set MONOGODB_URI env variable.
    ...(connectMongo
      ? [
          EmailProvider({
            sendVerificationRequest,
            maxAge: 10 * 60, // How long email links are valid for 10 min
          }),
        ]
      : []),
  ],
  // New users will be saved in Database (MongoDB Atlas). Each user (model) has some fields like name, email, image, etc..
  // Requires a MongoDB database. Set MONOGODB_URI env variable.
  // Learn more about the model type: https://next-auth.js.org/v3/adapters/models
  ...(connectMongo && { adapter: MongoDBAdapter(connectMongo) }),

  events: {
    createUser: async (message) => {
      const user: User = message.user;
      if (user.email) {
        await sendWelcomeEmail({ user });
      }
    },
  },
  callbacks: {
    session: async ({ session, token }) => {
      if (session?.user) {
        session.user.id = token.sub;
      }
      return session;
    },
  },
  session: {
    strategy: "jwt",
  },
  theme: {
    brandColor: config.colors.main,
    // Add you own logo below. Recommended size is rectangle (i.e. 200x50px) and show your logo + name.
    // It will be used in the login flow to display your logo. If you don't add it, it will look faded.
    logo: `https://${config.domainName}/images/Buena AI_logo.png`,
  },
};

export default NextAuth(authOptions);

4. Creating the Email Templates

Create a folder called email in root folder we will be using hbs to build templates and pass the magiclink information to the user as soon as login is successful or registration. We will be using handlerbars-loader and hbs to build the templates. Please install the following:npm i handlebars-loader --save

Setting declarations

We will need to add declarations to ensure our templates load and send off emails, add declarations.d.ts to root of your folder, same level as config.ts / next.config.ts:

declarations.d.ts
declare module "*.hbs" {
  const content: (context: any) => string;
  export default content;
}

Add the following webpack config to your app:

Adding handler-loaders to webpack config

Add the following webpack handle-loaders to your next.config.js to handle building the template server side and sending with Next.js 14:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: [
      // NextJS <Image> component needs to whitelist domains for src={}
      "lh3.googleusercontent.com",
      "pbs.twimg.com",
      "images.unsplash.com",
      "logos-world.net",
    ],
    remotePatterns: [
      {
        protocol: "https",
        hostname: "uploadthing.com",
      },
      {
        protocol: "https",
        hostname: "utfs.io",
      },
      {
        protocol: "https",
        hostname: "googleusercontent.com",
      },
      {
        protocol: "https",
        hostname: "*.googleusercontent.com",
      },
      {
        protocol: "https",
        hostname: "**.googleusercontent.com",
      },
    ],
  },
  webpack: (config) => {
    config.module.rules.push({
      test: /\.hbs$/,
      loader: "handlebars-loader",
    });

    return config;
  },
  experimental: {
    esmExternals: false, // THIS IS THE FLAG THAT MATTERS
  },
  typescript: {
    // !! WARN !!
    // Dangerously allow production builds to successfully complete even if
    // your project has type errors.
    // !! WARN !!
    ignoreBuildErrors: true,
  },
};

module.exports = nextConfig;

Create the following email templates in email folder:

Email Templating: Welcoming a User

This is how we welcome a user:

emails/welcome.hbs
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="x-apple-disable-message-reformatting" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="color-scheme" content="light dark" />
    <meta name="supported-color-schemes" content="light dark" />
    <title></title>
    <style type="text/css" rel="stylesheet" media="all">
      /* Base ------------------------------ */

      @import url("https://fonts.googleapis.com/css?family=Inter:400,700&display=swap");
      body {
        width: 100% !important;
        height: 100%;
        margin: 0;
        -webkit-text-size-adjust: none;
      }

      a {
        color: #3b82f6;
      }

      a img {
        border: none;
      }

      td {
        word-break: break-word;
      }

      .preheader {
        display: none !important;
        visibility: hidden;
        mso-hide: all;
        font-size: 1px;
        line-height: 1px;
        max-height: 0;
        max-width: 0;
        opacity: 0;
        overflow: hidden;
      }
      /* Type ------------------------------ */

      body,
      td,
      th {
        font-family: "Inter", Helvetica, Arial, sans-serif;
      }

      h1 {
        margin-top: 0;
        color: #333333;
        font-size: 22px;
        font-weight: bold;
        text-align: left;
      }

      h2 {
        margin-top: 0;
        color: #333333;
        font-size: 16px;
        font-weight: bold;
        text-align: left;
      }

      h3 {
        margin-top: 0;
        color: #333333;
        font-size: 14px;
        font-weight: bold;
        text-align: left;
      }

      td,
      th {
        font-size: 16px;
      }

      p,
      ul,
      ol,
      blockquote {
        margin: 0.4em 0 1.1875em;
        font-size: 16px;
        line-height: 1.625;
      }

      p.sub {
        font-size: 13px;
      }
      /* Utilities ------------------------------ */

      .align-right {
        text-align: right;
      }

      .align-left {
        text-align: left;
      }

      .align-center {
        text-align: center;
      }
      /* Buttons ------------------------------ */

      .button {
        background-color: #3b82f6;
        border-top: 10px solid #3b82f6;
        border-right: 18px solid #3b82f6;
        border-bottom: 10px solid #3b82f6;
        border-left: 18px solid #3b82f6;
        display: inline-block;
        color: #fff !important;
        text-decoration: none;
        border-radius: 0.375rem;
        box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
        -webkit-text-size-adjust: none;
        box-sizing: border-box;
      }

      @media only screen and (max-width: 500px) {
        .button {
          width: 100% !important;
          text-align: center !important;
        }
      }
      /* Attribute list ------------------------------ */

      .attributes {
        margin: 0 0 21px;
      }

      .attributes_content {
        background-color: #f4f4f7;
        padding: 16px;
      }

      .attributes_item {
        padding: 0;
      }
      /* Related Items ------------------------------ */

      .related {
        width: 100%;
        margin: 0;
        padding: 25px 0 0 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
      }

      .related_item {
        padding: 10px 0;
        color: #cbcccf;
        font-size: 15px;
        line-height: 18px;
      }

      .related_item-title {
        display: block;
        margin: 0.5em 0 0;
      }

      .related_item-thumb {
        display: block;
        padding-bottom: 10px;
      }

      .related_heading {
        border-top: 1px solid #cbcccf;
        text-align: center;
        padding: 25px 0 10px;
      }
      /* Data table ------------------------------ */

      body {
        background-color: #f2f4f6;
        color: #51545e;
      }

      p {
        color: #51545e;
      }

      .email-wrapper {
        width: 100%;
        margin: 0;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        background-color: #f2f4f6;
      }

      .email-content {
        width: 100%;
        margin: 0;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
      }
      /* Masthead ----------------------- */

      .email-masthead {
        padding: 25px 0;
        text-align: center;
      }

      .email-masthead_logo {
        width: 94px;
      }

      .email-masthead_name {
        font-size: 16px;
        font-weight: bold;
        color: #a8aaaf;
        text-decoration: none;
        text-shadow: 0 1px 0 white;
      }
      /* Body ------------------------------ */

      .email-body {
        width: 100%;
        margin: 0;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
      }

      .email-body_inner {
        width: 570px;
        margin: 0 auto;
        padding: 0;
        -premailer-width: 570px;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        background-color: #ffffff;
      }

      .email-footer {
        width: 570px;
        margin: 0 auto;
        padding: 0;
        -premailer-width: 570px;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        text-align: center;
      }

      .email-footer p {
        color: #a8aaaf;
      }

      .body-action {
        width: 100%;
        margin: 30px auto;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        text-align: center;
      }

      .body-sub {
        margin-top: 25px;
        padding-top: 25px;
        border-top: 1px solid #eaeaec;
      }

      .content-cell {
        padding: 45px;
      }
      /*Media Queries ------------------------------ */

      @media only screen and (max-width: 600px) {
        .email-body_inner,
        .email-footer {
          width: 100% !important;
        }
      }

      @media (prefers-color-scheme: dark) {
        body,
        .email-body,
        .email-body_inner,
        .email-content,
        .email-wrapper,
        .email-masthead,
        .email-footer {
          background-color: #333333 !important;
          color: #fff !important;
        }
        p,
        ul,
        ol,
        blockquote,
        h1,
        h2,
        h3,
        span,
        .purchase_item {
          color: #fff !important;
        }
        .attributes_content {
          background-color: #222 !important;
        }
        .email-masthead_name {
          text-shadow: none !important;
        }
      }

      :root {
        color-scheme: light dark;
        supported-color-schemes: light dark;
      }
    </style>
    <!--[if mso]>
      <style type="text/css">
        .f-fallback {
          font-family: Arial, sans-serif;
        }
      </style>
    <![endif]-->
  </head>
  <body>
    <span class="preheader"
      >Thanks for trying out Login Buena AI. We’ve pulled together some
      information and resources to help you get started.</span
    >
    <table
      class="email-wrapper"
      width="100%"
      cellpadding="0"
      cellspacing="0"
      role="presentation"
    >
      <tr>
        <td align="center">
          <table
            class="email-content"
            width="100%"
            cellpadding="0"
            cellspacing="0"
            role="presentation"
          >
            <tr>
              <td class="email-masthead">
                <a href="{{base_url}}" class="f-fallback email-masthead_name">
                  Welcome to Buena AI, Build Your Sales Agent!
                </a>
              </td>
            </tr>
            <!-- Email Body -->
            <tr>
              <td
                class="email-body"
                width="570"
                cellpadding="0"
                cellspacing="0"
              >
                <table
                  class="email-body_inner"
                  align="center"
                  width="570"
                  cellpadding="0"
                  cellspacing="0"
                  role="presentation"
                >
                  <!-- Body content -->
                  <tr>
                    <td class="content-cell">
                      <div class="f-fallback">
                        <h1>Welcome!</h1>
                        <p>
                          Thanks for joining us. Your AI Sales Agent is ready to
                          get started.
                        </p>
                        <p>
                          Whether you have questions, need help with getting set
                          up, or just want to send a virtual high five, please
                          feel free to
                          <a href="mailto:{{support_email}}"
                            >email our customer success team</a
                          >.
                        </p>
                        <p>
                          We're looking forward to helping you succeed with Your
                          Sales Agents!
                        </p>
                        <p>Thanks, <br />The Buena AI Team</p>
                      </div>
                    </td>
                  </tr>
                </table>
              </td>
            </tr>
            <tr>
              <td>
                <table
                  class="email-footer"
                  align="center"
                  width="570"
                  cellpadding="0"
                  cellspacing="0"
                  role="presentation"
                >
                  <tr>
                    <td class="content-cell" align="center">
                      <p class="f-fallback sub align-center">
                        &copy; 2023 Buena AI. All rights reserved.
                      </p>
                    </td>
                  </tr>
                </table>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>
  </body>
</html>

Email Templating: Magic links

This is how we send magic links to users:

emails/confirm-email.hbs
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="x-apple-disable-message-reformatting" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="color-scheme" content="light dark" />
    <meta name="supported-color-schemes" content="light dark" />
    <title></title>
    <style type="text/css" rel="stylesheet" media="all">
      /* Base ------------------------------ */

      @import url("https://fonts.googleapis.com/css?family=Inter:400,700&display=swap");
      body {
        width: 100% !important;
        height: 100%;
        margin: 0;
        -webkit-text-size-adjust: none;
      }

      a {
        color: #3b82f6;
      }

      a img {
        border: none;
      }

      td {
        word-break: break-word;
      }

      .preheader {
        display: none !important;
        visibility: hidden;
        mso-hide: all;
        font-size: 1px;
        line-height: 1px;
        max-height: 0;
        max-width: 0;
        opacity: 0;
        overflow: hidden;
      }
      /* Type ------------------------------ */

      body,
      td,
      th {
        font-family: "Inter", Helvetica, Arial, sans-serif;
      }

      h1 {
        margin-top: 0;
        color: #333333;
        font-size: 22px;
        font-weight: bold;
        text-align: left;
      }

      h2 {
        margin-top: 0;
        color: #333333;
        font-size: 16px;
        font-weight: bold;
        text-align: left;
      }

      h3 {
        margin-top: 0;
        color: #333333;
        font-size: 14px;
        font-weight: bold;
        text-align: left;
      }

      td,
      th {
        font-size: 16px;
      }

      p,
      ul,
      ol,
      blockquote {
        margin: 0.4em 0 1.1875em;
        font-size: 16px;
        line-height: 1.625;
      }

      p.sub {
        font-size: 13px;
      }
      /* Utilities ------------------------------ */

      .align-right {
        text-align: right;
      }

      .align-left {
        text-align: left;
      }

      .align-center {
        text-align: center;
      }
      /* Buttons ------------------------------ */

      .button {
        background-color: #3b82f6;
        border-top: 10px solid #3b82f6;
        border-right: 18px solid #3b82f6;
        border-bottom: 10px solid #3b82f6;
        border-left: 18px solid #3b82f6;
        display: inline-block;
        color: #fff !important;
        text-decoration: none;
        border-radius: 0.375rem;
        box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
        -webkit-text-size-adjust: none;
        box-sizing: border-box;
      }

      @media only screen and (max-width: 500px) {
        .button {
          width: 100% !important;
          text-align: center !important;
        }
      }
      /* Attribute list ------------------------------ */

      .attributes {
        margin: 0 0 21px;
      }

      .attributes_content {
        background-color: #f4f4f7;
        padding: 16px;
      }

      .attributes_item {
        padding: 0;
      }
      /* Related Items ------------------------------ */

      .related {
        width: 100%;
        margin: 0;
        padding: 25px 0 0 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
      }

      .related_item {
        padding: 10px 0;
        color: #cbcccf;
        font-size: 15px;
        line-height: 18px;
      }

      .related_item-title {
        display: block;
        margin: 0.5em 0 0;
      }

      .related_item-thumb {
        display: block;
        padding-bottom: 10px;
      }

      .related_heading {
        border-top: 1px solid #cbcccf;
        text-align: center;
        padding: 25px 0 10px;
      }
      /* Data table ------------------------------ */

      body {
        background-color: #f2f4f6;
        color: #51545e;
      }

      p {
        color: #51545e;
      }

      .email-wrapper {
        width: 100%;
        margin: 0;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        background-color: #f2f4f6;
      }

      .email-content {
        width: 100%;
        margin: 0;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
      }
      /* Masthead ----------------------- */

      .email-masthead {
        padding: 25px 0;
        text-align: center;
      }

      .email-masthead_logo {
        width: 94px;
      }

      .email-masthead_name {
        font-size: 16px;
        font-weight: bold;
        color: #a8aaaf;
        text-decoration: none;
        text-shadow: 0 1px 0 white;
      }
      /* Body ------------------------------ */

      .email-body {
        width: 100%;
        margin: 0;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
      }

      .email-body_inner {
        width: 570px;
        margin: 0 auto;
        padding: 0;
        -premailer-width: 570px;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        background-color: #ffffff;
      }

      .email-footer {
        width: 570px;
        margin: 0 auto;
        padding: 0;
        -premailer-width: 570px;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        text-align: center;
      }

      .email-footer p {
        color: #a8aaaf;
      }

      .body-action {
        width: 100%;
        margin: 30px auto;
        padding: 0;
        -premailer-width: 100%;
        -premailer-cellpadding: 0;
        -premailer-cellspacing: 0;
        text-align: center;
      }

      .body-sub {
        margin-top: 25px;
        padding-top: 25px;
        border-top: 1px solid #eaeaec;
      }

      .content-cell {
        padding: 45px;
      }
      /*Media Queries ------------------------------ */

      @media only screen and (max-width: 600px) {
        .email-body_inner,
        .email-footer {
          width: 100% !important;
        }
      }

      @media (prefers-color-scheme: dark) {
        body,
        .email-body,
        .email-body_inner,
        .email-content,
        .email-wrapper,
        .email-masthead,
        .email-footer {
          background-color: #333333 !important;
          color: #fff !important;
        }
        p,
        ul,
        ol,
        blockquote,
        h1,
        h2,
        h3,
        span,
        .purchase_item {
          color: #fff !important;
        }
        .attributes_content {
          background-color: #222 !important;
        }
        .email-masthead_name {
          text-shadow: none !important;
        }
      }

      :root {
        color-scheme: light dark;
        supported-color-schemes: light dark;
      }
    </style>
    <!--[if mso]>
      <style type="text/css">
        .f-fallback {
          font-family: Arial, sans-serif;
        }
      </style>
    <![endif]-->
  </head>
  <body>
    <span class="preheader">This link will expire in 10 min.</span>
    <table
      class="email-wrapper"
      width="100%"
      cellpadding="0"
      cellspacing="0"
      role="presentation"
    >
      <tr>
        <td align="center">
          <table
            class="email-content"
            width="100%"
            cellpadding="0"
            cellspacing="0"
            role="presentation"
          >
            <tr>
              <td class="email-masthead">
                <a href="{{base_url}}" class="f-fallback email-masthead_name">
                  Login to Buena AI
                </a>
              </td>
            </tr>
            <!-- Email Body -->
            <tr>
              <td
                class="email-body"
                width="570"
                cellpadding="0"
                cellspacing="0"
              >
                <table
                  class="email-body_inner"
                  align="center"
                  width="570"
                  cellpadding="0"
                  cellspacing="0"
                  role="presentation"
                >
                  <!-- Body content -->
                  <tr>
                    <td class="content-cell">
                      <div class="f-fallback">
                        <p>
                          Click the link below to log in to Login to AI Business
                          First.<br />
                          This link will expire in 10 minutes.
                        </p>
                        <!-- Action -->
                        <table
                          class="body-action"
                          align="center"
                          width="100%"
                          cellpadding="0"
                          cellspacing="0"
                          role="presentation"
                        >
                          <tr>
                            <td align="center">
                              <!-- Border based button
           https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
                              <table
                                width="100%"
                                border="0"
                                cellspacing="0"
                                cellpadding="0"
                                role="presentation"
                              >
                                <tr>
                                  <td>
                                    <a
                                      href="{{signin_url}}"
                                      class="f-fallback button"
                                      target="_blank"
                                      >Login to Buena AI</a
                                    >
                                  </td>
                                </tr>
                              </table>
                            </td>
                          </tr>
                        </table>
                        <p>
                          Confirming this request will securely log you in using
                          your email: {{email}}.
                        </p>
                        <p>Best,<br />The Buena AI Team</p>
                        <!-- Sub copy -->
                        <table class="body-sub" role="presentation">
                          <tr>
                            <td>
                              <p class="f-fallback sub">
                                If you&apos;re having trouble with the button
                                above, copy and paste the URL below into your
                                web browser.
                              </p>
                              <p class="f-fallback sub">{{signin_url}}</p>
                            </td>
                          </tr>
                        </table>
                      </div>
                    </td>
                  </tr>
                </table>
              </td>
            </tr>
            <tr>
              <td>
                <table
                  class="email-footer"
                  align="center"
                  width="570"
                  cellpadding="0"
                  cellspacing="0"
                  role="presentation"
                >
                  <tr>
                    <td class="content-cell" align="center">
                      <p class="f-fallback sub align-center">
                        &copy; 2023 Buena AI. All rights reserved.
                      </p>
                    </td>
                  </tr>
                </table>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>
  </body>
</html>

5. Editing tsconfig.json

Edit tsconfig.json to include the following declarations, bundler, typeRoots, and path:

Setting up tsconfig.json

Add the following settings to ensure compilation happens of your templates

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "typeRoots": ["./node_modules/@types", "./declarations.d.ts"],
    "target": "es2017",
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noImplicitAny": true,
    "jsx": "preserve",
    "paths": {
      "@/*": ["./*"],
      "@components/*": ["./components/*"],
      "@/emails/*": ["./emails/*"],
      "@/templates/*": ["./templates/*"],
      "@/libs/*": ["./libs/*"],
      "@/server/*": ["./server/*"],
      "@/utils/*": ["./utils/*"]
    },
    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": [
    "next-env.d.ts",
    "declarations.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

6. Adding Your Custom Login / Register Page

Add your page or custom page, there are many ways to do this, and tailwindcss has a great starter template for this. You can also use the following code which is what I use along with a component we will build to show the user the magic link has been sent to their email.

Main Signin Index Page:

This is my main custom login page:

@/templates/SignInPage/Index.tsx
"use client";
import Link from "next/link";
import { getSEOTags } from "@/libs/seo";
import Image from "@/components/Image";
import Icon from "@/components/Icon";
import Form from "./Form";

export const metadata = getSEOTags({
  title: "Dashboard - Buena AI",
  canonicalUrlRelative: "/login",
});

const SignInPage = () => {
  return (
    <div className="relative flex min-h-screen min-h-screen-ios lg:p-6 md:px-6 md:pt-16 md:pb-10">
      <div className="relative shrink-0 w-[40rem] p-20 overflow-hidden 2xl:w-[37.5rem] xl:w-[30rem] xl:p-10 lg:hidden">
        <div className="max-w-[25.4rem]">
          <div className="mb-4 h2 text-n-1">Unlock the power of Sales AI</div>
          <div className="body1 text-n-3">
            Deploy a fleet of AI-powered sales assistants to help you close more
            deals, faster.
          </div>
        </div>
        <div className="absolute top-52 left-5 right-5 h-[50rem] xl:top-24">
          <Image
            className="object-contain"
            src="/images/create-pic.png"
            fill
            sizes="(max-width: 1180px) 50vw, 33vw"
            alt=""
          />
        </div>
      </div>
      <div className="flex grow my-6 mr-6 p-10 bg-n-1 rounded-[1.25rem] lg:m-0 md:p-0 dark:bg-n-6">
        <Form />
      </div>
      <Link
        className="group absolute top-12 right-12 flex justify-center items-center w-10 h-10 bg-n-2 rounded-full text-0 transition-colors hover:bg-primary-1 md:top-6 md:right-6"
        href="https://aibusinessfirst.com/"
      >
        <Icon
          className="fill-n-7 transition-colors group-hover:fill-n-1"
          name="close"
        />
      </Link>
    </div>
  );
};

export default SignInPage;

Create Account Form:

This is my form and I use the @chakra-ui make sure you npm install @chakra-ui/react and npm i react-icons/io

@/templates/SignInPage/Form/CreateAccount/Index.tsx
"use client";
import React from "react";
import { useState } from "react";
import { signIn } from "next-auth/react";
import Link from "next/link";

import Field from "@/components/Field";
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
import SuccessRegistration from "@/components/SuccessRegistration";

const colors = {
  brand: {
    50: "#ecefff",
    100: "#cbceeb",
    200: "#a9aed6",
    300: "#888ec5",
    400: "#666db3",
    500: "#4d5499",
    600: "#3c4178",
    700: "#2a2f57",
    800: "#181c37",
    900: "#080819",
  },
};
const config = {
  initialColorMode: "dark",
  useSystemColorMode: false,
};

const theme = extendTheme({ colors, config });

const CreateAccount = () => {
  const [email, setEmail] = useState("");
  const [loading, setLoading] = useState(false);
  const [show, setShow] = useState(false);

  const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(event.target.value);
  };

  const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
    event
  ) => {
    event.preventDefault();
    try {
      if (loading) return;

      setLoading(true);

      signIn("email", {
        email,
        redirect: false,
        callbackUrl: `${window.location.origin}/`,
      })
        .then(() => {
          setShow(true);
          setLoading(true);
        })
        .catch((error) => {
          console.log(error);
          setLoading(false);
        });
    } catch (error) {
      console.log(error);
      setLoading(false);
    }
  };

  // if the user has successfully signed up, we show a success message
  if (loading) {
    return (
      <ChakraProvider theme={theme}>
        <SuccessRegistration email={email} />
      </ChakraProvider>
    );
  }

  return (
    <form action="" onSubmit={handleSubmit}>
      <Field
        className="mb-4"
        classInput="dark:bg-n-7 dark:border-n-7 dark:focus:bg-transparent"
        placeholder="Email"
        icon="email"
        type="email"
        onChange={handleEmailChange}
        required
      />
      <button className="btn-blue btn-large w-full mb-6" type="submit">
        Create Account
      </button>
      <div className="text-center caption1 text-n-4">
        By creating an account, you agree to our{" "}
        <Link
          className="text-n-5 transition-colors hover:text-n-7 dark:text-n-3 dark:hover:text-n-1"
          href="/terms-of-service"
        >
          Terms of Service
        </Link>{" "}
        and{" "}
        <Link
          className="text-n-5 transition-colors hover:text-n-7 dark:text-n-3 dark:hover:text-n-1"
          href="/privacy-policy"
        >
          Privacy & Cookie Statement
        </Link>
        .
      </div>
    </form>
  );
};

export default CreateAccount;

SignIn Page Form:

This is where the user is able to sign in and we import next-auth react

@/templates/SignInPage/Form/Index.tsx
import { signIn } from "next-auth/react";
import { Tab } from "@headlessui/react";

import { useColorMode } from "@chakra-ui/color-mode";

import Logo from "@/components/Logo";
import Image from "@/components/Image";

import SignIn from "./SignIn";
import CreateAccount from "./CreateAccount";

const tabNav = ["Sign in", "Create account"];

type FormProps = {};

const Form = () => {
  const { colorMode } = useColorMode();
  const isLightMode = colorMode === "light";

  return (
    <div className="w-full max-w-[31.5rem] m-auto">
      {/* {forgot ? (
        <ForgotPassword onClick={() => setForgot(false)} />
      ) : ( */}
      <>
        <Logo className="max-w-[11.875rem] mx-auto mb-8" dark={isLightMode} />
        <Tab.Group defaultIndex={0}>
          <Tab.List className="flex mb-8 p-1 bg-n-2 rounded-xl dark:bg-n-7">
            {tabNav.map((button, index) => (
              <Tab
                className="basis-1/2 h-10 rounded-[0.625rem] base2 font-semibold text-n-4 transition-colors outline-none hover:text-n-7 ui-selected:bg-n-1 ui-selected:text-n-7 ui-selected:shadow-[0_0.125rem_0.125rem_rgba(0,0,0,0.07),inset_0_0.25rem_0.125rem_#FFFFFF] tap-highlight-color dark:hover:text-n-1 dark:ui-selected:bg-n-6 dark:ui-selected:text-n-1 dark:ui-selected:shadow-[0_0.125rem_0.125rem_rgba(0,0,0,0.07),inset_0_0.0625rem_0.125rem_rgba(255,255,255,0.02)]"
                key={index}
              >
                {button}
              </Tab>
            ))}
          </Tab.List>
          <button
            onClick={() => {
              signIn("google");
            }}
            className="btn-stroke-light btn-large w-full mb-3"
          >
            <Image src="/images/google.svg" width={24} height={24} alt="" />
            <span className="ml-4">Continue with Google</span>
          </button>
          {/* Add more social login buttons here */}
          {/* <button className="btn-stroke-light btn-large w-full">
                            <Image
                                src="/images/apple.svg"
                                width={24}
                                height={24}
                                alt=""
                            />
                            <span className="ml-4">Continue with Apple</span>
                        </button> */}
          <div className="flex items-center my-8 md:my-4">
            <span className="grow h-0.25 bg-n-4/50"></span>
            <span className="shrink-0 mx-5 text-n-4/50">OR</span>
            <span className="grow h-0.25 bg-n-4/50"></span>
          </div>
          <Tab.Panels>
            <Tab.Panel>
              <SignIn />
            </Tab.Panel>
            <Tab.Panel>
              <CreateAccount />
            </Tab.Panel>
          </Tab.Panels>
        </Tab.Group>
      </>
    </div>
  );
};

export default Form;

Add SuccessSignIn component

Add the following component:

@/templates/components/SuccessSignIn/index.tsx
import { Box, Flex, Icon, chakra } from "@chakra-ui/react";

import React from "react";
import { IoMdCheckmarkCircle } from "react-icons/io";

type SuccessSignInProps = {
  email: string;
};

export default function SuccessSignIn({ email }: SuccessSignInProps) {
  return (
    <Flex
      w="full"
      bg="#edf3f8"
      _dark={{ bg: "#232627" }}
      p={50}
      shadow="md"
      alignItems="center"
      justifyContent="center"
    >
      <Flex
        maxW="sm"
        w="full"
        mx="auto"
        bg="white"
        _dark={{ bg: "gray.800" }}
        rounded="lg"
        overflow="hidden"
      >
        <Flex justifyContent="center" alignItems="center" w={12} bg="green.500">
          <Icon as={IoMdCheckmarkCircle} color="white" boxSize={6} />
        </Flex>

        <Box mx={-3} py={2} px={4}>
          <Box mx={3}>
            <chakra.span
              color="green.500"
              _dark={{ color: "green.400" }}
              fontWeight="bold"
            >
              Success
            </chakra.span>
            <chakra.p
              color="gray.600"
              _dark={{ color: "gray.200" }}
              fontSize="sm"
            >
              We sent a login link to: <b>{email}</b>. Please check your email
              to login.
            </chakra.p>
          </Box>
        </Box>
      </Flex>
    </Flex>
  );
}

Add SuccessRegistration component

Add the following component:

@/templates/components/SuccessRegistration/index.tsx
import { Box, Flex, Icon, chakra } from "@chakra-ui/react";

import React from "react";
import { IoMdCheckmarkCircle } from "react-icons/io";

type SuccessRegistrationProps = {
  email: string;
};

export default function SuccessRegistration({
  email,
}: SuccessRegistrationProps) {
  return (
    <Flex
      w="full"
      bg="#edf3f8"
      _dark={{ bg: "#232627" }}
      p={50}
      shadow="md"
      alignItems="center"
      justifyContent="center"
    >
      <Flex
        maxW="sm"
        w="full"
        mx="auto"
        bg="white"
        _dark={{ bg: "gray.800" }}
        rounded="lg"
        overflow="hidden"
      >
        <Flex justifyContent="center" alignItems="center" w={12} bg="green.500">
          <Icon as={IoMdCheckmarkCircle} color="white" boxSize={6} />
        </Flex>

        <Box mx={-3} py={2} px={4}>
          <Box mx={3}>
            <chakra.span
              color="green.500"
              _dark={{ color: "green.400" }}
              fontWeight="bold"
            >
              Success
            </chakra.span>
            <chakra.p
              color="gray.600"
              _dark={{ color: "gray.200" }}
              fontSize="sm"
            >
              We sent a verification link to <b>{email}</b>. Please check your
              email to login.
            </chakra.p>
          </Box>
        </Box>
      </Flex>
    </Flex>
  );
}

7. Done

Congrats! You now have a fully effective custom login page with Next.js App router, Auth.js, and MongoDB. You can now customize the UI and have this page be your own.

Supabase and ShipFast logo combinedSupabase and ShipFast logo combined
linkedinX