├── .eslintrc.json ├── .gitignore ├── README.md ├── actions └── auth │ ├── login.action.ts │ ├── reset.ts │ ├── signup.action.ts │ ├── tokens.ts │ ├── verifyResetToken.ts │ └── verifyToken.ts ├── app ├── (protected) │ └── setting │ │ └── page.tsx ├── (root) │ └── page.tsx ├── api │ └── auth │ │ └── [...nextauth] │ │ └── route.ts ├── auth │ ├── error │ │ └── page.tsx │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── new-verification │ │ └── page.tsx │ ├── register │ │ └── page.tsx │ ├── reset-password │ │ └── page.tsx │ └── reset │ │ └── page.tsx ├── favicon.ico ├── globals.css └── layout.tsx ├── auth.config.ts ├── auth.ts ├── components.json ├── components ├── shared │ └── auth │ │ ├── Auth_providers.tsx │ │ ├── FormError.tsx │ │ ├── FromSuccess.tsx │ │ ├── ResetPasswordCard.tsx │ │ ├── Signin-form.tsx │ │ ├── Signup-form.tsx │ │ ├── VerifyTokenForm.tsx │ │ └── verifyPassToken.tsx └── ui │ ├── button.tsx │ ├── form.tsx │ ├── input.tsx │ └── label.tsx ├── data └── auth │ ├── resettoken.ts │ ├── tokens.ts │ └── user.ts ├── lib ├── email.ts ├── prismaClient.ts └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── routes.ts ├── schema └── index.ts ├── tailwind.config.ts ├── tsconfig.json └── types └── next-auth.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next Auth v5 - Advanced Guide (2024) 2 | 3 | Key Features: 4 | - 🔐 Next-auth v5 (Auth.js) 5 | - 🚀 Next.js 14 with server actions 6 | - 🔑 Credentials Provider 7 | - 🌐 OAuth Provider (Social login with Google & GitHub) 8 | - 🔒 Forgot password functionality 9 | - ✉️ Email verification 10 | - 📱 Two factor verification 11 | - 🔓 Login component (Opens in redirect or modal) 12 | - 📝 Register component 13 | - 🤔 Forgot password component 14 | - ✅ Verification component 15 | - ⚠️ Error component 16 | - 🔘 Login button 17 | - 🚪 Logout button 18 | - 🚧 Role Gate 19 | - 🔍 Exploring next.js middleware 20 | - 📈 Extending & Exploring next-auth session 21 | 22 | ### Install packages 23 | 24 | ```shell 25 | npm i 26 | ``` 27 | 28 | ### Setup .env file 29 | 30 | 31 | ```js 32 | 33 | GITHUB_CLIENT_ID= 34 | GITHUB_CLIENT_SECRET= 35 | AUTH_SECRET= 36 | 37 | 38 | GOOGLE_CLIENT_ID= 39 | GOOGLE_CLIENT_SECRET= 40 | 41 | 42 | DATABASE_URL= 43 | 44 | ``` 45 | 46 | ### Setup Prisma 47 | ```shell 48 | npx prisma generate 49 | npx prisma db push 50 | ``` 51 | 52 | ### Start the app 53 | 54 | ```shell 55 | npm run dev 56 | ``` 57 | -------------------------------------------------------------------------------- /actions/auth/login.action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { signIn } from "@/auth"; 4 | import { getUserByEmail } from "@/data/auth/user"; 5 | import { sentVerificationEmail } from "@/lib/email"; 6 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; 7 | import { loginSchema } from "@/schema"; 8 | import bcrypt from "bcryptjs"; 9 | import { AuthError } from "next-auth"; 10 | import { z } from "zod"; 11 | import { generateVerificationToken } from "../auth/tokens"; 12 | 13 | export const login = async (values: z.infer) => { 14 | const validatedvalues = loginSchema.safeParse(values); 15 | 16 | if (!validatedvalues.success) { 17 | return { error: "invalid fields" }; 18 | } 19 | 20 | // Destructure email and password from validatedvalues 21 | 22 | const { email, password } = validatedvalues.data; 23 | 24 | // we write a logic of checking if the user exists in the database and if not we return an error and if it does but the email is not verified we generate a verification token and return a success message. 25 | 26 | const user = await getUserByEmail(email); 27 | 28 | if (!user || !user.password || !user.email) { 29 | return { error: "User not found" }; 30 | } 31 | 32 | const userPassword = await bcrypt.compare(password, user.password); 33 | 34 | if (!userPassword) { 35 | return { error: "invalid credentials" }; 36 | } 37 | 38 | if (!user.emailVerified) { 39 | const generateToken = await generateVerificationToken(email); 40 | 41 | await sentVerificationEmail(generateToken.email, generateToken.token); 42 | 43 | return { success: "verification email sent !" }; 44 | } 45 | 46 | // we use the signIn function from next-auth to sign in the user with the credentials provider and redirect the user to the DEFAULT_LOGIN_REDIRECT 47 | 48 | try { 49 | await signIn("credentials", { 50 | email, 51 | password, 52 | redirectTo: DEFAULT_LOGIN_REDIRECT, 53 | }); 54 | } catch (error) { 55 | if (error instanceof AuthError) { 56 | switch (error.type) { 57 | case "CredentialsSignin": 58 | return { error: "invalid credentials" }; 59 | 60 | default: 61 | return { error: "An error occurred " }; 62 | } 63 | } 64 | 65 | // we return the error, this is compulsory otherwise the function will not throw an error. 66 | throw error; 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /actions/auth/reset.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getAccountProviderById, getUserByEmail } from "@/data/auth/user"; 4 | import { sentResetPasswordEmail } from "@/lib/email"; 5 | import { resetPasswordSchema } from "@/schema"; 6 | import { z } from "zod"; 7 | import { generateResetPasswordVerificationToken } from "./tokens"; 8 | 9 | export const reset = async (data: z.infer) => { 10 | const validEmail = resetPasswordSchema.safeParse(data); 11 | 12 | if (!validEmail.success) { 13 | return { error: "Invalid email" }; 14 | } 15 | 16 | const { email } = validEmail.data; 17 | 18 | try { 19 | const user = await getUserByEmail(email); 20 | if (!user) { 21 | return { error: "User not found" }; 22 | } 23 | 24 | const AccountProvider = await getAccountProviderById(user.id); 25 | 26 | if (AccountProvider) { 27 | return { error: "Provider account! Login using provider" }; 28 | } 29 | 30 | const passwordtoken = await generateResetPasswordVerificationToken(email); 31 | 32 | console.log(passwordtoken); 33 | 34 | await sentResetPasswordEmail(email, passwordtoken.token); 35 | 36 | return { success: "Reset password email sent !" }; 37 | } catch (error) { 38 | console.log(error); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /actions/auth/signup.action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { sentVerificationEmail } from "@/lib/email"; 3 | import prisma from "@/lib/prismaClient"; 4 | import { signupSchema } from "@/schema"; 5 | import bcrypt from "bcryptjs"; 6 | import { z } from "zod"; 7 | import { generateVerificationToken } from "./tokens"; 8 | export const signup = async (data: z.infer) => { 9 | const validatedData = signupSchema.safeParse(data); 10 | 11 | if (!validatedData.success) { 12 | return { error: "invalid fields" }; 13 | } 14 | 15 | const { email, username, password } = validatedData.data; 16 | 17 | try { 18 | const existingUser = await prisma.user.findFirst({ 19 | where: { 20 | email, 21 | }, 22 | }); 23 | 24 | if (existingUser) { 25 | return { error: "User already exists" }; 26 | } 27 | 28 | const hashedPassword = bcrypt.hashSync(password, 10); 29 | 30 | const newUser = await prisma.user.create({ 31 | data: { 32 | email, 33 | username, 34 | password: hashedPassword, 35 | }, 36 | }); 37 | 38 | if (!newUser) { 39 | return { error: "User not created" }; 40 | } 41 | 42 | const verificationToken = await generateVerificationToken(email); 43 | 44 | await sentVerificationEmail( 45 | verificationToken.email, 46 | verificationToken.token 47 | ); 48 | 49 | return { success: "verification email sent please verify" }; 50 | } catch (error) { 51 | console.log(error); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /actions/auth/tokens.ts: -------------------------------------------------------------------------------- 1 | import { getResetTokenByEmail } from "@/data/auth/resettoken"; 2 | import { getVerificationTokenByEmail } from "@/data/auth/tokens"; 3 | import prisma from "@/lib/prismaClient"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | 6 | export const generateResetPasswordVerificationToken = async (email: string) => { 7 | const token = uuidv4(); 8 | const expires = new Date(new Date().getTime() + 3600 * 1000); // This token will expire in 1 hour 9 | try { 10 | const extistingToken = await getResetTokenByEmail(email); 11 | 12 | if (extistingToken) { 13 | await prisma.passwordResetToken.delete({ 14 | where: { 15 | id: extistingToken.id, 16 | }, 17 | }); 18 | } 19 | 20 | const verificationToken = await prisma.passwordResetToken.create({ 21 | data: { 22 | email, 23 | token, 24 | expires, 25 | }, 26 | }); 27 | 28 | return verificationToken; 29 | } catch (error) { 30 | console.log(error); 31 | } 32 | }; 33 | 34 | export const generateVerificationToken = async (email: string) => { 35 | const token = uuidv4(); 36 | const expires = new Date(new Date().getTime() + 3600 * 1000); // This token will expire in 1 hour 37 | try { 38 | const extistingToken = await getVerificationTokenByEmail(email); 39 | 40 | if (extistingToken) { 41 | await prisma.verificationToken.delete({ 42 | where: { 43 | id: extistingToken.id, 44 | }, 45 | }); 46 | } 47 | 48 | const verificationToken = await prisma.verificationToken.create({ 49 | data: { 50 | email, 51 | token, 52 | expires, 53 | }, 54 | }); 55 | 56 | return verificationToken; 57 | } catch (error) { 58 | console.log(error); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /actions/auth/verifyResetToken.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getResetTokenByToken } from "@/data/auth/resettoken"; 4 | import { getUserByEmail } from "@/data/auth/user"; 5 | import prisma from "@/lib/prismaClient"; 6 | import { passwordschema } from "@/schema"; 7 | import bcrypt from "bcryptjs"; 8 | import { z } from "zod"; 9 | 10 | export const verifyResetPasswordToken = async ( 11 | token: string, 12 | data: z.infer 13 | ) => { 14 | const validpassword = passwordschema.safeParse(data); 15 | 16 | if (!validpassword.success) { 17 | return { error: "Invalid password" }; 18 | } 19 | 20 | const { password } = validpassword.data; 21 | 22 | try { 23 | const existingToken = await getResetTokenByToken(token); 24 | 25 | if (!existingToken) { 26 | return { error: "Token not found" }; 27 | } 28 | 29 | const tokenExpire = new Date(existingToken.expires) < new Date(); 30 | 31 | if (tokenExpire) { 32 | return { error: "Token is expired" }; 33 | } 34 | 35 | const user = await getUserByEmail(existingToken.email); 36 | 37 | if (!user) { 38 | return { error: "email not found" }; 39 | } 40 | 41 | const hashedPassword = await bcrypt.hash(password, 10); 42 | 43 | await prisma.user.update({ 44 | where: { 45 | email: existingToken.email, 46 | }, 47 | data: { 48 | emailVerified: new Date(), 49 | email: existingToken.email, 50 | password: hashedPassword, 51 | }, 52 | }); 53 | 54 | await prisma.passwordResetToken.delete({ 55 | where: { 56 | id: existingToken.id, 57 | }, 58 | }); 59 | 60 | return { success: "password updated " }; 61 | } catch (error) { 62 | console.log(error); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /actions/auth/verifyToken.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getVerificationTokenByToken } from "@/data/auth/tokens"; 4 | import { getUserByEmail } from "@/data/auth/user"; 5 | import prisma from "@/lib/prismaClient"; 6 | 7 | export const verifyToken = async (token: string) => { 8 | try { 9 | const existingToken = await getVerificationTokenByToken(token); 10 | 11 | if (!existingToken) { 12 | return { error: "Token not found" }; 13 | } 14 | 15 | const tokenExpire = new Date(existingToken.expires) < new Date(); 16 | 17 | if (tokenExpire) { 18 | return { error: "Token is expired" }; 19 | } 20 | 21 | const user = await getUserByEmail(existingToken.email); 22 | 23 | if (!user) { 24 | return { error: "email not found" }; 25 | } 26 | 27 | await prisma.user.update({ 28 | where: { 29 | email: existingToken.email, 30 | }, 31 | data: { 32 | emailVerified: new Date(), 33 | email: existingToken.email, 34 | }, 35 | }); 36 | 37 | await prisma.verificationToken.delete({ 38 | where: { 39 | id: existingToken.id, 40 | }, 41 | }); 42 | 43 | return { success: "email verified" }; 44 | } catch (error) { 45 | console.log(error); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /app/(protected)/setting/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth, signOut } from "@/auth"; 2 | import { Button } from "@/components/ui/button"; 3 | 4 | const Setting = async () => { 5 | const session = await auth(); 6 | 7 | const handleSignOut = async () => { 8 | "use server"; 9 | await signOut({ 10 | redirectTo: "/auth/login", 11 | }); 12 | }; 13 | 14 | return ( 15 |
16 | {JSON.stringify(session)} 17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Setting; 28 | -------------------------------------------------------------------------------- /app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import Link from "next/link"; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 | Home 8 | 9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Home; 16 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth"; 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /app/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | const ErrorPage = () => { 2 | return
there was an error during Login
; 3 | }; 4 | 5 | export default ErrorPage; 6 | -------------------------------------------------------------------------------- /app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function authLayout({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode; 5 | }>) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Signin } from "@/components/shared/auth/Signin-form"; 2 | import { Suspense } from "react"; 3 | 4 | const login = () => { 5 | return ( 6 | Loading...}> 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default login; 13 | -------------------------------------------------------------------------------- /app/auth/new-verification/page.tsx: -------------------------------------------------------------------------------- 1 | import VerifyTokenForm from "@/components/shared/auth/VerifyTokenForm"; 2 | import { Suspense } from "react"; 3 | 4 | const VerifyToken = () => { 5 | return ( 6 | Loading...}> 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default VerifyToken; 13 | -------------------------------------------------------------------------------- /app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { Signup } from "@/components/shared/auth/Signup-form"; 2 | 3 | const Register = () => { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default Register; 12 | -------------------------------------------------------------------------------- /app/auth/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | import VerifyPassToken from "@/components/shared/auth/verifyPassToken"; 2 | import { Suspense } from "react"; 3 | 4 | const VerifyToken = () => { 5 | return ( 6 | Loading...}> 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default VerifyToken; 13 | -------------------------------------------------------------------------------- /app/auth/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetPasswordCard } from "@/components/shared/auth/ResetPasswordCard"; 2 | 3 | const ResetPassword = () => { 4 | return ; 5 | }; 6 | 7 | export default ResetPassword; 8 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abdullah-dev0/Authjs-starter-pack/421d6d268a77893c6316d96ba4fe28a97bfe9509/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "NextAuth", 9 | description: "NextAuth Example stater project", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/prismaClient"; 2 | import bcrypt from "bcryptjs"; 3 | import { NextAuthConfig } from "next-auth"; 4 | import Credentials from "next-auth/providers/credentials"; 5 | import GitHub from "next-auth/providers/github"; 6 | import Google from "next-auth/providers/google"; 7 | import { loginSchema } from "./schema"; 8 | 9 | export default { 10 | providers: [ 11 | GitHub({ 12 | clientId: process.env.GITHUB_CLIENT_ID as string, 13 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string, 14 | }), 15 | Google({ 16 | clientId: process.env.GOOGLE_CLIENT_ID as string, 17 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 18 | }), 19 | Credentials({ 20 | credentials: { 21 | email: {}, 22 | password: {}, 23 | }, 24 | authorize: async (credentials) => { 25 | const validateFields = loginSchema.safeParse(credentials); 26 | 27 | if (validateFields.success) { 28 | const { email, password } = validateFields.data; 29 | 30 | const user = await prisma.user.findFirst({ 31 | where: { 32 | email, 33 | }, 34 | }); 35 | 36 | if (!user || !user.password) return null; 37 | 38 | const isPasswordValid = await bcrypt.compare( 39 | password, 40 | user.password 41 | ); 42 | 43 | if (isPasswordValid) return user; 44 | } 45 | 46 | return null; 47 | }, 48 | }), 49 | ], 50 | } satisfies NextAuthConfig; 51 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/prismaClient"; 2 | import { PrismaAdapter } from "@auth/prisma-adapter"; 3 | import NextAuth from "next-auth"; 4 | import authConfig from "./auth.config"; 5 | import { getUserById } from "./data/auth/user"; 6 | 7 | export const { handlers, signIn, signOut, auth } = NextAuth({ 8 | adapter: PrismaAdapter(prisma), 9 | ...authConfig, 10 | session: { 11 | strategy: "jwt", 12 | }, 13 | pages: { 14 | signIn: "/auth/login", 15 | error: "/auth/error", 16 | }, 17 | 18 | events: { 19 | async linkAccount({ user }) { 20 | await prisma.user.update({ 21 | where: { 22 | id: user.id, 23 | }, 24 | data: { 25 | emailVerified: new Date(), 26 | }, 27 | }); 28 | }, 29 | }, 30 | 31 | callbacks: { 32 | async signIn({ user, account }) { 33 | // we have already done this in the login action but we should do it here as well for extra security and here is important. 34 | if (account.provider !== "credentials") return true; 35 | 36 | const existingUser = await getUserById(user.id); 37 | 38 | if (!existingUser.emailVerified) return false; 39 | 40 | return true; 41 | }, 42 | async jwt({ token, user }) { 43 | return token; 44 | }, 45 | async session({ session, token, user }) { 46 | // Add property to session, like an access_token from a provider. you can as many as you want 47 | if (token.sub && session.user) { 48 | session.user.id = token.sub; 49 | } 50 | return session; 51 | }, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/shared/auth/Auth_providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; 5 | import { signIn } from "next-auth/react"; 6 | import { useTransition } from "react"; 7 | 8 | import { FaGithub, FaGoogle } from "react-icons/fa"; 9 | 10 | type AuthProvider = "google" | "github"; 11 | 12 | const Authproviders = () => { 13 | const [isPending, startTransition] = useTransition(); 14 | const handleClick = async (provider: AuthProvider) => { 15 | startTransition(() => { 16 | signIn(provider, { 17 | callbackUrl: DEFAULT_LOGIN_REDIRECT, 18 | }); 19 | }); 20 | }; 21 | 22 | return ( 23 |
24 | 32 | 40 |
41 | ); 42 | }; 43 | 44 | export default Authproviders; 45 | -------------------------------------------------------------------------------- /components/shared/auth/FormError.tsx: -------------------------------------------------------------------------------- 1 | import { TriangleAlert } from "lucide-react"; 2 | 3 | type Props = { 4 | message: string; 5 | }; 6 | 7 | const FormError = ({ message }: Props) => { 8 | if (!message) return null; 9 | return ( 10 |
11 | 12 |

{message}

13 |
14 | ); 15 | }; 16 | 17 | export default FormError; 18 | -------------------------------------------------------------------------------- /components/shared/auth/FromSuccess.tsx: -------------------------------------------------------------------------------- 1 | import { CircleCheckBig } from "lucide-react"; 2 | type Props = { 3 | message: string; 4 | }; 5 | 6 | const FormSuccess = ({ message }: Props) => { 7 | if (!message) return null; 8 | return ( 9 |
10 | 11 |

{message}

12 |
13 | ); 14 | }; 15 | 16 | export default FormSuccess; 17 | -------------------------------------------------------------------------------- /components/shared/auth/ResetPasswordCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { reset } from "@/actions/auth/reset"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Form, 7 | FormControl, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { resetPasswordSchema } from "@/schema"; 15 | import { zodResolver } from "@hookform/resolvers/zod"; 16 | import Link from "next/link"; 17 | import { useState, useTransition } from "react"; 18 | import { useForm } from "react-hook-form"; 19 | import { z } from "zod"; 20 | import FormError from "./FormError"; 21 | import FormSuccess from "./FromSuccess"; 22 | export function ResetPasswordCard() { 23 | const [isPending, startTransition] = useTransition(); 24 | const [error, setError] = useState(""); 25 | const [success, setSuccess] = useState(""); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(resetPasswordSchema), 29 | defaultValues: { 30 | email: "", 31 | }, 32 | }); 33 | 34 | const onSubmit = (data: z.infer) => { 35 | setError(""); 36 | setSuccess(""); 37 | console.log(data); 38 | startTransition(() => { 39 | reset(data).then((res) => { 40 | setError(res?.error); 41 | setSuccess(res?.success); 42 | }); 43 | }); 44 | }; 45 | return ( 46 | <> 47 |
48 | 52 | ( 56 | 57 | email 58 | 59 | 65 | 66 | 67 | 68 | 69 | )} 70 | /> 71 | 72 | 73 | 74 | 77 | 78 | 79 | 82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /components/shared/auth/Signin-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { login } from "@/actions/auth/login.action"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Form, 7 | FormControl, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { loginSchema } from "@/schema"; 15 | import { zodResolver } from "@hookform/resolvers/zod"; 16 | import Link from "next/link"; 17 | import { useSearchParams } from "next/navigation"; 18 | import { useState, useTransition } from "react"; 19 | import { useForm } from "react-hook-form"; 20 | import { z } from "zod"; 21 | import Authproviders from "./Auth_providers"; 22 | import FormError from "./FormError"; 23 | import FormSuccess from "./FromSuccess"; 24 | export function Signin() { 25 | const searchParams = useSearchParams(); 26 | const urlError = 27 | searchParams.get("error") === "OAuthAccountNotLinked" 28 | ? "Please Login with different email !" 29 | : ""; 30 | 31 | const [isPending, startTransition] = useTransition(); 32 | const [error, setError] = useState(""); 33 | const [success, setSuccess] = useState(""); 34 | 35 | const form = useForm>({ 36 | resolver: zodResolver(loginSchema), 37 | defaultValues: { 38 | email: "", 39 | password: "", 40 | }, 41 | }); 42 | 43 | const onSubmit = (data: z.infer) => { 44 | setError(""); 45 | startTransition(() => { 46 | login(data).then((res) => { 47 | setError(res?.error); 48 | setSuccess(res?.success); 49 | }); 50 | }); 51 | }; 52 | return ( 53 | <> 54 |
55 | 59 | ( 63 | 64 | email 65 | 66 | 72 | 73 | 74 | 75 | 76 | )} 77 | /> 78 | ( 82 | 83 | password 84 | 85 | 91 | 92 | 93 |

94 | Forgot password? 95 |

96 | 97 |
98 | )} 99 | /> 100 | 101 | 102 | 103 | 106 | 107 | 108 |

109 | Dont have an account? Sign up 110 |

111 | 112 | 113 | 114 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /components/shared/auth/Signup-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signup } from "@/actions/auth/signup.action"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Form, 7 | FormControl, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { signupSchema } from "@/schema"; 15 | import { zodResolver } from "@hookform/resolvers/zod"; 16 | import Link from "next/link"; 17 | import { useState, useTransition } from "react"; 18 | import { useForm } from "react-hook-form"; 19 | import { z } from "zod"; 20 | import Authproviders from "./Auth_providers"; 21 | import FormError from "./FormError"; 22 | import FormSuccess from "./FromSuccess"; 23 | export function Signup() { 24 | const [isPending, startTransition] = useTransition(); 25 | const [error, setError] = useState(""); 26 | const [success, setSuccess] = useState(""); 27 | 28 | const form = useForm>({ 29 | resolver: zodResolver(signupSchema), 30 | defaultValues: { 31 | username: "", 32 | email: "", 33 | password: "", 34 | }, 35 | }); 36 | 37 | const onSubmit = (data: z.infer) => { 38 | setError(""); 39 | setSuccess(""); 40 | 41 | startTransition(async () => { 42 | setError(""); 43 | setSuccess(""); 44 | await signup(data).then((data) => { 45 | setSuccess(data?.success); 46 | setError(data?.error); 47 | }); 48 | }); 49 | }; 50 | return ( 51 |
52 |
56 | 🔓 Welcome To Auth 57 |
58 |
59 | 63 | ( 67 | 68 | username 69 | 70 | 76 | 77 | 78 | 79 | )} 80 | /> 81 | ( 85 | 86 | email 87 | 88 | 94 | 95 | 96 | 97 | )} 98 | /> 99 | ( 103 | 104 | password 105 | 106 | 112 | 113 | 114 | 115 | )} 116 | /> 117 | 118 | 119 | 120 | 121 | 124 | 125 | 126 | 127 |
128 | 129 |

130 | already have an account ? 131 |

132 | 133 |
134 |
135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /components/shared/auth/VerifyTokenForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { verifyToken } from "@/actions/auth/verifyToken"; 3 | import Link from "next/link"; 4 | import { useSearchParams } from "next/navigation"; 5 | import { useCallback, useEffect, useState } from "react"; 6 | import { BeatLoader } from "react-spinners"; 7 | import FormError from "./FormError"; 8 | import Formsuccess from "./FromSuccess"; 9 | 10 | const VerifyTokenForm = () => { 11 | const [error, setError] = useState(""); 12 | const [success, setSuccess] = useState(""); 13 | const token = useSearchParams().get("token"); 14 | 15 | const onSubmit = useCallback(async () => { 16 | if (!token) { 17 | setError("Token not found"); 18 | return; 19 | } 20 | try { 21 | const res = await verifyToken(token); 22 | setError(res.error); 23 | setSuccess(res.success); 24 | } catch (err) { 25 | setError(err.error); 26 | } 27 | }, [token]); 28 | 29 | useEffect(() => { 30 | onSubmit(); 31 | }, [onSubmit]); 32 | 33 | return ( 34 |
35 |
36 |

Auth 🔐

37 |

comfriming your email

38 | 39 | {!error && !success && } 40 | 41 | 42 | 43 |
44 |
45 | Back to Login 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default VerifyTokenForm; 52 | -------------------------------------------------------------------------------- /components/shared/auth/verifyPassToken.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { verifyResetPasswordToken } from "@/actions/auth/verifyResetToken"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Form, 7 | FormControl, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { passwordschema } from "@/schema"; 15 | import { zodResolver } from "@hookform/resolvers/zod"; 16 | import Link from "next/link"; 17 | import { useSearchParams } from "next/navigation"; 18 | import { useState } from "react"; 19 | import { useForm } from "react-hook-form"; 20 | import { z } from "zod"; 21 | import FormError from "./FormError"; 22 | import Formsuccess from "./FromSuccess"; 23 | 24 | const VerifyTokenForm = () => { 25 | const [error, setError] = useState(""); 26 | const [success, setSuccess] = useState(""); 27 | const [isPending, startTransition] = useState(); 28 | const token = useSearchParams().get("token"); 29 | 30 | const form = useForm>({ 31 | resolver: zodResolver(passwordschema), 32 | defaultValues: { 33 | password: "", 34 | }, 35 | }); 36 | 37 | const onSubmit = (data: z.infer) => { 38 | setError(""); 39 | setSuccess(""); 40 | console.log(data, token); 41 | startTransition(() => { 42 | verifyResetPasswordToken(token, data).then((res) => { 43 | setError(res?.error); 44 | setSuccess(res?.success); 45 | }); 46 | }); 47 | }; 48 | return ( 49 |
50 |
51 |

Auth 🔐

52 |

Reset your Password

53 |
54 | 58 | ( 62 | 63 | Enter New Password 64 | 65 | 71 | 72 | 73 | 74 | )} 75 | /> 76 | 77 | 78 | 79 | 82 | 83 | 84 |
85 | 92 |
93 | ); 94 | }; 95 | 96 | export default VerifyTokenForm; 97 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |