article

Thursday, July 4, 2024

Next.js 14 NextAuth.js Authentication Login Register profile MongoDB | Tailwindcss

Next.js 14 NextAuth.js Authentication Login Register profile MongoDB | Tailwindcss

Install nextjs npx create-next-app@latest https://nextjs.org/docs/getting-started/installation

Install the following

NextAuth.js
npm install next-auth
https://www.npmjs.com/package/next-auth/v/4.23.1
NextAuth.js is a complete open source authentication solution for Next.js applications.

Mongoose
npm install mongoose
https://www.npmjs.com/package/mongoose

bcrypt.js
npm install bcryptjs
https://www.npmjs.com/package/bcryptjs

app\page.js
//app\page.js
import Image from "next/image";
import Link from "next/link";

export default async function Home() {
  return (
    <>
    <nav id="header" className="fixed w-full z-30 top-0 text-white">
      <div className="w-full container mx-auto flex flex-wrap items-center justify-between mt-0 py-2">
        <div className="pl-4 flex items-center">
          <span className="toggleColour text-white no-underline hover:no-underline font-bold text-2xl lg:text-4xl">
            Cairocoders
          </span>
        </div>
        <div className="w-full flex-grow lg:flex lg:items-center lg:w-auto hidden mt-2 lg:mt-0 bg-white lg:bg-transparent text-black p-4 lg:p-0 z-20" id="nav-content">
          <ul className="list-reset lg:flex justify-end flex-1 items-center">
            <li className="mr-3">
              <Link className="inline-block py-2 px-4 text-black font-bold no-underline" href={"/"}>
                Home
              </Link>
            </li>
            <li className="mr-3">
              <Link className="inline-block text-black no-underline hover:text-gray-800 hover:text-underline py-2 px-4" href={"/"}>
                About
              </Link>
            </li>
          </ul>
            <Link className="mx-auto lg:mx-0 hover:underline bg-white text-gray-800 font-bold rounded-full mt-4 lg:mt-0 py-4 px-8 shadow opacity-75 focus:outline-none focus:shadow-outline transform transition hover:scale-105 duration-300 ease-in-out" 
              href={"/login"}>
                Login
            </Link>
        </div>
      </div>
      <hr className="border-b border-gray-100 opacity-25 my-0 py-0" />
    </nav>

 <div>
  {/*Hero*/}
  <div className="pt-24">
    <div className="container px-3 mx-auto flex flex-wrap flex-col md:flex-row items-center">
      {/*Left Col*/}
      <div className="flex flex-col w-full md:w-2/5 justify-center items-start text-center md:text-left">
        <p className="uppercase tracking-loose w-full">What business are you?</p>
        <h1 className="my-4 text-5xl font-bold leading-tight">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam
        </h1>
        <p className="leading-normal text-2xl mb-8">
          Snunc commodo posuere et sit amet ligula
        </p>
        <button className="mx-auto lg:mx-0 hover:underline bg-white text-gray-800 font-bold rounded-full my-6 py-4 px-8 shadow-lg focus:outline-none focus:shadow-outline transform transition hover:scale-105 duration-300 ease-in-out">
          Subscribe
        </button>
      </div>
      {/*Right Col*/}
      <div className="w-full md:w-3/5 py-6 text-center">
        <Image
              src="/hero.png"
              alt="hero"
              className="w-full md:w-4/5 z-50"
              width={613}
              height={529}
              priority
        />
      </div>
    </div>
  </div>
  <div className="relative -mt-12 lg:-mt-24">
    <svg viewBox="0 0 1428 174" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink">
      <g stroke="none" strokeWidth={1} fill="none" fillRule="evenodd">
        <g transform="translate(-2.000000, 44.000000)" fill="#FFFFFF" fillRule="nonzero">
          <path d="M0,0 C90.7283404,0.927527913 147.912752,27.187927 291.910178,59.9119003 C387.908462,81.7278826 543.605069,89.334785 759,82.7326078 C469.336065,156.254352 216.336065,153.6679 0,74.9732496" opacity="0.100000001" />
          <path d="M100,104.708498 C277.413333,72.2345949 426.147877,52.5246657 546.203633,45.5787101 C666.259389,38.6327546 810.524845,41.7979068 979,55.0741668 C931.069965,56.122511 810.303266,74.8455141 616.699903,111.243176 C423.096539,147.640838 250.863238,145.462612 100,104.708498 Z" opacity="0.100000001" />
          <path d="M1046,51.6521276 C1130.83045,29.328812 1279.08318,17.607883 1439,40.1656806 L1439,120 C1271.17211,77.9435312 1140.17211,55.1609071 1046,51.6521276 Z" id="Path-4" opacity="0.200000003" />
        </g>
        <g transform="translate(-4.000000, 76.000000)" fill="#FFFFFF" fillRule="nonzero">
          <path d="M0.457,34.035 C57.086,53.198 98.208,65.809 123.822,71.865 C181.454,85.495 234.295,90.29 272.033,93.459 C311.355,96.759 396.635,95.801 461.025,91.663 C486.76,90.01 518.727,86.372 556.926,80.752 C595.747,74.596 622.372,70.008 636.799,66.991 C663.913,61.324 712.501,49.503 727.605,46.128 C780.47,34.317 818.839,22.532 856.324,15.904 C922.689,4.169 955.676,2.522 1011.185,0.432 C1060.705,1.477 1097.39,3.129 1121.236,5.387 C1161.703,9.219 1208.621,17.821 1235.4,22.304 C1285.855,30.748 1354.351,47.432 1440.886,72.354 L1441.191,104.352 L1.121,104.031 L0.457,34.035 Z" />
        </g>
      </g>
    </svg>
  </div>
  <section className="bg-white border-b py-8">
    <div className="container max-w-5xl mx-auto m-8">
      <h2 className="w-full my-2 text-5xl font-bold leading-tight text-center text-gray-800">
        Title
      </h2>
    </div>
  </section>
</div>

<footer className="bg-white">
  <div className="container mx-auto px-8">
    <div className="w-full flex flex-col md:flex-row py-6">
      <div className="flex-1 mb-6 text-black">
        <span className="text-pink-600 no-underline hover:no-underline font-bold text-2xl lg:text-4xl">
          Cairocoders
        </span>
      </div>
      <div className="flex-1">
        <p className="uppercase text-gray-500 md:mb-6">Links</p>
        <ul className="list-reset mb-6">
          <li className="mt-2 inline-block mr-2 md:block md:mr-0">
            <a href="#" className="no-underline hover:underline text-gray-800 hover:text-pink-500">FAQ</a>
          </li>
          <li className="mt-2 inline-block mr-2 md:block md:mr-0">
            <a href="#" className="no-underline hover:underline text-gray-800 hover:text-pink-500">Help</a>
          </li>
          <li className="mt-2 inline-block mr-2 md:block md:mr-0">
            <a href="#" className="no-underline hover:underline text-gray-800 hover:text-pink-500">Support</a>
          </li>
        </ul>
      </div>
      <div className="flex-1">
        <p className="uppercase text-gray-500 md:mb-6">Social</p>
        <ul className="list-reset mb-6">
          <li className="mt-2 inline-block mr-2 md:block md:mr-0">
            <a href="#" className="no-underline hover:underline text-gray-800 hover:text-pink-500">Facebook</a>
          </li>
          <li className="mt-2 inline-block mr-2 md:block md:mr-0">
            <a href="#" className="no-underline hover:underline text-gray-800 hover:text-pink-500">Linkedin</a>
          </li>
          <li className="mt-2 inline-block mr-2 md:block md:mr-0">
            <a href="#" className="no-underline hover:underline text-gray-800 hover:text-pink-500">Twitter</a>
          </li>
        </ul>
      </div>
    </div>
  </div>
  </footer>

    </>
  );
}
app\layout.js
//app\layout.js
import { AuthProvider } from "./Providers";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className="leading-normal tracking-normal text-white gradient">
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}
app\Providers.js
//app\Providers.js
"use client";

import { SessionProvider } from "next-auth/react";

export const AuthProvider = ({ children }) => {
  return <SessionProvider>{children}</SessionProvider>;
};
app\login\page.jsx
//app\login\page.jsx
import LoginForm from "@/components/LoginForm";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "../api/auth/[...nextauth]/route";

export default async function Home() {
  const session = await getServerSession(authOptions);

  if (session) redirect("/dashboard");

  return (
    <main>
      <LoginForm /> 
    </main>
  );
}
app\register\page.jsx
//app\register\page.jsx
import RegisterForm from "@/components/RegisterForm";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "../api/auth/[...nextauth]/route";

export default async function Register() {
  const session = await getServerSession(authOptions);

  if (session) redirect("/dashboard");

  return <RegisterForm />;
}
app\dashboard\page.jsx
//app\dashboard\page.jsx
import UserInfo from "@/components/UserInfo";
import Link from "next/link";

export default function Dashboard() {
  return (
    <div className="container mx-auto px-4 place-items-center h-screen p-5">
      <UserInfo />
    </div>    
  );
}
app\api\auth\[...nextauth]\route.js
//app\api\auth\[...nextauth]\route.js
import { connectMongoDB } from "@/lib/mongodb";
import User from "@/models/user";
import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: "credentials",
      credentials: {},

      async authorize(credentials) {
        const { email, password } = credentials;

        try {
          await connectMongoDB();
          const user = await User.findOne({ email });

          if (!user) {
            return null;
          }

          const passwordsMatch = await bcrypt.compare(password, user.password);

          if (!passwordsMatch) {
            return null;
          }

          return user;
        } catch (error) {
          console.log("Error: ", error);
        }
      },
    }),
  ],
  session: {
    strategy: "jwt",
  },
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/",
  },
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
app\api\register\route.js
//app\api\register\route.js
import { connectMongoDB } from "@/lib/mongodb";
import User from "@/models/user";
import { NextResponse } from "next/server";
import bcrypt from "bcryptjs";

