Next.js Authentication

Next.js

Installation:

npx create-next-app@latest

your-app-name 

<--- Backend Code Start Here --->

You Can Define environment variables: 

Create a .env file in next.js project:

And define environment variables as per your requirement:

MONGO_URI="your mongodb connection string"
SECRET_TOKEN="your secret token" //must be in capital
DOMAIN="http://localhost:3000/"

Connect Next.js to MongoDB:

Install Mongoose

  • npm i mongoose 

 In scr folder create a new folder dbConfig

In dbConfig folder create a new file dbConfig.ts

In dbConfig.ts file:

import mongoose, { connection } from "mongoose";

export async function connect() {
    try {
        mongoose.connect(process.env.MONGO_URI!);
        connection.on("connected", () => {
            console.log("Database Connected.");
        });
        connection.on("error", (err) => {
            console.log("Database Connection Error" + err);
        });
    } catch (error: any) {
        console.log("Something went wrong in connecting to Database.");
        console.log(error.message);
    }
}

Create a New Model:

In scr folder create a new folder models

In models folder create a new file userModel.js

In userModel.js file:

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
    userName: {
        type: String,
        required: [true, "Please provide a user name."]
    },
    email: {
        type: String,
        required: [true, "Please provide an email."],
        unique: true,
    },
    password: {
        type: String,
        required: [true, "Please provide a strong password."]
    },
    isVerified: {
        type: Boolean,
        default: false
    },
    isAdmin: {
        type: Boolean,
        default: false
    },
    forgotPasswordToken: String,
    forgotPasswordTokenExpiry: Date,
    verifyToken: String,
    verifyTokenExpiry: Date,
})

const User = mongoose.model.users || mongoose.model("users", userSchema)

export default User;

Create a New User or Register User:

In scr/app folder create a new folder api/users/register

In api/users/register folder create a new file route.ts

In route.ts file:

Install bcryptjs: 

  • npm i bcryptjs
// Without Verify User

import { connect } from "@/dbConfig/dbConfig";
import User from "@/models/userModel";
import { NextRequest, NextResponse } from "next/server";
import bcryptjs from "bcryptjs";

connect();

export async function POST(request: NextRequest) {
    try {
        const reqBody = await request.json();
        const { username, email, password } = reqBody;

        const user = User.find({ email });
        if (user) {
            return NextResponse.json(
                { error: "User is already exist." },
                { status: 400 }
            );
        }

        const salt = await bcryptjs.genSalt(10);
        const hashedPassword = await bcryptjs.hash(password, salt);

        const newUser = new User({
            username,
            email,
            password: hashedPassword,
        });

        const savedUser = await newUser.save();
        if (savedUser) {
            return NextResponse.json(
                { message: "User Registered Successfully", success: true, savedUser },
                { status: 200 }
            );
        } else {
            return NextResponse.json(
                { error: "Something Went Wrong" },
                { status: 500 }
            );
        }
    } catch (error: any) {
        console.log(error.message);
        return NextResponse.json({ error: error.message }, { status: 500 });
    }
}

If you want to verify the user:

In scr folder create a new folder helpers

In helpers folder create a new file mailer.ts

In mailer.ts file:

Install nodemailer:

  • npm i nodemailer
// With Verify User

import User from "@/models/userModel";
import nodemailer from "nodemailer";
import bcryptjs from "bcryptjs";

export const sendEmail = async ({ email, emailType, userId }: any) => {
    try {
        const hashedToken = await bcryptjs.hash(userId.toString(), 10);

        if (emailType === "VERIFY") {
            await User.findByIdAndUpdate(userId, {
                $set: {
                    verifyToken: hashedToken,
                    verifyTokenExpiry: Date.now() + 3600000,
                },
            });
        } else if (emailType === "RESET") {
            await User.findByIdAndUpdate(userId, {
                $set: {
                    forgotPasswordToken: hashedToken,
                    forgotPasswordTokenExpiry: Date.now() + 3600000,
                },
            });
        }

        var transport = nodemailer.createTransport({
            host: "sandbox.smtp.mailtrap.io",
            port: 2525,
            auth: {
                user: "your-secret-id",
                pass: "your-secret-password",
            },
        });

        const mailOptions = {
            from: "ms6498289@gmail.com",
            to: email,
            subject:
                emailType === "VERIFY" ? "Verify your email" : "Reset your password",
            html: `<p>Click <a href="${
                process.env.DOMAIN
            }/verifyemail?token=${hashedToken}">here</a> to  ${
                emailType === "VERIFY" ? "verify your email" : "reset your password"
            }
            or copy and paste the link below in your browser.
            </br> ${process.env.DOMAIN}/verifyemail?token=${hashedToken}
            </p>`,
        };

        const mailResponse = await transport.sendMail(mailOptions);
        return mailResponse;
    } catch (error: any) {
        throw new Error(error.messsage);
    }
};

After that there are some changes:

In api/users/register/route.ts

import { connect } from "@/dbConfig/dbConfig";
import User from "@/models/userModel";
import { NextRequest, NextResponse } from "next/server";
import bcryptjs from "bcryptjs";
import { sendEmail } from "@/helpers/mailer";

connect();

export async function POST(request: NextRequest) {
    try {
        const reqBody = await request.json();
        const { username, email, password } = reqBody;

        const user = await User.findOne({ email });
        if (user) {
            return NextResponse.json(
                { error: "User is already exist" },
                { status: 400 }
            );
        }

        const salt = await bcryptjs.genSalt(10);
        const hashedPassword = await bcryptjs.hash(password, salt);

        const newUser = new User({
            username,
            email,
            password: hashedPassword,
        });

        const savedUser = await newUser.save();

        // send verification email
        const userId = savedUser._id;
        await sendEmail({ email, emailType: "VERIFY", userId: userId });
        return NextResponse.json({
            message: "User registered successfully",
            success: true,
            savedUser,
        });
    } catch (error: any) {
        console.log(error.message);
        return NextResponse.json({ error: error.message }, { status: 500 });
    }
}

After sending the mail we have to verify the email:

In scr/app folder create a new folder api/users/verifyemail

In api/users/verifyemail folder create a new file route.ts

In route.ts file:

import { connect } from "@/dbConfig/dbConfig";
import User from "@/models/userModel";
import { NextRequest, NextResponse } from "next/server";

connect();

export async function POST(request: NextRequest) {
    try {
        const reqBody = await request.json();
        const { token } = reqBody;

        const user = await User.findOne({
            verifyToken: token,
            verifyTokenExpiry: { $gt: Date.now() },
        });

        if (!user) {
            return NextResponse.json({ error: "Invalid token" }, { status: 400 });
        }

        user.isVerified = true;
        user.verifyToken = undefined;
        user.verifyTokenExpiry = undefined;

        await user.save();
        return NextResponse.json(
            { message: "Email Verified Successfully", success: true },
            { status: 400 }
        );
    } catch (error: any) {
        console.log({ error: error.message });
        return NextResponse.json({ error: error.message }, { status: 500 });
    }
}

Login User:

In scr/app folder create a new folder api/users/login

In api/users/login folder create a new file route.ts

In route.ts file:

Install jsonwebtoken:

  • npm i jsonwebtoken
import { connect } from "@/dbConfig/dbConfig";
import User from "@/models/userModel";
import { NextRequest, NextResponse } from "next/server";
import bcryptjs from "bcryptjs";
import jwt from "jsonwebtoken";

connect();

export async function POST(request: NextRequest) {
    try {
        const reqBody = await request.json();
        const { email, password } = reqBody;

        const user = await User.findOne({ email });
        if (!user) {
            return NextResponse.json(
                { error: "User does not exist" },
                { status: 400 }
            );
        }

        const isValidPassword = await bcryptjs.compare(password, user.password);

        if (!isValidPassword) {
            return NextResponse.json(
                { error: "Check your credentials" },
                { status: 400 }
            );
        }

        const tokenData = {
            id: user._id,
            username: user.username,
            email: user.email,
        };

        const token = await jwt.sign(tokenData, process.env.SECRET_TOKEN!, {
            expiresIn: "1d",
        });

        const response = NextResponse.json({
            message: "Logged In Successfully",
            success: true,
        });

        response.cookies.set("token", token, { httpOnly: true });

        return response;
    } catch (error: any) {
        console.log(error.message);
        return NextResponse.json({ error: error.message }, { status: 500 });
    }
}

Logout User:

In scr/app folder create a new folder api/users/logout

In api/users/logout folder create a new file route.ts

In route.ts file:

import { connect } from "@/dbConfig/dbConfig";
import { NextRequest, NextResponse } from "next/server";

connect();

export async function GET(request: NextRequest) {
    try {
        const response = NextResponse.json({
            message: "Logged Out Successfully",
            success: true,
        });

        response.cookies.set("token", "", {
            httpOnly: true,
            expires: new Date(0),
        });

        return response;
    } catch (error: any) {
        console.log(error.message);
        return NextResponse.json({ error: error.message }, { status: 500 });
    }
}

Before Get User Profile Data we have to get data from the token:

To get data from token:

In helpers folder create a another new file getDataFromToken.ts

In getDataFromToken.ts file:

import { NextRequest, NextResponse } from "next/server";
import jwt from "jsonwebtoken";

export const getDataFromToken = (request: NextRequest) => {
    try {
        const token = request.cookies.get("token")?.value || "";

        const decodedToken: any = jwt.verify(token, process.env.SECRET_TOKEN!);
        return decodedToken.id;
    } catch (error: any) {
        console.log(error.message);
        throw new Error(error.message);
    }
};

 

Get User Profile Data:

In scr/app folder create a new folder api/users/my-profile

In api/users/my-profile folder create a new file route.ts

In route.ts file:

import { connect } from "@/dbConfig/dbConfig";
import User from "@/models/userModel";
import { NextRequest, NextResponse } from "next/server";
import { getDataFromToken } from "@/helpers/getDataFromToken";

connect();

export async function GET(request: NextRequest) {
    try {
        const userId = await getDataFromToken(request);
        const user = await User.findOne({ _id: userId }).select("-password");

        return NextResponse.json({
            message: "User found",
            data: user,
        });
    } catch (error: any) {
        console.log(error.message);
        return NextResponse.json({ error: error.message }, { status: 500 });
    }
}

Note: Please Check And Run Your API To Postman:

<--- Backend Code End Here --->

<--- Frontend Code Start Here --->

Register User:

To Register User:

In scr/app folder create a new folder /register

In /register folder create a new file page.tsx

In page.tsx file: 

But first we need to install some packages:

  •  npm i axios
  • npm i react-hot-toast

Note: To use Client Components, you can add the React "use client" directive at the top of a file, above your imports. 

"use client";

import axios from "axios";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import toast, { Toaster } from "react-hot-toast";

export default function page() {
    const router = useRouter();

    const [user, setUser] = useState({
        username: "",
        email: "",
        password: "",
    });

    const [disabledButton, setDisableButton] = useState(false);

    const [loading, setLoading] = useState(false);

    const onRegister = async () => {
        try {
            setLoading(true);
            const response = await axios.post("/api/users/register", user);
            console.log(response.data);
            router.push("/login");
            // toast.success(response.message);
        } catch (error: any) {
            setLoading(false);
            console.log(error.response.data.error);
            toast.error(error.response.data.error);
        }
    };

    useEffect(() => {
        if (
            user.username.length > 0 &&
            user.email.length > 0 &&
            user.password.length > 0
        ) {
            setDisableButton(false);
        } else {
            setDisableButton(true);
        }
    }, [user]);

    return (
        <>
            <Toaster />
            {loading ? (
                <div className="flex justify-center items-center min-h-screen">
                    <div className="w-16 h-16 border-4 border-blue-500 border-solid rounded-full animate-spin border-t-transparent border-b-transparent"></div>
                </div>
            ) : (
                <>
                    <div className="flex justify-center items-center min-h-screen bg-gray-100">
                        <form className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
                            <h2 className="text-2xl font-bold text-center mb-6">Register</h2>

                            <div className="mb-4">
                                <label
                                    htmlFor="username"
                                    className="block text-gray-700 text-sm font-bold mb-2"
                                >
                                    Username
                                </label>
                                <input
                                    type="text"
                                    id="username"
                                    value={user.username}
                                    onChange={(e) =>
                                        setUser({ ...user, username: e.target.value })
                                    }
                                    className="w-full px-3 py-2 border rounded-md text-gray-700 focus:outline-none focus:border-blue-500"
                                    placeholder="Enter your username"
                                    required
                                />
                            </div>

                            <div className="mb-4">
                                <label
                                    htmlFor="email"
                                    className="block text-gray-700 text-sm font-bold mb-2"
                                >
                                    Email
                                </label>
                                <input
                                    type="email"
                                    id="email"
                                    value={user.email}
                                    onChange={(e) => setUser({ ...user, email: e.target.value })}
                                    className="w-full px-3 py-2 border rounded-md text-gray-700 focus:outline-none focus:border-blue-500"
                                    placeholder="Enter your email"
                                    required
                                />
                            </div>

                            <div className="mb-6">
                                <label
                                    htmlFor="password"
                                    className="block text-gray-700 text-sm font-bold mb-2"
                                >
                                    Password
                                </label>
                                <input
                                    type="password"
                                    id="password"
                                    value={user.password}
                                    onChange={(e) =>
                                        setUser({ ...user, password: e.target.value })
                                    }
                                    className="w-full px-3 py-2 border rounded-md text-gray-700 focus:outline-none focus:border-blue-500"
                                    placeholder="Enter your password"
                                    required
                                />
                            </div>

                            <button
                                onClick={onRegister}
                                className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:bg-blue-700"
                                disabled={disabledButton}
                            >
                                Register
                            </button>

                            <div className="mt-4 text-center">
                                <span className="text-gray-600">
                                    Already have an account?{" "}
                                    <Link href="/login" className="text-blue-500 hover:underline">
                                        Login
                                    </Link>
                                </span>
                            </div>
                        </form>
                    </div>
                </>
            )}
        </>
    );
}

 Verify User:

To Verify User:

In scr/app folder create a new folder /verifyemail

In /verifyemail folder create a new file page.tsx

In page.tsx file:

"use client";

import axios from "axios";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import toast from "react-hot-toast";

export default function page() {
    const [token, setToken] = useState("");
    const [verified, setVerified] = useState(false);
    const [error, setError] = useState(false);

    const verifyUserEmail = async () => {
        try {
            await axios.post("/api/users/verifyemail", { token });
            setVerified(true);
        } catch (error: any) {
            setError(true);
            console.log(error.response.data.error);
            toast.error(error.response.data.error);
        }
    };

    useEffect(() => {
        setError(false);
        const urlToken = window.location.search.split("=")[1];
        setToken(urlToken || "");
    }, []);

    useEffect(() => {
        setError(false);
        if (token.length > 0) {
            verifyUserEmail();
        }
    }, [token]);

    console.log(token);

    return (
        <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
            <div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
                <h2 className="text-2xl font-bold text-gray-800 mb-4 text-center">
                    Verify Email
                </h2>
                <div className="my-5">Token: {token ? `${token}` : "No Token"}</div>
                {verified && (
                    <div>
                        <h2>Verified</h2>
                        <Link href="/login">Login</Link>
                    </div>
                )}
                {error && (
                    <div>
                        <h2>Error</h2>
                    </div>
                )}
            </div>
        </div>
    );
}

Login User:

To Login User:

In scr/app folder create a new folder /login

In /login folder create a new file page.tsx

In page.tsx file:

"use client";

import axios from "axios";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { toast, Toaster } from "react-hot-toast";

