├── .eslintrc.json ├── .gitignore ├── README.md ├── actions └── auth │ ├── admin.ts │ ├── login.ts │ ├── logout.ts │ ├── new-password.ts │ ├── new-verification.ts │ ├── register.ts │ ├── reset.ts │ └── settings.ts ├── app ├── (protected) │ ├── _components │ │ └── navbar.tsx │ ├── admin │ │ └── page.tsx │ ├── client │ │ └── page.tsx │ ├── layout.tsx │ ├── server │ │ └── page.tsx │ └── settings │ │ └── page.tsx ├── api │ ├── admin │ │ └── route.ts │ └── auth │ │ └── [...nextauth] │ │ └── route.ts ├── auth │ ├── error │ │ └── page.tsx │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── new-password │ │ └── page.tsx │ ├── new-verification │ │ └── page.tsx │ ├── register │ │ └── page.tsx │ └── reset │ │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── auth.config.ts ├── auth.ts ├── components.json ├── components ├── auth │ ├── back-button.tsx │ ├── card-wrapper.tsx │ ├── error-card.tsx │ ├── header.tsx │ ├── login-button.tsx │ ├── login-form.tsx │ ├── logout-button.tsx │ ├── new-password-form.tsx │ ├── new-verification-form.tsx │ ├── register-form.tsx │ ├── reset-form.tsx │ ├── role-gate.tsx │ ├── social.tsx │ └── user-button.tsx ├── form-error.tsx ├── form-sucess.tsx ├── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── sonner.tsx │ └── switch.tsx └── user-info.tsx ├── hooks ├── use-current-role.ts └── use-current-user.ts ├── lib ├── account.ts ├── actions │ ├── auth │ │ ├── password-reset-token.ts │ │ ├── two-factor-confirmation.ts │ │ ├── two-factor-token.ts │ │ └── verification-token.ts │ └── user.action.ts ├── auth.ts ├── database.connection.ts ├── mail.ts ├── token.ts └── utils.ts ├── middleware.ts ├── next-auth.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── route.ts ├── schema └── index.ts ├── tailwind.config.ts ├── tsconfig.json └── webpack.config.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | .env 37 | 38 | # hiding tutorial readme 39 | hiddenReadme.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Home Page 3 | ![image](https://github.com/CodeMaster17/role-based-authentication-Authjs/assets/96763776/e1dfd40a-1dda-43ea-8f62-e839aadd30f5) 4 | 5 | Login page 6 | ![image](https://github.com/CodeMaster17/role-based-authentication-Authjs/assets/96763776/9f0e2fad-b380-4f1c-a622-1b45ac9702f3) 7 | 8 | Register Page 9 | ![image](https://github.com/CodeMaster17/role-based-authentication-Authjs/assets/96763776/91375ff6-d19d-47c3-be3e-d8893a6eff66) 10 | 11 | Settings Page 12 | ![image](https://github.com/CodeMaster17/role-based-authentication-Authjs/assets/96763776/91663aaf-f2e1-4aa4-87fe-3b4fde78817d) 13 | 14 | Description:
15 | Welcome to our Next.js Authentication Guide, a comprehensive resource designed to empower developers with the tools and knowledge needed to implement a robust authentication system in their Next.js applications. Leveraging NextAuth.js, this guide covers everything from setting up basic login mechanisms to implementing advanced security features. 16 | 17 | Key Features: 18 | - 🔐 Next-auth v5 (Auth.js) 19 | - 🚀 Next.js 14 with server actions 20 | - 🔑 Credentials Provider 21 | - 🌐 OAuth Provider (Social login with Google & GitHub) 22 | - 🔒 Forgot password functionality 23 | - ✉️ Email verification 24 | - 📱 Two factor verification 25 | - 👥 User roles (Admin & User) 26 | - 🔓 Login component (Opens in redirect or modal) 27 | - 📝 Register component 28 | - 🤔 Forgot password component 29 | - ✅ Verification component 30 | - ⚠️ Error component 31 | - 🔘 Login button 32 | - 🚪 Logout button 33 | - 🚧 Role Gate 34 | - 🔍 Exploring next.js middleware 35 | - 📈 Extending & Exploring next-auth session 36 | - 🔄 Exploring next-auth callbacks 37 | - 👤 useCurrentUser hook 38 | - 🛂 useRole hook 39 | - 🧑 currentUser utility 40 | - 👮 currentRole utility 41 | - 🖥️ Example with server component 42 | - 💻 Example with client component 43 | - 👑 Render content for admins using RoleGate component 44 | - 🛡️ Protect API Routes for admins only 45 | - 🔐 Protect Server Actions for admins only 46 | - 📧 Change email with new verification in Settings page 47 | - 🔑 Change password with old password confirmation in Settings page 48 | - 🔔 Enable/disable two-factor auth in Settings page 49 | - 🔄 Change user role in Settings page (for development purposes only) 50 | 51 | ### Prerequisites 52 | 53 | **Node version 18.7.x** 54 | 55 | ### Cloning the repository 56 | 57 | ```shell 58 | git clone https://github.com/CodeMaster17/role-based-authentication-Authjs.git 59 | ``` 60 | 61 | ### Install packages 62 | 63 | ```shell 64 | npm i 65 | ``` 66 | 67 | ### Setup .env file 68 | 69 | 70 | ```js 71 | DATABASE_URL= 72 | DIRECT_URL= 73 | 74 | AUTH_SECRET= 75 | 76 | GITHUB_CLIENT_ID= 77 | GITHUB_CLIENT_SECRET= 78 | 79 | GOOGLE_CLIENT_ID= 80 | GOOGLE_CLIENT_SECRET= 81 | 82 | RESEND_API_KEY= 83 | 84 | NEXT_PUBLIC_APP_URL= 85 | ``` 86 | 87 | ### Setup Prisma 88 | ```shell 89 | npx prisma generate 90 | npx prisma db push 91 | ``` 92 | 93 | ### Start the app 94 | 95 | ```shell 96 | npm run dev 97 | ``` 98 | 99 | ## Available commands 100 | 101 | Running commands with npm `npm run [command]` 102 | 103 | | command | description | 104 | | :-------------- | :--------------------------------------- | 105 | | `dev` | Starts a development instance of the app | 106 | -------------------------------------------------------------------------------- /actions/auth/admin.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { currentRole } from "@/lib/auth"; 4 | import { UserRole } from "@prisma/client"; 5 | 6 | export const admin = async () => { 7 | const role = await currentRole(); 8 | 9 | if (role === UserRole.ADMIN) { 10 | return { success: "Allowed Server Action!" }; 11 | } 12 | 13 | return { error: "Forbidden Server Action!" }; 14 | }; 15 | -------------------------------------------------------------------------------- /actions/auth/login.ts: -------------------------------------------------------------------------------- 1 | "use server"; // necessary in every auth action 2 | 3 | import * as z from "zod"; 4 | import { LoginSchema } from "@/schema"; 5 | import { signIn } from "@/auth"; 6 | import { DEFAULT_LOGIN_REDIRECT } from "@/route"; 7 | import { AuthError } from "next-auth"; 8 | import { getUserByEmail } from "@/lib/actions/user.action"; 9 | import { generateTwoFactorToken, generateVerificationToken } from "@/lib/token"; 10 | import { sendTwoFactorTokenEmail, sendVerificationEmail } from "@/lib/mail"; 11 | import { getTwoFactorConfirmationByUserId } from "@/lib/actions/auth/two-factor-confirmation"; 12 | import { db } from "@/lib/database.connection"; 13 | import { getTwoFactorTokenByEmail } from "@/lib/actions/auth/two-factor-token"; 14 | 15 | export const Login = async ( 16 | values: z.infer, 17 | callbackUrl?: string | null 18 | ) => { 19 | const validatedFields = LoginSchema.safeParse(values); // valdiating the input values 20 | if (!validatedFields.success) { 21 | return { error: "Invalid fields! " }; 22 | } 23 | const { email, password, code } = validatedFields.data; 24 | 25 | // * not allowing the user to login if the email is not verified (69) 26 | const exisitingUser = await getUserByEmail(email); 27 | 28 | if (!exisitingUser || !exisitingUser.password || !exisitingUser.email) { 29 | return { error: "Email does not exist" }; 30 | } 31 | 32 | if (!exisitingUser.emailVerified) { 33 | const verificationToken = await generateVerificationToken( 34 | exisitingUser.email 35 | ); 36 | 37 | // * sending mail while logging in if email is not verified (72) 38 | await sendVerificationEmail( 39 | verificationToken.email, 40 | verificationToken.token 41 | ); 42 | 43 | return { success: "Confirmation Email sent!" }; 44 | } 45 | //* 2FA verification 46 | if (exisitingUser.isTwoFactorEnabled && exisitingUser.email) { 47 | if (code) { 48 | const twoFactorToken = await getTwoFactorTokenByEmail( 49 | exisitingUser.email 50 | ); 51 | 52 | if (!twoFactorToken) { 53 | return { error: "Invalid code!" }; 54 | } 55 | 56 | if (twoFactorToken.token !== code) { 57 | return { error: "Invalid code!" }; 58 | } 59 | 60 | const hasExpired = new Date(twoFactorToken.expires) < new Date(); 61 | 62 | if (hasExpired) { 63 | return { error: "Code expired!" }; 64 | } 65 | 66 | await db.twoFactorToken.delete({ 67 | where: { id: twoFactorToken.id }, 68 | }); 69 | 70 | const existingConfirmation = await getTwoFactorConfirmationByUserId( 71 | exisitingUser.id 72 | ); 73 | 74 | if (existingConfirmation) { 75 | await db.twoFactorConfirmation.delete({ 76 | where: { id: existingConfirmation.id }, 77 | }); 78 | } 79 | 80 | await db.twoFactorConfirmation.create({ 81 | data: { 82 | userId: exisitingUser.id, 83 | }, 84 | }); 85 | } else { 86 | const twoFactorToken = await generateTwoFactorToken(exisitingUser.email); 87 | await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token); 88 | 89 | return { twoFactor: true }; 90 | } 91 | } 92 | 93 | try { 94 | await signIn("credentials", { 95 | email, 96 | password, 97 | redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT, 98 | }); 99 | } catch (error) { 100 | if (error instanceof AuthError) { 101 | switch (error.type) { 102 | case "CredentialsSignin": 103 | return { error: "Invalid credentials!" }; 104 | default: 105 | return { error: "Something went wrong!" }; 106 | } 107 | } 108 | throw error; 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /actions/auth/logout.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { signOut } from "@/auth"; 4 | 5 | export const logout = async () => { 6 | await signOut(); 7 | }; 8 | -------------------------------------------------------------------------------- /actions/auth/new-password.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import * as z from "zod"; 4 | import bcrypt from "bcryptjs"; 5 | import { NewPasswordSchema } from "@/schema"; 6 | import { getPasswordResetTokenByToken } from "@/lib/actions/auth/password-reset-token"; 7 | import { getUserByEmail } from "@/lib/actions/user.action"; 8 | import { db } from "@/lib/database.connection"; 9 | 10 | 11 | export const newPassword = async ( 12 | values: z.infer, 13 | token?: string | null 14 | ) => { 15 | if (!token) { 16 | return { error: "Missing token!" }; 17 | } 18 | 19 | const validatedFields = NewPasswordSchema.safeParse(values); 20 | 21 | if (!validatedFields.success) { 22 | return { error: "Invalid fields!" }; 23 | } 24 | 25 | const { password } = validatedFields.data; 26 | 27 | const existingToken = await getPasswordResetTokenByToken(token); 28 | 29 | if (!existingToken) { 30 | return { error: "Invalid token!" }; 31 | } 32 | 33 | const hasExpired = new Date(existingToken.expires) < new Date(); 34 | 35 | if (hasExpired) { 36 | return { error: "Token has expired!" }; 37 | } 38 | 39 | const existingUser = await getUserByEmail(existingToken.email); 40 | 41 | if (!existingUser) { 42 | return { error: "Email does not exist!" }; 43 | } 44 | 45 | const hashedPassword = await bcrypt.hash(password, 10); 46 | 47 | await db.user.update({ 48 | where: { id: existingUser.id }, 49 | data: { password: hashedPassword }, 50 | }); 51 | 52 | await db.passwordResetToken.delete({ 53 | where: { id: existingToken.id }, 54 | }); 55 | 56 | return { success: "Password updated!" }; 57 | }; 58 | -------------------------------------------------------------------------------- /actions/auth/new-verification.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getVerificationTokenByToken } from "@/lib/actions/auth/verification-token"; 4 | import { getUserByEmail } from "@/lib/actions/user.action"; 5 | import { db } from "@/lib/database.connection"; 6 | 7 | 8 | 9 | export const newVerification = async (token: string) => { 10 | const existingToken = await getVerificationTokenByToken(token); 11 | 12 | if (!existingToken) { 13 | return { error: "Token does not exist!" }; 14 | } 15 | 16 | const hasExpired = new Date(existingToken.expires) < new Date(); 17 | 18 | if (hasExpired) { 19 | return { error: "Token has expired!" }; 20 | } 21 | 22 | const existingUser = await getUserByEmail(existingToken.email); 23 | 24 | if (!existingUser) { 25 | return { error: "Email does not exist!" }; 26 | } 27 | 28 | // when user updates his email, we create a token and send it to new mail, when user verifies it, we update the email 29 | await db.user.update({ 30 | where: { id: existingUser.id }, 31 | data: { 32 | emailVerified: new Date(), 33 | email: existingToken.email, 34 | }, 35 | }); 36 | 37 | await db.verificationToken.delete({ 38 | where: { id: existingToken.id }, 39 | }); 40 | 41 | return { success: "Email verified!" }; 42 | }; 43 | -------------------------------------------------------------------------------- /actions/auth/register.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { RegisterSchema } from "@/schema"; 4 | import * as z from "zod"; 5 | import bcrypt from "bcryptjs"; 6 | import { db } from "@/lib/database.connection"; 7 | import { getUserByEmail } from "@/lib/actions/user.action"; 8 | import { generateVerificationToken } from "@/lib/token"; 9 | import { sendVerificationEmail } from "@/lib/mail"; 10 | 11 | export const register = async (values: z.infer) => { 12 | // * check and store user in database 13 | 14 | const validatedFields = RegisterSchema.safeParse(values); // safeParse returns a ZodResult object, and it is used to validate the input values 15 | if (!validatedFields.success) { 16 | return { error: "Invalid fields!" }; 17 | } 18 | 19 | const { email, password, name } = validatedFields.data; 20 | const hashedPassword = await bcrypt.hash(password, 10); // 10 is the number of salt rounds 21 | 22 | //finding the email in database 23 | const exisitingUser = await getUserByEmail(email); 24 | 25 | // if user already exists, return error 26 | if (exisitingUser) { 27 | return { error: "Email already exists!" }; 28 | } 29 | 30 | // if not, create and save it in database 31 | await db.user.create({ 32 | data: { 33 | name, 34 | email, 35 | password: hashedPassword, 36 | }, 37 | }); 38 | 39 | // * generating the token after the user is created (68) 40 | const verificationToken = await generateVerificationToken(email); 41 | await sendVerificationEmail(verificationToken.email, verificationToken.token); 42 | 43 | return { success: "Email sent!" }; 44 | }; 45 | -------------------------------------------------------------------------------- /actions/auth/reset.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getUserByEmail } from "@/lib/actions/user.action"; 4 | import { sendPasswordResetEmail } from "@/lib/mail"; 5 | import { generatePasswordResetToken } from "@/lib/token"; 6 | import { ResetSchema } from "@/schema"; 7 | import * as z from "zod"; 8 | 9 | export const reset = async (values: z.infer) => { 10 | const validatedFields = ResetSchema.safeParse(values); 11 | 12 | if (!validatedFields.success) { 13 | return { error: "Invalid emaiL!" }; 14 | } 15 | 16 | const { email } = validatedFields.data; 17 | 18 | const existingUser = await getUserByEmail(email); 19 | 20 | if (!existingUser) { 21 | return { error: "Email not found!" }; 22 | } 23 | 24 | const passwordResetToken = await generatePasswordResetToken(email); 25 | await sendPasswordResetEmail( 26 | passwordResetToken.email, 27 | passwordResetToken.token 28 | ); 29 | 30 | return { success: "Reset email sent!" }; 31 | }; 32 | -------------------------------------------------------------------------------- /actions/auth/settings.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { update } from "@/auth"; 3 | import { getUserByEmail, getUserById } from "@/lib/actions/user.action"; 4 | import { currentUser } from "@/lib/auth"; 5 | import { db } from "@/lib/database.connection"; 6 | import { sendVerificationEmail } from "@/lib/mail"; 7 | import { generateVerificationToken } from "@/lib/token"; 8 | import { SettingsSchema } from "@/schema"; 9 | import bcrypt from "bcryptjs"; 10 | import * as z from "zod"; 11 | 12 | export const settings = async (values: z.infer) => { 13 | const user = await currentUser(); 14 | 15 | if (!user) { 16 | return { error: "Unauthorized" }; 17 | } 18 | 19 | const dbUser = await getUserById(user.id); 20 | 21 | if (!dbUser) { 22 | return { error: "Unauthorized" }; 23 | } 24 | 25 | // if the user is signed in from google or another account 26 | if (user.isOAuth) { 27 | values.email = undefined; 28 | values.password = undefined; 29 | values.newPassword = undefined; 30 | values.isTwoFactorEnabled = undefined; 31 | } 32 | 33 | // checking validations for email 34 | if (values.email && values.email !== user.email) { 35 | const existingUser = await getUserByEmail(values.email); 36 | 37 | if (existingUser && existingUser.id !== user.id) { 38 | return { error: "Email already in use!" }; 39 | } 40 | 41 | const verificationToken = await generateVerificationToken(values.email); 42 | await sendVerificationEmail( 43 | verificationToken.email, 44 | verificationToken.token 45 | ); 46 | 47 | return { success: "Verification email sent!" }; 48 | } 49 | 50 | // checking validations for password 51 | if (values.password && values.newPassword && dbUser.password) { 52 | const passwordsMatch = await bcrypt.compare( 53 | values.password, 54 | dbUser.password 55 | ); 56 | 57 | if (!passwordsMatch) { 58 | return { error: "Incorrect password!" }; 59 | } 60 | 61 | const hashedPassword = await bcrypt.hash(values.newPassword, 10); 62 | values.password = hashedPassword; 63 | values.newPassword = undefined; 64 | } 65 | 66 | // updating the user 67 | const updatedUser = await db.user.update({ 68 | where: { id: dbUser.id }, 69 | data: { 70 | ...values, 71 | }, 72 | }); 73 | 74 | // updating in the session 75 | update({ 76 | user: { 77 | name: updatedUser.name, 78 | email: updatedUser.email, 79 | isTwoFactorEnabled: updatedUser.isTwoFactorEnabled, 80 | role: updatedUser.role, 81 | }, 82 | }); 83 | 84 | return { success: "Settings Updated!" }; 85 | }; 86 | -------------------------------------------------------------------------------- /app/(protected)/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { UserButton } from "@/components/auth/user-button"; 8 | 9 | 10 | export const Navbar = () => { 11 | const pathname = usePathname(); 12 | 13 | return ( 14 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /app/(protected)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { admin } from '@/actions/auth/admin'; 3 | import { RoleGate } from '@/components/auth/role-gate'; 4 | import { FormSuccess } from '@/components/form-sucess'; 5 | import { Button } from '@/components/ui/button'; 6 | import { Card, CardContent, CardHeader } from '@/components/ui/card'; 7 | import { UserRole } from '@prisma/client'; 8 | import React from 'react' 9 | import { toast } from "sonner"; 10 | const AdminPage = () => { 11 | 12 | // for server side 13 | const onServerActionClick = () => { 14 | admin() 15 | .then((data) => { 16 | if (data.error) { 17 | toast.error(data.error); 18 | } 19 | 20 | if (data.success) { 21 | toast.success(data.success); 22 | } 23 | }) 24 | } 25 | 26 | // for client side 27 | const onApiRouteClick = () => { 28 | fetch("/api/admin") 29 | .then((response) => { 30 | if (response.ok) { 31 | toast.success("Allowed API Route!"); 32 | } else { 33 | toast.error("Forbidden API Route!"); 34 | } 35 | }) 36 | } 37 | 38 | return ( 39 | 40 | 41 |