export async function POST(req) {
  try {
    const { name, email, password } = await req.json();
    const hashedPassword = await bcrypt.hash(password, 10);
    await connectMongoDB();
    await User.create({ name, email, password: hashedPassword });

    return NextResponse.json({ message: "User registered." }, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { message: "An error occurred while registering the user." },
      { status: 500 }
    );
  }
}
app\api\userExists\route.js
//app\api\userExists\route.js
import { connectMongoDB } from "@/lib/mongodb";
import User from "@/models/user";
import { NextResponse } from "next/server";

export async function POST(req) {
  try {
    await connectMongoDB();
    const { email } = await req.json();
    const user = await User.findOne({ email }).select("_id");
    console.log("user: ", user);
    return NextResponse.json({ user });
  } catch (error) {
    console.log(error);
  }
}
components\LoginForm.jsx
//components\LoginForm.jsx
"use client";

import Link from "next/link";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";

export default function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const res = await signIn("credentials", {
        email,
        password,
        redirect: false,
      });

      if (res.error) {
        setError("Invalid Credentials");
        return;
      }

      router.replace("dashboard");
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <div className="grid place-items-center h-screen">
      <div className="shadow-lg p-5 rounded-lg border-t-4 border-green-400 lg:w-5/12">
        <h1 className="text-xl font-bold my-4">Login</h1>

        <form onSubmit={handleSubmit} className="flex flex-col gap-3">
          <div class="flex items-center text-lg mb-6 md:mb-8">
            <svg class="absolute ml-3" width="24" viewBox="0 0 24 24">
              <path d="M20.822 18.096c-3.439-.794-6.64-1.49-5.09-4.418 4.72-8.912 1.251-13.678-3.732-13.678-5.082 0-8.464 4.949-3.732 13.678 1.597 2.945-1.725 3.641-5.09 4.418-3.073.71-3.188 2.236-3.178 4.904l.004 1h23.99l.004-.969c.012-2.688-.092-4.222-3.176-4.935z"/>
            </svg>
            <input
              onChange={(e) => setEmail(e.target.value)}
              type="text"
              className="bg-gray-200 rounded pl-12 py-2 md:py-4 focus:outline-none w-full text-black"
              placeholder="Email"
            />
          </div>

          <div class="flex items-center text-lg mb-6 md:mb-8">
            <svg class="absolute ml-3" viewBox="0 0 24 24" width="24">
              <path d="m18.75 9h-.75v-3c0-3.309-2.691-6-6-6s-6 2.691-6 6v3h-.75c-1.24 0-2.25 1.009-2.25 2.25v10.5c0 1.241 1.01 2.25 2.25 2.25h13.5c1.24 0 2.25-1.009 2.25-2.25v-10.5c0-1.241-1.01-2.25-2.25-2.25zm-10.75-3c0-2.206 1.794-4 4-4s4 1.794 4 4v3h-8zm5 10.722v2.278c0 .552-.447 1-1 1s-1-.448-1-1v-2.278c-.595-.347-1-.985-1-1.722 0-1.103.897-2 2-2s2 .897 2 2c0 .737-.405 1.375-1 1.722z"/>
            </svg>
            <input
              onChange={(e) => setPassword(e.target.value)}
              type="password"
              class="bg-gray-200 rounded pl-12 py-2 md:py-4 focus:outline-none w-full text-black"
              placeholder="Password"
            />
          </div>
          <button className="bg-green-600 text-white p-2 md:p-4 font-bold cursor-pointer px-6 py-2 rounded">
            Login
          </button>
          {error && (
            <div className="bg-red-500 text-white w-fit text-sm py-1 px-3 rounded-md mt-2 w-full p-2 md:p-4">
              {error}
            </div>
          )}

          <Link className="text-sm mt-3 text-right" href={"/register"}>
            Don't have an account? <span className="underline">Register</span>
          </Link>
        </form>
      </div>
    </div>
  );
}
components\RegisterForm.jsx
//components\RegisterForm.jsx
"use client";

