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
Angel PadillaIntroduction
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.
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
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:
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:
/** @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:
<!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">
© 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:
<!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'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">
© 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
{
"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:
"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
"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
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:
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:
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.