42 | 🔑 Admin 43 |

44 |
45 | 46 | {/* only admin would be able to see this */} 47 | 48 | 51 | 52 |
53 |

54 | Admin-only API Route 55 |

56 | 59 |
60 | 61 |
62 |

63 | Admin-only Server Action 64 |

65 | 68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export default AdminPage 75 | -------------------------------------------------------------------------------- /app/(protected)/client/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { UserInfo } from '@/components/user-info'; 3 | import { useCurrentUser } from '@/hooks/use-current-user'; 4 | import React from 'react' 5 | 6 | const ClientPage = () => { 7 | const user = useCurrentUser(); 8 | console.log(user); 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | 16 | export default ClientPage 17 | -------------------------------------------------------------------------------- /app/(protected)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "./_components/navbar"; 2 | 3 | interface ProtectedLayoutProps { 4 | children: React.ReactNode; 5 | }; 6 | 7 | const ProtectedLayout = ({ children }: ProtectedLayoutProps) => { 8 | return ( 9 |
10 | 11 | {children} 12 |
13 | ); 14 | } 15 | 16 | export default ProtectedLayout; -------------------------------------------------------------------------------- /app/(protected)/server/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserInfo } from '@/components/user-info' 2 | import { currentUser } from '@/lib/auth'; 3 | 4 | import React from 'react' 5 | 6 | const ServerPage = async () => { 7 | const user = await currentUser(); 8 | console.log(user); 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | 16 | export default ServerPage 17 | -------------------------------------------------------------------------------- /app/(protected)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { useForm } from "react-hook-form"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { useTransition, useState } from "react"; 7 | import { useSession } from "next-auth/react"; 8 | 9 | import { Switch } from "@/components/ui/switch"; 10 | import { 11 | Select, 12 | SelectContent, 13 | SelectItem, 14 | SelectTrigger, 15 | SelectValue, 16 | } from "@/components/ui/select"; 17 | 18 | import { 19 | Card, 20 | CardHeader, 21 | CardContent, 22 | } from "@/components/ui/card"; 23 | import { Button } from "@/components/ui/button"; 24 | 25 | import { 26 | Form, 27 | FormField, 28 | FormControl, 29 | FormItem, 30 | FormLabel, 31 | FormDescription, 32 | FormMessage, 33 | } from "@/components/ui/form"; 34 | import { Input } from "@/components/ui/input"; 35 | import { useCurrentUser } from "@/hooks/use-current-user"; 36 | import { FormError } from "@/components/form-error"; 37 | 38 | import { UserRole } from "@prisma/client"; 39 | import { SettingsSchema } from "@/schema"; 40 | import { settings } from "@/actions/auth/settings"; 41 | import { FormSuccess } from "@/components/form-sucess"; 42 | 43 | const SettingsPage = () => { 44 | const user = useCurrentUser(); 45 | 46 | const [error, setError] = useState(); 47 | const [success, setSuccess] = useState(); 48 | const { update } = useSession(); 49 | const [isPending, startTransition] = useTransition(); 50 | 51 | const form = useForm>({ 52 | resolver: zodResolver(SettingsSchema), 53 | defaultValues: { 54 | password: undefined, 55 | newPassword: undefined, 56 | name: user?.name || undefined, 57 | email: user?.email || undefined, 58 | role: user?.role || undefined, 59 | isTwoFactorEnabled: user?.isTwoFactorEnabled || undefined, 60 | } 61 | }); 62 | 63 | const onSubmit = (values: z.infer) => { 64 | startTransition(() => { 65 | settings(values) 66 | .then((data) => { 67 | if (data.error) { 68 | setError(data.error); 69 | } 70 | 71 | if (data.success) { 72 | update(); 73 | setSuccess(data.success); 74 | } 75 | }) 76 | .catch(() => setError("Something went wrong!")); 77 | }); 78 | } 79 | 80 | return ( 81 | 82 | 83 |

84 | ⚙️ Settings 85 |

86 |
87 | 88 |
89 | 93 |
94 | ( 98 | 99 | Name 100 | 101 | 106 | 107 | 108 | 109 | )} 110 | /> 111 | {user?.isOAuth === false && ( 112 | <> 113 | ( 117 | 118 | Email 119 | 120 | 126 | 127 | 128 | 129 | )} 130 | /> 131 | ( 135 | 136 | Password 137 | 138 | 144 | 145 | 146 | 147 | )} 148 | /> 149 | ( 153 | 154 | New Password 155 | 156 | 162 | 163 | 164 | 165 | )} 166 | /> 167 | 168 | )} 169 | ( 173 | 174 | Role 175 | 194 | 195 | 196 | )} 197 | /> 198 | {user?.isOAuth === false && ( 199 | ( 203 | 204 |
205 | Two Factor Authentication 206 | 207 | Enable two factor authentication for your account 208 | 209 |
210 | 211 | 216 | 217 |
218 | )} 219 | /> 220 | )} 221 |
222 | 223 | 224 | 230 | 231 | 232 |
233 |
234 | 235 | ); 236 | } 237 | 238 | export default SettingsPage; -------------------------------------------------------------------------------- /app/api/admin/route.ts: -------------------------------------------------------------------------------- 1 | import { currentRole } from "@/lib/auth"; 2 | import { UserRole } from "@prisma/client"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function GET() { 6 | const role = await currentRole(); 7 | 8 | if (role === UserRole.ADMIN) { 9 | return new NextResponse(null, { status: 200 }); 10 | } 11 | 12 | return new NextResponse(null, { status: 403 }); 13 | } 14 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "@/auth"; 2 | -------------------------------------------------------------------------------- /app/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from "@/components/auth/error-card"; 2 | 3 | const AuthErrorPage = () => { 4 | return ( 5 | 6 | ); 7 | }; 8 | 9 | export default AuthErrorPage; 10 | -------------------------------------------------------------------------------- /app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const AuthLayout = ({ children }: { children: React.ReactNode }) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ) 9 | } 10 | 11 | export default AuthLayout 12 | -------------------------------------------------------------------------------- /app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { LoginForm } from '@/components/auth/login-form' 3 | import React from 'react' 4 | 5 | const LoginPage = () => { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default LoginPage 14 | -------------------------------------------------------------------------------- /app/auth/new-password/page.tsx: -------------------------------------------------------------------------------- 1 | import NewPasswordForm from '@/components/auth/new-password-form' 2 | import React from 'react' 3 | 4 | const NewPassword = () => { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default NewPassword 13 | -------------------------------------------------------------------------------- /app/auth/new-verification/page.tsx: -------------------------------------------------------------------------------- 1 | import NewVerficationForm from '@/components/auth/new-verification-form' 2 | import React from 'react' 3 | 4 | const NewVerificationPage = () => { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default NewVerificationPage 13 | -------------------------------------------------------------------------------- /app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | import RegisterForm from '@/components/auth/register-form' 2 | import React from 'react' 3 | 4 | const Register = () => { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default Register 15 | -------------------------------------------------------------------------------- /app/auth/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetForm } from "@/components/auth/reset-form"; 2 | 3 | const ResetPage = () => { 4 | return ( 5 | 6 | ); 7 | } 8 | 9 | export default ResetPage; -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeMaster17/role-based-authentication-Authjs/39f49f522b28f7ac953d7eaaee1e862b1fb87c92/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 222.2 84% 4.9%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --muted: 210 40% 96.1%; 29 | --muted-foreground: 215.4 16.3% 46.9%; 30 | 31 | --accent: 210 40% 96.1%; 32 | --accent-foreground: 222.2 47.4% 11.2%; 33 | 34 | --destructive: 0 84.2% 60.2%; 35 | --destructive-foreground: 210 40% 98%; 36 | 37 | --border: 214.3 31.8% 91.4%; 38 | --input: 214.3 31.8% 91.4%; 39 | --ring: 222.2 84% 4.9%; 40 | 41 | --radius: 0.5rem; 42 | } 43 | 44 | .dark { 45 | --background: 222.2 84% 4.9%; 46 | --foreground: 210 40% 98%; 47 | 48 | --card: 222.2 84% 4.9%; 49 | --card-foreground: 210 40% 98%; 50 | 51 | --popover: 222.2 84% 4.9%; 52 | --popover-foreground: 210 40% 98%; 53 | 54 | --primary: 210 40% 98%; 55 | --primary-foreground: 222.2 47.4% 11.2%; 56 | 57 | --secondary: 217.2 32.6% 17.5%; 58 | --secondary-foreground: 210 40% 98%; 59 | 60 | --muted: 217.2 32.6% 17.5%; 61 | --muted-foreground: 215 20.2% 65.1%; 62 | 63 | --accent: 217.2 32.6% 17.5%; 64 | --accent-foreground: 210 40% 98%; 65 | 66 | --destructive: 0 62.8% 30.6%; 67 | --destructive-foreground: 210 40% 98%; 68 | 69 | --border: 217.2 32.6% 17.5%; 70 | --input: 217.2 32.6% 17.5%; 71 | --ring: 212.7 26.8% 83.9%; 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | 80 | body { 81 | @apply bg-background text-foreground; 82 | } 83 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | import { SessionProvider } from 'next-auth/react' 5 | import { auth } from '@/auth' 6 | import { Toaster } from "@/components/ui/sonner" 7 | 8 | const inter = Inter({ subsets: ['latin'] }) 9 | 10 | export const metadata: Metadata = { 11 | title: 'Next Auth V4 Example', 12 | description: 'Next.js + Next Auth V4 Example', 13 | } 14 | 15 | export default async function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode 19 | }) { 20 | 21 | const session = await auth(); 22 | return ( 23 | 24 | 25 | 26 | {children} 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Poppins } from "next/font/google"; 3 | import { cn } from "@/lib/utils"; 4 | import { Button } from "@/components/ui/button"; 5 | import { LoginButton } from "@/components/auth/login-button"; 6 | 7 | 8 | 9 | const font = Poppins({ 10 | subsets: ["latin"], 11 | weight: ["600"] 12 | }) 13 | 14 | export default function Home() { 15 | return ( 16 | <> 17 |
18 |
19 |

23 | 🔐 Auth 24 |

25 |

26 | A simple authentication service 27 |

28 | 29 | 30 | 33 | 34 |
35 |
36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | // * This file is used to trigger the middleware on the edge 2 | // * That is why we use auth.ts and auth.config.ts as seprate files 3 | 4 | import bcrypt from "bcryptjs"; 5 | import Credentials from "next-auth/providers/credentials"; 6 | import Github from "next-auth/providers/github"; 7 | import Google from "next-auth/providers/google"; 8 | import type { NextAuthConfig } from "next-auth"; 9 | import { LoginSchema } from "./schema"; 10 | import { getUserByEmail } from "./lib/actions/user.action"; 11 | 12 | export default { 13 | providers: [ 14 | Github({ 15 | clientId: process.env.GITHUB_CLIENT_ID, 16 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 17 | }), 18 | Google({ 19 | clientId: process.env.GOOGLE_CLIENT_ID, 20 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 21 | }), 22 | Credentials({ 23 | async authorize(credentials) { 24 | const validatedFields = LoginSchema.safeParse(credentials); // again doing validation 25 | if (validatedFields.success) { 26 | // if validation is successfull 27 | const { email, password } = validatedFields.data; 28 | 29 | const user = await getUserByEmail(email); // checking if user is present in database 30 | if (!user || !user.password) return null; // password will be null when user has registered using google or github 31 | 32 | const passwordsMatch = await bcrypt.compare(password, user.password); // comparing the hashed password 33 | 34 | if (passwordsMatch) { 35 | return user; 36 | } 37 | } 38 | return null; 39 | }, 40 | }), 41 | ], 42 | } satisfies NextAuthConfig; 43 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | // * Comnfiguration for authentication 2 | import NextAuth from "next-auth"; 3 | import authConfig from "@/auth.config"; 4 | import { db } from "./lib/database.connection"; 5 | import { PrismaAdapter } from "@auth/prisma-adapter"; 6 | import { getUserById } from "./lib/actions/user.action"; 7 | import { UserRole } from "@prisma/client"; 8 | import { getTwoFactorConfirmationByUserId } from "./lib/actions/auth/two-factor-confirmation"; 9 | import { getAccountByUserId } from "./lib/account"; 10 | 11 | export const { 12 | handlers: { GET, POST }, 13 | auth, 14 | signIn, 15 | signOut, 16 | update, 17 | } = NextAuth({ 18 | // * This is for solving errors when using linkAccount feature 19 | pages: { 20 | signIn: "/auth/login", 21 | error: "/auth/error", 22 | }, 23 | 24 | // * This is for linkAccount feature 25 | events: { 26 | async linkAccount({ user }) { 27 | await db.user.update({ 28 | where: { id: user.id }, 29 | data: { emailVerified: new Date() }, 30 | }); 31 | }, 32 | }, 33 | 34 | callbacks: { 35 | // * (70) 36 | async signIn({ user, account }) { 37 | // Allow OAuth without email verification 38 | if (account?.provider !== "credentials") return true; 39 | 40 | const existingUser = await getUserById(user.id); 41 | 42 | // Prevent sign in without email verification 43 | if (!existingUser?.emailVerified) return false; 44 | 45 | // * Prevent sign in without two factor confirmation (99) 46 | if (existingUser.isTwoFactorEnabled) { 47 | const twoFactorConfirmation = await getTwoFactorConfirmationByUserId( 48 | existingUser.id 49 | ); 50 | 51 | if (!twoFactorConfirmation) return false; 52 | 53 | // Delete two factor confirmation for next sign in 54 | await db.twoFactorConfirmation.delete({ 55 | where: { id: twoFactorConfirmation.id }, 56 | }); 57 | } 58 | 59 | return true; 60 | }, 61 | 62 | async session({ token, session }) { 63 | if (token.sub && session.user) { 64 | session.user.id = token.sub; 65 | } 66 | 67 | if (token.role && session.user) { 68 | session.user.role = token.role as UserRole; 69 | } 70 | 71 | if (session.user) { 72 | session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean; 73 | } 74 | 75 | if (session.user) { 76 | session.user.name = token.name; 77 | session.user.email = token.email; 78 | session.user.isOAuth = token.isOAuth as boolean; 79 | } 80 | 81 | return session; 82 | }, 83 | 84 | async jwt({ token }) { 85 | // fecthing the user 86 | 87 | if (!token.sub) return token; 88 | const exisitingUser = await getUserById(token.sub); 89 | if (!exisitingUser) return token; 90 | 91 | const existingAccount = await getAccountByUserId(exisitingUser.id); 92 | 93 | token.isOAuth = !!existingAccount; 94 | token.role = exisitingUser.role; 95 | token.name = exisitingUser.name; 96 | token.email = exisitingUser.email; 97 | token.isTwoFactorEnabled = exisitingUser.isTwoFactorEnabled; 98 | 99 | return token; 100 | }, 101 | }, 102 | adapter: PrismaAdapter(db), // prisma adapter is supported on non edge 103 | session: { strategy: "jwt" }, 104 | ...authConfig, 105 | }); 106 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 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/auth/back-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | 7 | interface BackButtonProps { 8 | href: string; 9 | label: string; 10 | }; 11 | 12 | export const BackButton = ({ 13 | href, 14 | label, 15 | }: BackButtonProps) => { 16 | return ( 17 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /components/auth/card-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardContent, 6 | CardFooter, 7 | CardHeader 8 | } from "@/components/ui/card"; 9 | import { Header } from "@/components/auth/header"; 10 | import { Social } from "@/components/auth/social"; 11 | import { BackButton } from "@/components/auth/back-button"; 12 | 13 | interface CardWrapperProps { 14 | children: React.ReactNode; 15 | headerLabel: string; 16 | backButtonLabel: string; 17 | backButtonHref: string; 18 | showSocial?: boolean; 19 | }; 20 | 21 | export const CardWrapper = ({ 22 | children, 23 | headerLabel, 24 | backButtonLabel, 25 | backButtonHref, 26 | showSocial 27 | }: CardWrapperProps) => { 28 | return ( 29 | 30 | 31 |
32 | 33 | 34 | {children} 35 | 36 | {showSocial && ( 37 | 38 | 39 | 40 | )} 41 | 42 | 46 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /components/auth/error-card.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; 2 | 3 | import { CardWrapper } from "@/components/auth/card-wrapper"; 4 | 5 | export const ErrorCard = () => { 6 | return ( 7 | 12 |
13 | 14 |
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /components/auth/header.tsx: -------------------------------------------------------------------------------- 1 | import { Poppins } from "next/font/google"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const font = Poppins({ 6 | subsets: ["latin"], 7 | weight: ["600"], 8 | }); 9 | 10 | interface HeaderProps { 11 | label: string; 12 | }; 13 | 14 | export const Header = ({ 15 | label, 16 | }: HeaderProps) => { 17 | return ( 18 |
19 |

23 | 🔐 Auth 24 |

25 |

26 | {label} 27 |

28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/auth/login-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogTrigger, 9 | } from "@/components/ui/dialog"; 10 | import { LoginForm } from "@/components/auth/login-form"; 11 | 12 | interface LoginButtonProps { 13 | children: React.ReactNode; 14 | mode?: "modal" | "redirect", 15 | asChild?: boolean; 16 | }; 17 | 18 | export const LoginButton = ({ 19 | children, 20 | mode = "redirect", 21 | asChild 22 | }: LoginButtonProps) => { 23 | const router = useRouter(); 24 | 25 | const onClick = () => { 26 | router.push("/auth/login"); 27 | }; 28 | 29 | if (mode === "modal") { 30 | return ( 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | return ( 43 | 44 | {children} 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /components/auth/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { useForm } from "react-hook-form"; 5 | import { useState, useTransition } from "react"; 6 | import { useSearchParams } from "next/navigation"; 7 | import { zodResolver } from "@hookform/resolvers/zod"; 8 | import Link from "next/link"; 9 | 10 | import { Input } from "@/components/ui/input"; 11 | import { 12 | Form, 13 | FormControl, 14 | FormField, 15 | FormItem, 16 | FormLabel, 17 | FormMessage, 18 | } from "@/components/ui/form"; 19 | import { CardWrapper } from "@/components/auth/card-wrapper" 20 | import { Button } from "@/components/ui/button"; 21 | import { FormError } from "@/components/form-error"; 22 | import { LoginSchema } from "@/schema"; 23 | import { Login } from "@/actions/auth/login"; 24 | import { FormSuccess } from "../form-sucess"; 25 | 26 | 27 | export const LoginForm = () => { 28 | const searchParams = useSearchParams() 29 | const callbackUrl = searchParams.get("callbackUrl"); 30 | const urlError = searchParams.get("error") === "OAuthAccountNotLinked" 31 | ? "Email already in use with different provider!" 32 | : ""; 33 | 34 | const [showTwoFactor, setShowTwoFactor] = useState(false); 35 | const [error, setError] = useState(""); 36 | const [success, setSuccess] = useState(""); 37 | const [isPending, startTransition] = useTransition(); 38 | 39 | const form = useForm>({ 40 | resolver: zodResolver(LoginSchema), 41 | defaultValues: { 42 | email: "", 43 | password: "", 44 | }, 45 | }); 46 | 47 | const onSubmit = (values: z.infer) => { 48 | setError(""); 49 | setSuccess(""); 50 | 51 | startTransition(() => { 52 | Login(values, callbackUrl) 53 | .then((data) => { 54 | if (data?.error) { 55 | form.reset(); 56 | setError(data.error); 57 | } 58 | 59 | if (data?.success) { 60 | form.reset(); 61 | setSuccess(data.success); 62 | } 63 | 64 | if (data?.twoFactor) { 65 | setShowTwoFactor(true); 66 | } 67 | }) 68 | .catch(() => setError("Something went wrong")); 69 | }); 70 | }; 71 | 72 | return ( 73 | 79 |
80 | 84 |
85 | {showTwoFactor && ( 86 | ( 90 | 91 | Two Factor Code 92 | 93 | 98 | 99 | 100 | 101 | )} 102 | /> 103 | )} 104 | {!showTwoFactor && ( 105 | <> 106 | ( 110 | 111 | Email 112 | 113 | 119 | 120 | 121 | 122 | )} 123 | /> 124 | ( 128 | 129 | Password 130 | 131 | 137 | 138 | 148 | 149 | 150 | )} 151 | /> 152 | 153 | )} 154 |
155 | 156 | 157 | 164 | 165 | 166 |
167 | ); 168 | }; 169 | -------------------------------------------------------------------------------- /components/auth/logout-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { logout } from "@/actions/auth/logout"; 4 | 5 | 6 | interface LogoutButtonProps { 7 | children?: React.ReactNode; 8 | }; 9 | 10 | export const LogoutButton = ({ 11 | children 12 | }: LogoutButtonProps) => { 13 | const onClick = () => { 14 | logout(); 15 | }; 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /components/auth/new-password-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useState, useTransition } from 'react' 3 | import { CardWrapper } from './card-wrapper'; 4 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; 5 | import { Input } from "@/components/ui/input"; 6 | import { useForm } from 'react-hook-form'; 7 | import { NewPasswordSchema } from '@/schema'; 8 | import { zodResolver } from '@hookform/resolvers/zod'; 9 | import { z } from 'zod'; 10 | 11 | import { FormError } from '../form-error'; 12 | import { FormSuccess } from '../form-sucess'; 13 | import { Button } from '../ui/button'; 14 | import { useSearchParams } from 'next/navigation'; 15 | import { newPassword } from '@/actions/auth/new-password'; 16 | 17 | const NewPasswordForm = () => { 18 | const searchParams = useSearchParams(); 19 | const token = searchParams.get("token"); 20 | 21 | const [error, setError] = useState(""); 22 | const [success, setSuccess] = useState(""); 23 | const [isPending, startTransition] = useTransition(); 24 | 25 | const form = useForm>({ 26 | resolver: zodResolver(NewPasswordSchema), 27 | defaultValues: { 28 | password: "", 29 | }, 30 | }); 31 | 32 | const onSubmit = (values: z.infer) => { 33 | setError(""); 34 | setSuccess(""); 35 | 36 | startTransition(() => { 37 | newPassword(values, token) 38 | .then((data) => { 39 | setError(data?.error); 40 | setSuccess(data?.success); 41 | }); 42 | }); 43 | }; 44 | 45 | return ( 46 | 51 |
52 | 56 |
57 | ( 61 | 62 | Password 63 | 64 | 70 | 71 | 72 | 73 | )} 74 | /> 75 |
76 | 77 | 78 | 85 | 86 | 87 |
88 | ) 89 | } 90 | 91 | export default NewPasswordForm 92 | -------------------------------------------------------------------------------- /components/auth/new-verification-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useCallback, useEffect, useState } from 'react' 3 | import { CardWrapper } from './card-wrapper' 4 | import { BeatLoader } from 'react-spinners' 5 | import { useSearchParams } from 'next/navigation' 6 | import { newVerification } from '@/actions/auth/new-verification' 7 | import { FormSuccess } from '../form-sucess' 8 | import { FormError } from '../form-error' 9 | 10 | const NewVerficationForm = () => { 11 | 12 | const [error, setError] = useState(); 13 | const [success, setSuccess] = useState(); 14 | 15 | const searchParams = useSearchParams(); 16 | 17 | const token = searchParams.get("token"); 18 | 19 | const onSubmit = useCallback(() => { 20 | if (success || error) return; 21 | 22 | console.log("token", token) 23 | 24 | if (!token) { 25 | setError("Missing token!"); 26 | return; 27 | } 28 | 29 | newVerification(token) 30 | .then((data) => { 31 | setSuccess(data.success); 32 | setError(data.error); 33 | }) 34 | .catch(() => { 35 | setError("Something went wrong!"); 36 | }) 37 | }, [token, success, error]); 38 | 39 | useEffect(() => { 40 | onSubmit(); 41 | }, [onSubmit]); 42 | return ( 43 | 48 |
49 | {!success && !error && ( 50 | 51 | )} 52 | 53 | {!success && ( 54 | 55 | )} 56 |
57 |
58 | ) 59 | } 60 | 61 | export default NewVerficationForm 62 | -------------------------------------------------------------------------------- /components/auth/register-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import * as z from "zod"; 3 | import { useState, useTransition } from "react"; 4 | import { useForm } from "react-hook-form"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { Input } from "@/components/ui/input"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormLabel, 13 | FormMessage, 14 | } from "@/components/ui/form"; 15 | import { CardWrapper } from "@/components/auth/card-wrapper" 16 | import { Button } from "@/components/ui/button"; 17 | import { FormError } from "@/components/form-error"; 18 | import { register } from "@/actions/auth/register"; 19 | import { FormSuccess } from "../form-sucess"; 20 | import { RegisterSchema } from "@/schema"; 21 | 22 | export const RegisterForm = () => { 23 | const [error, setError] = useState(""); 24 | const [success, setSuccess] = useState(""); 25 | const [isPending, startTransition] = useTransition(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(RegisterSchema), 29 | defaultValues: { 30 | email: "", 31 | password: "", 32 | name: "", 33 | }, 34 | }); 35 | 36 | const onSubmit = (values: z.infer) => { 37 | setError(""); 38 | setSuccess(""); 39 | 40 | startTransition(() => { 41 | register(values) 42 | .then((data: any) => { 43 | setError(data.error); 44 | setSuccess(data.success); 45 | }); 46 | }); 47 | }; 48 | return ( 49 | 55 |
56 | 60 |
61 | ( 65 | 66 | Name 67 | 68 | 73 | 74 | 75 | 76 | )} 77 | /> 78 | ( 82 | 83 | Email 84 | 85 | 91 | 92 | 93 | 94 | )} 95 | /> 96 | ( 100 | 101 | Password 102 | 103 | 109 | 110 | 111 | 112 | )} 113 | /> 114 |
115 | 116 | 117 | 124 | 125 | 126 |
127 | ) 128 | } 129 | 130 | export default RegisterForm 131 | -------------------------------------------------------------------------------- /components/auth/reset-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { useForm } from "react-hook-form"; 5 | import { useState, useTransition } from "react"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | 8 | import { Input } from "@/components/ui/input"; 9 | import { 10 | Form, 11 | FormControl, 12 | FormField, 13 | FormItem, 14 | FormLabel, 15 | FormMessage, 16 | } from "@/components/ui/form"; 17 | import { CardWrapper } from "@/components/auth/card-wrapper" 18 | import { Button } from "@/components/ui/button"; 19 | import { FormError } from "@/components/form-error"; 20 | import { ResetSchema } from "@/schema"; 21 | import { FormSuccess } from "../form-sucess"; 22 | import { reset } from "@/actions/auth/reset"; 23 | 24 | 25 | export const ResetForm = () => { 26 | const [error, setError] = useState(""); 27 | const [success, setSuccess] = useState(""); 28 | const [isPending, startTransition] = useTransition(); 29 | 30 | const form = useForm>({ 31 | resolver: zodResolver(ResetSchema), 32 | defaultValues: { 33 | email: "", 34 | }, 35 | }); 36 | 37 | const onSubmit = (values: z.infer) => { 38 | setError(""); 39 | setSuccess(""); 40 | 41 | startTransition(() => { 42 | reset(values) 43 | .then((data) => { 44 | setError(data?.error); 45 | setSuccess(data?.success); 46 | }); 47 | }); 48 | }; 49 | 50 | return ( 51 | 56 |
57 | 61 |
62 | ( 66 | 67 | Email 68 | 69 | 75 | 76 | 77 | 78 | )} 79 | /> 80 |
81 | 82 | 83 | 90 | 91 | 92 |
93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /components/auth/role-gate.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { UserRole } from "@prisma/client"; 4 | 5 | import { useCurrentRole } from "@/hooks/use-current-role"; 6 | import { FormError } from "@/components/form-error"; 7 | 8 | interface RoleGateProps { 9 | children: React.ReactNode; 10 | allowedRole: UserRole; 11 | }; 12 | 13 | export const RoleGate = ({ 14 | children, 15 | allowedRole, 16 | }: RoleGateProps) => { 17 | const role = useCurrentRole(); 18 | 19 | if (role !== allowedRole) { 20 | return ( 21 | 22 | ) 23 | } 24 | 25 | return ( 26 | <> 27 | {children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/auth/social.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; // this is we have to import when in client side 4 | import { FcGoogle } from "react-icons/fc"; 5 | import { FaGithub } from "react-icons/fa"; 6 | import { useSearchParams } from "next/navigation"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | import { DEFAULT_LOGIN_REDIRECT } from "@/route"; 10 | 11 | export const Social = () => { 12 | const searchParams = useSearchParams(); 13 | const callbackUrl = searchParams.get("callbackUrl"); 14 | 15 | const onClick = (provider: "google" | "github") => { 16 | signIn(provider, { 17 | callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT, 18 | }); 19 | } 20 | 21 | return ( 22 |
23 | 31 | 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /components/auth/user-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FaUser } from "react-icons/fa"; 4 | import { ExitIcon } from "@radix-ui/react-icons" 5 | 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu"; 12 | import { 13 | Avatar, 14 | AvatarImage, 15 | AvatarFallback, 16 | } from "@/components/ui/avatar"; 17 | import { useCurrentUser } from "@/hooks/use-current-user"; 18 | import { LogoutButton } from "@/components/auth/logout-button"; 19 | 20 | export const UserButton = () => { 21 | const user = useCurrentUser(); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Logout 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /components/form-error.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; 2 | 3 | interface FormErrorProps { 4 | message?: string; 5 | }; 6 | 7 | export const FormError = ({ 8 | message, 9 | }: FormErrorProps) => { 10 | if (!message) return null; 11 | 12 | return ( 13 |
14 | 15 |

{message}

16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/form-sucess.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircledIcon } from "@radix-ui/react-icons"; 2 | 3 | interface FormSuccessProps { 4 | message?: string; 5 | }; 6 | 7 | export const FormSuccess = ({ 8 | message, 9 | }: FormSuccessProps) => { 10 | if (!message) return null; 11 | 12 | return ( 13 |
14 | 15 |

{message}

16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | success: "border-transparent bg-emerald-500 text-primary-foreground" 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: "default", 23 | }, 24 | } 25 | ) 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps { } 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
34 | ) 35 | } 36 | 37 | export { Badge, badgeVariants } 38 | -------------------------------------------------------------------------------- /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 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { 6 | CheckIcon, 7 | ChevronRightIcon, 8 | DotFilledIcon, 9 | } from "@radix-ui/react-icons" 10 | 11 | import { cn } from "@/lib/utils" 12 | 13 | const DropdownMenu = DropdownMenuPrimitive.Root 14 | 15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 16 | 17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 18 | 19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 20 | 21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 22 | 23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 24 | 25 | const DropdownMenuSubTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef & { 28 | inset?: boolean 29 | } 30 | >(({ className, inset, children, ...props }, ref) => ( 31 | 40 | {children} 41 | 42 | 43 | )) 44 | DropdownMenuSubTrigger.displayName = 45 | DropdownMenuPrimitive.SubTrigger.displayName 46 | 47 | const DropdownMenuSubContent = React.forwardRef< 48 | React.ElementRef, 49 | React.ComponentPropsWithoutRef 50 | >(({ className, ...props }, ref) => ( 51 | 59 | )) 60 | DropdownMenuSubContent.displayName = 61 | DropdownMenuPrimitive.SubContent.displayName 62 | 63 | const DropdownMenuContent = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, sideOffset = 4, ...props }, ref) => ( 67 | 68 | 78 | 79 | )) 80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 81 | 82 | const DropdownMenuItem = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef & { 85 | inset?: boolean 86 | } 87 | >(({ className, inset, ...props }, ref) => ( 88 | 97 | )) 98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 99 | 100 | const DropdownMenuCheckboxItem = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, children, checked, ...props }, ref) => ( 104 | 113 | 114 | 115 | 116 | 117 | 118 | {children} 119 | 120 | )) 121 | DropdownMenuCheckboxItem.displayName = 122 | DropdownMenuPrimitive.CheckboxItem.displayName 123 | 124 | const DropdownMenuRadioItem = React.forwardRef< 125 | React.ElementRef, 126 | React.ComponentPropsWithoutRef 127 | >(({ className, children, ...props }, ref) => ( 128 | 136 | 137 | 138 | 139 | 140 | 141 | {children} 142 | 143 | )) 144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 145 | 146 | const DropdownMenuLabel = React.forwardRef< 147 | React.ElementRef, 148 | React.ComponentPropsWithoutRef & { 149 | inset?: boolean 150 | } 151 | >(({ className, inset, ...props }, ref) => ( 152 | 161 | )) 162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 163 | 164 | const DropdownMenuSeparator = React.forwardRef< 165 | React.ElementRef, 166 | React.ComponentPropsWithoutRef 167 | >(({ className, ...props }, ref) => ( 168 | 173 | )) 174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 175 | 176 | const DropdownMenuShortcut = ({ 177 | className, 178 | ...props 179 | }: React.HTMLAttributes) => { 180 | return ( 181 | 185 | ) 186 | } 187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 188 | 189 | export { 190 | DropdownMenu, 191 | DropdownMenuTrigger, 192 | DropdownMenuContent, 193 | DropdownMenuItem, 194 | DropdownMenuCheckboxItem, 195 | DropdownMenuRadioItem, 196 | DropdownMenuLabel, 197 | DropdownMenuSeparator, 198 | DropdownMenuShortcut, 199 | DropdownMenuGroup, 200 | DropdownMenuPortal, 201 | DropdownMenuSub, 202 | DropdownMenuSubContent, 203 | DropdownMenuSubTrigger, 204 | DropdownMenuRadioGroup, 205 | } 206 | -------------------------------------------------------------------------------- /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 |