import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";

export default function RegisterForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!name || !email || !password) {
      setError("All fields are necessary.");
      return;
    }

    try {
      const resUserExists = await fetch("api/userExists", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ email }),
      });

      const { user } = await resUserExists.json();

      if (user) {
        setError("User already exists.");
        return;
      }

      const res = await fetch("api/register", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          name,
          email,
          password,
        }),
      });

      if (res.ok) {
        const form = e.target;
        form.reset();
        router.push("/");
      } else {
        console.log("User registration failed.");
      }
    } catch (error) {
      console.log("Error during registration: ", error);
    }
  };

  return (
    <div className="grid place-items-center h-screen">
      <div className="shadow-lg p-5 rounded-lg border-t-4 border-green-400 lg:w-5/12">
        <h1 className="text-xl font-bold my-4">Register</h1>

        <form onSubmit={handleSubmit} className="flex flex-col gap-3">
          <div class="flex items-center text-lg mb-6 md:mb-8">
            <svg xmlns="http://www.w3.org/2000/svg" class="absolute ml-3" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle>
            </svg>
            <input
              onChange={(e) => setName(e.target.value)}
              type="text"
              className="bg-gray-200 rounded pl-12 py-2 md:py-4 focus:outline-none w-full text-black"
              placeholder="Full Name"
            />
          </div>
          <div class="flex items-center text-lg mb-6 md:mb-8">
            <svg xmlns="http://www.w3.org/2000/svg" class="absolute ml-3" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline>
            </svg>
            <input
              onChange={(e) => setEmail(e.target.value)}
              type="text"
              className="bg-gray-200 rounded pl-12 py-2 md:py-4 focus:outline-none w-full text-black"
              placeholder="Email"
            />
          </div>
          <div class="flex items-center text-lg mb-6 md:mb-8">
            <svg xmlns="http://www.w3.org/2000/svg" class="absolute ml-3" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
              <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
            </svg>
            <input
              onChange={(e) => setPassword(e.target.value)}
              type="password"
              class="bg-gray-200 rounded pl-12 py-2 md:py-4 focus:outline-none w-full text-black"
              placeholder="Password"
            />
          </div>

          <button className="bg-green-600 text-white p-2 md:p-4 font-bold cursor-pointer px-6 py-2 rounded">
            Register
          </button>

          {error && (
            <div className="bg-red-500 text-white w-fit text-sm py-1 px-3 rounded-md mt-2 w-full p-2 md:p-4">
              {error}
            </div>
          )}

          <Link className="text-sm mt-3 text-right" href={"/login"}>
            Already have an account? <span className="underline">Login</span>
          </Link>
        </form>
      </div>
    </div>
  );
}
components\UserInfo.jsx
//components\UserInfo.jsx
"use client";

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

export default function UserInfo() {
  const { data: session } = useSession();

  return (
    <div className="p-5">
        <div>
          Name: <span className="font-bold">{session?.user?.name}</span>
        </div>
        <div>
          Email: <span className="font-bold">{session?.user?.email}</span>
        </div>
        <button
          onClick={() => signOut()}
          className="bg-red-500 text-white font-bold px-6 py-2 mt-3"
        >
          Log Out
        </button>
    </div>
  );
}
lib\mongodb.js
//lib\mongodb.js
import mongoose from "mongoose";

export const connectMongoDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI);
    console.log("Connected to MongoDB");
  } catch (error) {
    console.log("Error connecting to MongoDB: ", error);
  }
};
models\user.js
//models\user.js
import mongoose, { Schema, models } from "mongoose";

const userSchema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);

const User = models.User || mongoose.model("User", userSchema);
export default User;
middleware.js
//middleware.js
export { default } from "next-auth/middleware";

export const config = { matcher: ["/dashboard"] };
.env
//.env
MONGODB_URI=mongodb://127.0.0.1/nextjs14
NEXTAUTH_SECRET=cairocoders
run C:\nextjs>npm run dev

Related Post