Aunthentication with Next Auth and Prisma
Lets start with Prisma
Install prisma and prisma client
>_ Terminal
npm install prisma @prisma/client
Generate a prisma schema
>_ Terminal
npx prisma init --datasource-provider mongodb
After the Installation,
Add your Mongodb Database URL
DATABASE_URL =
"mongodb+srv://test:test@cluster0.ns1yp.mongodb.net/myFirstDatabase";
Create a prisma Schema
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @db.ObjectId
type String
provider String
providerAccountId String
refresh_token String? @db.String
access_token String? @db.String
expires_at Int?
token_type String?
scope String?
id_token String? @db.String
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(auto()) @map("_id") @db.ObjectId
sessionToken String @unique
userId String @db.ObjectId
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
enum UserRole {
USER
ADMIN
SERVICE_PROVIDER
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String?
email String? @unique
emailVerified DateTime?
image String?
role UserRole @default(USER)
password String
accounts Account[]
sessions Session[]
isVerfied Boolean @default(false)
token Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
slug String @unique
imageUrl String?
demoLink String?
description String?
products Product[]
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
}
model Product {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
slug String @unique
price Float?
qty Int?
imageUrl String?
productImages String[]
features String[]
description String?
categoryId String @db.ObjectId
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
}
Create a prisma Client Global Instance
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined;
}
export const prismaClient = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalThis.prisma = prismaClient;
Registration of Users
Install Dependencies necessary for Registation
npm install resend react-email @react-email/components -E
npm i bcrypt @types/bcrypt
We will Use Credentials for Authentication
here is the Registration Form
Registration Form
"use client";
import { type RegisterInputProps } from "@/types/types";
import Link from "next/link";
import { useForm } from "react-hook-form";
import TextInput from "../FormInputs/TextInput";
import SubmitButton from "../FormInputs/SubmitButton";
import { useState } from "react";
import { createUser } from "@/actions/users";
import { UserRole } from "@prisma/client";
import toast from "react-hot-toast";
export default function RegisterForm({ role = "USER" }: { role?: UserRole }) {
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<RegisterInputProps>();
async function onSubmit(data: RegisterInputProps) {
// console.log(data);
setIsLoading(true);
data.role = role;
try {
const user = await createUser(data);
if (user && user.status === 200) {
console.log("User Created successfully");
reset();
setIsLoading(false);
toast.success("User Created successfully");
console.log(user.data);
} else {
console.log(user.error);
}
} catch (error) {
console.log(error);
}
}
return (
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<img
className="mx-auto h-10 w-auto"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
alt="Your Company"
/>
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Create new account
</h2>
</div>
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<TextInput
label="Full Name"
register={register}
name="fullName"
errors={errors}
/>
<TextInput
label="Email Address"
register={register}
name="email"
type="email"
errors={errors}
/>
<TextInput
label="Phone Number"
register={register}
name="phone"
type="tel"
errors={errors}
/>
<TextInput
label="Password"
register={register}
name="password"
type="password"
errors={errors}
/>
<div>
<SubmitButton
title="Create Account"
isLoading={isLoading}
loadingTitle="Creating please wait..."
/>
</div>
</form>
<p className="mt-10 text-center text-sm text-gray-500">
Already have Account?{" "}
<Link
href="/login"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
>
Login
</Link>
</p>
</div>
</div>
);
}
Registration Server Actions
"use server";
import EmailTemplate from "@/components/Emails/email-template";
import { prismaClient } from "@/lib/db";
import { RegisterInputProps } from "@/types/types";
import bcrypt from "bcrypt";
import { Resend } from "resend";
export async function createUser(formData: RegisterInputProps) {
const resend = new Resend(process.env.RESEND_API_KEY);
const { fullName, email, role, phone, password } = formData;
try {
const existingUser = await prismaClient.user.findUnique({
where: {
email,
},
});
if (existingUser) {
return {
data: null,
error: `User with this email ( ${email}) already exists in the Database`,
status: 409,
};
}
// Encrypt the Password =>bcrypt
const hashedPassword = await bcrypt.hash(password, 10);
//Generate Token
const generateToken = () => {
const min = 100000; // Minimum 6-figure number
const max = 999999; // Maximum 6-figure number
return Math.floor(Math.random() * (max - min + 1)) + min;
};
const userToken = generateToken();
const newUser = await prismaClient.user.create({
data: {
name: fullName,
email,
phone,
password: hashedPassword,
role,
token: userToken,
},
});
//Send an Email with the Token on the link as a search param
const token = newUser.token;
const userId = newUser.id;
const firstName = newUser.name.split(" ")[0];
const linkText = "Verify your Account ";
const message =
"Thank you for registering with Gecko. To complete your registration and verify your email address, please enter the following 6-digit verification code on our website :";
const sendMail = await resend.emails.send({
from: "Medical App <info@jazzafricaadventures.com>",
to: email,
subject: "Verify Your Email Address",
react: EmailTemplate({ firstName, token, linkText, message }),
});
console.log(token);
console.log(sendMail);
console.log(newUser);
return {
data: newUser,
error: null,
status: 200,
};
} catch (error) {
console.log(error);
return {
error: "Something went wrong",
};
}
}
React Email Template
import * as React from "react";
import {
Body,
Button,
Container,
Head,
Html,
Img,
Link,
Preview,
Section,
Text,
} from "@react-email/components";
interface EmailTemplateProps {
name?: string;
token: number;
linkText: string;
message: string;
}
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
export const EmailTemplate = ({
name = "",
token,
linkText,
message,
}: EmailTemplateProps) => (
<Html>
<Head />
<Preview>{linkText}</Preview>
<Body style={main}>
<Container style={container}>
<Img
src={`${baseUrl}/static/logo.png`}
width="32"
height="32"
alt="Claridy"
/>
<Text style={title}>
<strong>@{name}</strong>, thank you for Joining Us
</Text>
<Section style={section}>
<Text style={text}>
Hey <strong>{name}</strong>!
</Text>
<Text style={text}>{message}</Text>
<Button style={button}>{token}</Button>
<Text style={text}>
If you have any questions, feel free to reach out.
</Text>
</Section>
<Text style={links}>
<Link style={link}>Your security audit log</Link> ・{" "}
<Link style={link}>Contact support</Link>
</Text>
<Text style={footer}>
GitHub, Inc. ・88 Colin P Kelly Jr Street ・San Francisco, CA 94107
</Text>
</Container>
</Body>
</Html>
);
export default EmailTemplate;
const main = {
backgroundColor: "#ffffff",
color: "#24292e",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
};
const container = {
width: "480px",
margin: "0 auto",
padding: "20px 0 48px",
};
const title = {
fontSize: "24px",
lineHeight: 1.25,
};
const section = {
padding: "24px",
border: "solid 1px #dedede",
borderRadius: "5px",
textAlign: "center" as const,
};
const text = {
margin: "0 0 10px 0",
textAlign: "left" as const,
};
const button = {
fontSize: "24px",
backgroundColor: "#28a745",
color: "#fff",
lineHeight: 1.5,
borderRadius: "0.5em",
padding: "0.75em 1.5em",
};
const links = {
textAlign: "center" as const,
};
const link = {
color: "#0366d6",
fontSize: "12px",
};
const footer = {
color: "#6a737d",
fontSize: "12px",
textAlign: "center" as const,
marginTop: "60px",
};
Create Verify Account Dynamic Page
Lets Create a page in front called /verify-account/[id]/page.tsx
and add the following Code
import { getUserById } from "@/actions/users";
import VerifyTokenForm from "@/components/VerifyTokenForm";
export default async function VerifyAccount({
params: { id },
}: {
params: { id: string };
}) {
//Get a User
const user = await getUserById(id);
const userToken = user?.token;
return (
<section className="bg-gray-50 dark:bg-gray-900">
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div className="w-full bg-white rounded-lg shadow-2xl dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white text-center">
Verify Account
</h1>
<VerifyTokenForm userToken={userToken} id={id} />
</div>
</div>
</div>
</section>
);
}
Create Verify TokenForm Component
The VerifyToken Component requires you to Install the following dependencies
>_ Terminal
npm i zod @hookform/resolvers react-icons
npx shadcn-ui@latest add form input-otp
Create the Component
Add the following Code
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { HiInformationCircle } from "react-icons/hi";
import { Alert } from "flowbite-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { Loader } from "lucide-react";
import { updateUserById } from "@/actions/users";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
const FormSchema = z.object({
token: z.string().min(6, {
message: "Your one-time password must be 6 characters.",
}),
});
export default function VerifyTokenForm({
userToken,
id,
}: {
userToken: any;
id: string;
}) {
const [loading, setLoading] = useState(false);
const [showNotification, setShowNotification] = useState(false);
const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
token: "",
},
});
async function onSubmit(data: z.infer<typeof FormSchema>) {
setLoading(true);
const userInputToken = parseInt(data.token);
if (userInputToken === userToken) {
setShowNotification(false);
//Update User
try {
await updateUserById(id);
setLoading(false);
// reset();
toast.success("Account Verified");
router.push("/login");
} catch (error) {
setLoading(false);
console.log(error);
}
} else {
setShowNotification(true);
setLoading(false);
}
console.log(data);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
{showNotification && (
<Alert color="failure" icon={HiInformationCircle}>
<span className="font-medium">Wrong Token!</span> Please Check the
token and Enter again
</Alert>
)}
<FormField
control={form.control}
name="token"
render={({ field }) => (
<FormItem>
<FormLabel>Enter Token Here</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Please enter the 6-figure pass code sent to your email.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
Next Auth and Login
Install Next Auth
npm install next-auth @auth/prisma-adapter
Create the catch-all route
app / api / auth / [...nextauth] / route.ts;
and add this code
import { authOptions } from "@/config/auth";
import NextAuth from "next-auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Create the AuthOptions
import { AuthOptions, NextAuthOptions } from "next-auth";
// import { PrismaAdapter } from "@auth/prisma-adapter";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prismaClient } from "@/prisma/db";
import GoogleProvider from "next-auth/providers/google";
import EmailProvider from "next-auth/providers/email";
import type { Adapter } from "next-auth/adapters";
import CredentialsProvider from "next-auth/providers/credentials";
import { compare } from "bcrypt";
// more providers at https://next-auth.js.org/providers
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prismaClient) as Adapter,
secret: process.env.NEXTAUTH_SECRET,
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
},
providers: [
EmailProvider({
server: process.env.GMAIL_EMAIL_SERVER || "", // any SMTP server will work
from: process.env.EMAIL_FROM || "",
// maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
}),
GoogleProvider({
//Checking if the role exista and if not add USER Bydefault
// profile(profile) {
// return { role: profile.role ?? "USER", ... }
// },
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email", placeholder: "jb@gmail.com" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
try {
console.log(
"Authorize function called with credentials:",
credentials
);
// Check if user credentials are Correct
if (!credentials?.email || !credentials?.password) {
throw { error: "No Inputs Found", status: 401 };
}
console.log("Pass 1 checked ");
//Check if user exists
const existingUser = await prismaClient.user.findUnique({
where: { email: credentials.email },
});
if (!existingUser) {
console.log("No user found");
throw { error: "No user found", status: 401 };
}
console.log("Pass 2 Checked");
console.log(existingUser);
let passwordMatch: boolean = false;
//Check if Password is correct
if (existingUser && existingUser.password) {
// if user exists and password exists
passwordMatch = await compare(
credentials.password,
existingUser.password
);
}
if (!passwordMatch) {
console.log("Password incorrect");
throw { error: "Password Incorrect", status: 401 };
}
console.log("Pass 3 Checked");
const user = {
id: existingUser.id,
name: existingUser.name,
email: existingUser.email,
role: existingUser.role,
};
//
console.log("User Compiled");
console.log(user);
return user;
} catch (error) {
console.log("aLL Failed");
console.log(error);
throw { error: "Something went wrong", status: 401 };
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
const dbUser = await prismaClient.user.findFirst({
where: { email: token?.email ?? "" },
});
if (!dbUser) {
token.id = user!.id;
return token;
}
return {
id: dbUser.id,
name: dbUser.name,
email: dbUser.email,
role: dbUser.role,
picture: dbUser.image,
};
},
session({ session, token }) {
if (token && session.user) {
session.user.id = token.id;
session.user.name = token.name;
session.user.email = token.email;
session.user.image = token.picture;
session.user.role = token.role;
}
return session;
},
},
};
Update the Next AUTH Types for token and session
next-auth.d.ts
import NextAuth from "next-auth";
import { UserRole } from "@prisma/client";
import type { User } from "next-auth";
import "next-auth/jwt";
type UserId = string;
declare module "next-auth/jwt" {
interface JWT {
id: UserId;
role: UserRole;
}
}
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user: User & {
id: UserId;
role: UserRole;
};
}
}
Create the Login Form
"use client";
import Link from "next/link";
import TextInput from "../FormInputs/TextInput";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { LoginInputProps } from "@/types/types";
import SubmitButton from "../FormInputs/SubmitButton";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react";
import { Alert } from "flowbite-react";
import { HiInformationCircle } from "react-icons/hi";
export default function LoginForm() {
const [isLoading, setIsLoading] = useState(false);
const [showNotification, setShowNotification] = useState(false);
const router = useRouter();
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<LoginInputProps>();
async function onSubmit(data: LoginInputProps) {
try {
setIsLoading(true);
console.log("Attempting to sign in with credentials:", data);
const loginData = await signIn("credentials", {
...data,
redirect: false,
});
console.log("SignIn response:", loginData);
if (loginData?.error) {
setIsLoading(false);
toast.error("Sign-in error: Check your credentials");
setShowNotification(true);
} else {
// Sign-in was successful
setShowNotification(false);
reset();
setIsLoading(false);
toast.success("Login Successful");
router.push("/dashboard");
}
} catch (error) {
setIsLoading(false);
console.error("Network Error:", error);
toast.error("Its seems something is wrong with your Network");
}
}
return (
<div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<img
className="mx-auto h-10 w-auto"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
alt="Your Company"
/>
<h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Sign in to your account
</h2>
</div>
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
{showNotification && (
<Alert color="failure" icon={HiInformationCircle}>
<span className="font-medium">Sign-in error!</span> Please Check
your credentials
</Alert>
)}
<TextInput
label="Email Address"
register={register}
name="email"
type="email"
errors={errors}
/>
<div>
<div className="flex items-center justify-between">
<label
htmlFor="password"
className="block text-sm font-medium leading-6 text-gray-900"
>
Password
</label>
<div className="text-sm">
<a
href="#"
className="font-semibold text-indigo-600 hover:text-indigo-500"
>
Forgot password?
</a>
</div>
</div>
<div className="mt-2">
<input
{...register("password", { required: true })}
id="password"
name="password"
type="password"
autoComplete="current-password"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
{errors["password"] && (
<span className="text-red-600 text-sm">
Password is required
</span>
)}
</div>
</div>
<div>
<SubmitButton
title="Login"
isLoading={isLoading}
loadingTitle="Logging you in please wait..."
/>
</div>
</form>
<p className="mt-10 text-center text-sm text-gray-500">
Don't have Account?{" "}
<Link
href="/register"
className="font-semibold leading-6 text-indigo-600 hover:text-indigo-500"
>
Register
</Link>
</p>
</div>
</div>
);
}
Add Middleware to Protect the Dashboard Page
middleware.ts
import { withAuth } from "next-auth/middleware";
export default withAuth({
// Matches the pages config in `[...nextauth]`
pages: {
signIn: "/login",
// error: '/error',
},
});
export const config = {
matcher: ["/dashboard/:path*"],
};
Your .env
DATABASE_URL=""
# NextAuth Configuration
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="3cc9d3166430d0605de2ede088934d4e72085ed6"
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
# ANALYZE=true
# RESEND_API_KEY=""
RESEND_API_KEY=""
UPLOADTHING_SECRET=
UPLOADTHING_APP_ID=