export default function page() {
    const router = useRouter();

    const [user, setUser] = useState({
        email: "",
        password: "",
    });

    const [disabledButton, setDisableButton] = useState(false);

    const [loading, setLoading] = useState(false);

    const onLogin = async () => {
        try {
            setLoading(true);
            const response = await axios.post("/api/users/login", user);
            console.log(response.data);
            toast.success(response.data.message);
            router.push("/my-profile");
        } catch (error: any) {
            setLoading(false);
            console.log(error.response.data);
            toast.error(error.response.data.error);
        }
    };

    useEffect(() => {
        if (user.email.length > 0 && user.password.length > 0) {
            setDisableButton(false);
        } else {
            setDisableButton(true);
        }
    }, [user]);

    return (
        <>
            <Toaster />
            {loading ? (
                <>
                    <div className="flex justify-center items-center min-h-screen">
                        <div className="w-16 h-16 border-4 border-blue-500 border-solid rounded-full animate-spin border-t-transparent border-b-transparent"></div>
                    </div>
                </>
            ) : (
                <>
                    <div className="flex justify-center items-center min-h-screen bg-gray-100">
                        <form className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
                            <h2 className="text-2xl font-bold text-center mb-6">Login</h2>

                            <div className="mb-4">
                                <label
                                    htmlFor="email"
                                    className="block text-gray-700 text-sm font-bold mb-2"
                                >
                                    Email
                                </label>
                                <input
                                    type="email"
                                    id="email"
                                    value={user.email}
                                    onChange={(e) => setUser({ ...user, email: e.target.value })}
                                    className="w-full px-3 py-2 border rounded-md text-gray-700 focus:outline-none focus:border-blue-500"
                                    placeholder="Enter your email"
                                    required
                                />
                            </div>

                            <div className="mb-6">
                                <label
                                    htmlFor="password"
                                    className="block text-gray-700 text-sm font-bold mb-2"
                                >
                                    Password
                                </label>
                                <input
                                    type="password"
                                    id="password"
                                    value={user.password}
                                    onChange={(e) =>
                                        setUser({ ...user, password: e.target.value })
                                    }
                                    className="w-full px-3 py-2 border rounded-md text-gray-700 focus:outline-none focus:border-blue-500"
                                    placeholder="Enter your password"
                                    required
                                />
                            </div>

                            <button
                                onClick={onLogin}
                                className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:bg-blue-700"
                                disabled={disabledButton}
                            >
                                Login
                            </button>

                            <div className="mt-4 text-center">
                                <span className="text-gray-600">
                                    Don't have an account?{" "}
                                    <Link
                                        href="/register"
                                        className="text-blue-500 hover:underline"
                                    >
                                        Register
                                    </Link>
                                </span>
                            </div>
                        </form>
                    </div>
                </>
            )}
        </>
    );
}

User Profile:

To User Profile:

In scr/app folder create a new folder /my-profile

In /my-profile folder create a new file page.tsx

In page.tsx file:

"use client";

import React, { useEffect, useState } from "react";
import axios from "axios";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";

export default function page() {
    const router = useRouter();
    const [data, setData] = useState("");

    const getUserDetails = async () => {
        try {
            const response = await axios.get("/api/users/my-profile");
            setData(response.data.data);
            toast.success(response.data.message);
            router.push("/my-profile");
        } catch (error: any) {
            toast.error(error.response.data.error);
        }
    };

    useEffect(() => {
        getUserDetails();
    }, []);

    const logout = async () => {
        try {
            await axios.get("/api/users/logout");
            toast.success("Logged Out");
            router.push("/login");
        } catch (error: any) {
            toast.error(error.response.data.error);
        }
    };

    return (
        <>
            {data === "" ? (
                <>
                    <h1 className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
                        No Data Found
                    </h1>
                </>
            ) : (
                <>
                    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
                        <div className="bg-white shadow-lg rounded-lg p-6 w-80">
                            <div className="flex flex-col items-center">
                                <img
                                    className="w-24 h-24 rounded-full object-cover mb-4"
                                    src="https://via.placeholder.com/150"
                                    alt="Profile"
                                />
                                <h2 className="text-xl font-semibold mb-2">{data.username}</h2>
                                <p className="text-gray-600 mb-4">Full Stack Developer</p>
                            </div>
                            <div className="text-center">
                                <p className="text-gray-800 mb-2">Email: {data.email}</p>
                            </div>
                            <button
                                onClick={logout}
                                className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
                            >
                                Logout
                            </button>
                        </div>
                    </div>
                </>
            )}
        </>
    );
}

<--- Frontend Code End Here --->

Protect Routes:

To Protect Routes:

In scr/ folder create a new file middleware.tsx

In middleware.tsx file:

 import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
    const path = request.nextUrl.pathname;

    const isPublicPath =
        path === "/login" || path === "/register" || path === "/verifyemail";

    const token = request.cookies.get("token")?.value || "";

    if (isPublicPath && token) {
        return NextResponse.redirect(new URL("/", request.url));
    }
    if (!isPublicPath && !token) {
        return NextResponse.redirect(new URL("/login", request.url));
    }
}

export const config = {
    matcher: ["/", "/login", "/register", "/verifyemail", "/my-profile"],
};


Comments

Popular posts from this blog

Create a Connection in Node js and make an API

React JS | Store form data in localStorage

React Redux Toolkit