├── .env.example ├── .eslintrc.json ├── .github └── workflows │ ├── codeql.yml │ ├── greetings.yml │ ├── npm-publish.yml │ └── stale.yml ├── .gitignore ├── README.md ├── actions ├── admin.ts ├── login.ts ├── logout.ts ├── new-password.ts ├── new-verification.ts ├── register.ts ├── reset.ts └── settings.ts ├── app ├── (design) │ ├── heading-text.tsx │ ├── landing-start-button.tsx │ ├── use-3dcard.tsx │ └── use-sparkles.tsx ├── (marketing) │ ├── _components │ │ └── logo.tsx │ ├── layout.tsx │ └── page.tsx ├── (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 │ └── cron │ │ └── 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 ├── auth.config.ts ├── auth.ts ├── components.json ├── components ├── 3d-card.tsx ├── 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 ├── email-template.tsx ├── form-error.tsx ├── form-success.tsx ├── sparkles.tsx ├── sparkles2.tsx ├── theme-provider.tsx ├── ui │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ └── switch.tsx └── user-info.tsx ├── data ├── account.ts ├── password-reset-token.ts ├── two-factor-confirmation.ts ├── two-factor-token.ts ├── user.ts └── verification-token.ts ├── hooks ├── use-current-role.ts └── use-current-user.ts ├── lib ├── auth.ts ├── db.ts ├── mail.ts ├── token.ts └── utils.ts ├── middleware.ts ├── next-auth.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── favicon.ico ├── logo.png ├── lottie │ └── moving-file │ │ └── Animation1.json ├── next.svg ├── shitty1.png └── vercel.svg ├── routes.ts ├── schema └── index.ts ├── tailwind.config.js ├── tailwind.config.ts ├── tsconfig.json └── vercel.json /.env.example: -------------------------------------------------------------------------------- 1 | 2 | 3 | # This was inserted by `prisma init`: 4 | # Environment variables declared in this file are automatically made available to Prisma. 5 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 6 | 7 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 8 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 9 | 10 | DATABASE_URL="" 11 | DIRECT_URL="" 12 | 13 | NEXTAUTH_SECRET= 14 | 15 | GITHUB_CLIENT_ID= 16 | GITHUB_CLIENT_SECRET= 17 | 18 | GOOGLE_CLIENT_ID= 19 | GOOGLE_CLIENT_SECRET= 20 | 21 | RESEND_API_KEY= 22 | 23 | NEXT_PUBLIC_APP_URL="http://localhost:3000" 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | schedule: 20 | - cron: '18 23 * * 3' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners 29 | # Consider using larger runners for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 32 | permissions: 33 | # required for all workflows 34 | security-events: write 35 | 36 | # only required for workflows in private repositories 37 | actions: read 38 | contents: read 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | language: [ 'javascript-typescript' ] 44 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 45 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 46 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 47 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 48 | 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v4 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@v3 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 63 | # queries: security-extended,security-and-quality 64 | 65 | 66 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 67 | # If this step fails, then you should remove it and run the build manually (see below) 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@v3 70 | 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | 74 | # If the Autobuild fails above, remove it and uncomment the following three lines. 75 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 76 | 77 | # - run: | 78 | # echo "Run, Build Application using script" 79 | # ./location_of_script_within_repo/buildscript.sh 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@v3 83 | with: 84 | category: "/language:${{matrix.language}}" 85 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "Message that will be displayed on users' first issue" 16 | pr-message: "Message that will be displayed on users' first pull request" 17 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '24 19 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: 'Stale issue message' 25 | stale-pr-message: 'Stale pull request message' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' 28 | -------------------------------------------------------------------------------- /.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 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔐 Authify - Auth for devs: (Next-auth V5) 2 | ## Top-20 Product of the Day on PH 3 | [ProductHunt](https://www.producthunt.com/products/authify) 4 | 5 | Authify delivers a developer-friendly authentication solution 🤝. Our focus: Simplicity meets Robust Security! 🛡️ Seamlessly integrate into your projects with ease, hassle-free implementation guaranteed! 🌐💻 6 | > Demo Available @ bottom 🎥 7 | 8 | ## 🚀 Key Features 9 | 10 | - **MFA Login**: Securely authenticate users with Multi-Factor Authentication for an added layer of protection. 11 | - **Password Recovery**: Enable users to recover their passwords through a secure and user-friendly process. 12 | - **Resend Verification Emails**: Effortlessly resend verification emails to users, ensuring a smooth onboarding experience. 13 | - **Role-Based Access Control (RBAC)**: Implement fine-grained access controls with role-based permissions, allowing you to tailor access levels to different users. 14 | - **Server and Client Components Built-in**: Authify comes with both server and client components out of the box, streamlining the integration process for your application. 15 | - **Customizable to Your Needs**: Tailor Authify to fit your specific requirements by customizing its features and functionality according to your application's unique needs. 16 | 17 | ## ⚡️ Quick Setup (Use this repo as a template and start coding 👨🏻‍💻) 18 | ## 1. Install Dependencies 19 | 20 | ```bash 21 | npm i 22 | ``` 23 | ## 2. Configure Environment Variables: 24 | Copy the .env.example file to a new file named .env. Update the file with your secrets and credentials. 25 | 26 | ```bash 27 | cp .env.example .env 28 | # Edit the .env file with your secrets 29 | ``` 30 | ## 3. Create OAuth Apps 31 | Create OAuth apps on Google and GitHub. Obtain the client IDs and secrets, then update your .env file with these values. 32 | 33 | ## 4. Set Up Prisma 34 | Run the following commands to set up and generate Prisma: 35 | 36 | ```bash 37 | npx prisma generate 38 | npx prisma db push 39 | npx prisma studio 40 | ``` 41 | ## 5. Set Up Local Database or Spin Up your own. 42 | If you don't have Docker installed, you can install it from Docker's official website. 43 | 44 | ```bash 45 | docker run -p 5432:5432 --name your-postgres-container -e POSTGRES_PASSWORD=your-password -d postgres 46 | ``` 47 | ### or 48 | 49 | Use neon.tech for PostgreSQL: 50 | Follow the instructions on neon.tech to set up a PostgreSQL database. 51 | 52 | ## 7. Optional: Set Up Email (with Resend) 53 | Configure email settings if you want to enable email functionality. Update the necessary fields in your .env file. 54 | 55 | ## 8. Start the Development Server 56 | Run the following command to start the development server: 57 | ```bash 58 | npm run dev 59 | ``` 60 | ### Your Authify setup should now be running locally. Access it at http://localhost:3000. 🎉 61 | 62 | ## 📦 Built With 63 | 64 | - TailwindCSS 65 | - NextJS 66 | - TypeScript 67 | - Resend 68 | - Next-Auth 69 | - Shadcn-UI 70 | 71 | ## 💡 Future Improvements 72 | 73 | - *Testing*: Add certain edge test cases to test the product on every paradigm. 74 | - *EnterPrise Level*: Introduction of enterprise level SSO. 75 | - *Themes*: Let users pick their own color themes, including dark and light mode. 76 | - *PassKey Authentication*: Addition of web-authn passkey authentication in the future. 77 | 78 | ## 🎬 Video 79 | 80 | https://github.com/Neon-20/Authy/assets/55043383/c4921663-f1c9-4e17-a0b5-124934f83f0b 81 | -------------------------------------------------------------------------------- /actions/admin.ts: -------------------------------------------------------------------------------- 1 | //Similarly to be done for server actions 2 | "use server"; 3 | 4 | import { CurrentRole } from "@/lib/auth"; 5 | import { UserRole } from "@prisma/client"; 6 | 7 | export const admin = async() => { 8 | const role = await CurrentRole(); 9 | if(role!==UserRole.ADMIN){ 10 | return{ 11 | error:"Only Admins are allowed over here" 12 | } 13 | } 14 | return{ 15 | success:"You are allowed to access Server Actions" 16 | } 17 | } -------------------------------------------------------------------------------- /actions/login.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import {LoginSchema} from "@/schema"; 4 | import * as z from "zod"; 5 | import { signIn } from "@/auth"; 6 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; 7 | import {AuthError} from "next-auth"; 8 | import { generateVerificationToken,generateTwoFactorToken } from "@/lib/token"; 9 | import { getUserByEmail } from "@/data/user"; 10 | import { sendVerificationEmail,sendTwoFactorEmail } from "@/lib/mail"; 11 | import { getTwoFactorTokenfromEmail } from "@/data/two-factor-token"; 12 | import { db } from "@/lib/db"; 13 | import { getTwoFactorConfirmationByUserId } from "@/data/two-factor-confirmation"; 14 | import bcrypt from 'bcryptjs'; 15 | 16 | export const Login = async( 17 | values:z.infer, 18 | callbackUrl?:string | null 19 | ) =>{ 20 | //validate these fields on server side as well 21 | // because data can be manipulated from client side as well 22 | const validatedFields = LoginSchema.safeParse(values); 23 | 24 | if(!validatedFields.success){ 25 | return{ 26 | error:"Invalid Fields" 27 | } 28 | } 29 | 30 | //If the details are validated then we have as 31 | const {email,password,code} = validatedFields.data 32 | const existingUser = await getUserByEmail(email); 33 | if(!existingUser?.email || !existingUser || !existingUser.password || 34 | !bcrypt.compareSync(password,existingUser.password)){ // 35 | return { 36 | error:"Invalid Credentials" 37 | } 38 | } 39 | 40 | if(!existingUser.emailVerified){ 41 | const verificationToken = await generateVerificationToken(existingUser.email); 42 | 43 | await sendVerificationEmail( 44 | verificationToken.email, 45 | verificationToken.token 46 | ) 47 | return { 48 | success:"Confirmation email sent!" 49 | } 50 | } 51 | 52 | if(existingUser.isTwoFactorEnabled && existingUser.email){ 53 | if(code){ 54 | const twoFactorToken = await getTwoFactorTokenfromEmail( 55 | existingUser.email 56 | ); 57 | if(!twoFactorToken){ 58 | return{ 59 | error:"Invalid Code" 60 | } 61 | } 62 | 63 | if(twoFactorToken.token!==code){ 64 | return{ 65 | error:"Invalid Code" 66 | } 67 | } 68 | 69 | const hasExpired = new Date(twoFactorToken.expires) < new Date(); 70 | if(hasExpired){ 71 | return{ 72 | error:"Code has expired" 73 | } 74 | } 75 | 76 | //finally remove the 2FA code and create the new 2FA token 77 | await db.twoFactorToken.delete({ 78 | where:{ 79 | id:twoFactorToken.id 80 | } 81 | }) 82 | 83 | const existingConfirmation = await getTwoFactorConfirmationByUserId(existingUser.id); 84 | if(existingConfirmation){ 85 | await db.twoFactorConfirmation.delete({ 86 | where:{ 87 | id:existingConfirmation.id 88 | } 89 | }) 90 | } 91 | //finally if we dont have any case we will create 92 | await db.twoFactorConfirmation.create({ 93 | data:{ 94 | userId:existingUser.id 95 | } 96 | }) 97 | } 98 | else{ 99 | const twoFactorToken = await generateTwoFactorToken(existingUser.email); 100 | // console.log({ 101 | // twoFactorToken 102 | // }) 103 | //Ok so 2FA code is coming, now we have to put it inside the box from frontend side 104 | 105 | await sendTwoFactorEmail( 106 | twoFactorToken.email, 107 | twoFactorToken.token 108 | ) 109 | return {twoFactor: true} 110 | } 111 | } 112 | 113 | 114 | 115 | try{ 116 | await signIn("credentials",{ 117 | email, 118 | password, 119 | redirectTo:callbackUrl || DEFAULT_LOGIN_REDIRECT, 120 | }) 121 | } 122 | catch(error){ 123 | if(error instanceof AuthError){ 124 | switch(error.type){ 125 | case "CredentialsSignin": 126 | return { 127 | error:"Invalid Credentials" 128 | } 129 | default: 130 | return{ 131 | error:"Something went wrong" 132 | } 133 | } 134 | } 135 | throw error; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /actions/logout.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { signOut } from "@/auth" 4 | 5 | export const logout = async() =>{ 6 | await signOut(); 7 | } -------------------------------------------------------------------------------- /actions/new-password.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { getPasswordResetTokenByToken } from "@/data/password-reset-token"; 3 | import { getUserByEmail } from "@/data/user"; 4 | import { db } from "@/lib/db" 5 | import { NewPasswordSchema } from "@/schema" 6 | import * as z from "zod"; 7 | import bcrypt from 'bcryptjs'; 8 | 9 | export const newPassword = async( 10 | values:z.infer, 11 | token?:string|null 12 | ) =>{ 13 | if(!token){ 14 | return{ 15 | error:"Missing token" 16 | } 17 | } 18 | 19 | const validatedFields = NewPasswordSchema.safeParse(values); 20 | if(!validatedFields.success){ 21 | return{ 22 | error:"Invalid Fields" 23 | } 24 | } 25 | 26 | const {password} = validatedFields.data; 27 | const existingToken = await getPasswordResetTokenByToken(token); 28 | if(!existingToken){ 29 | return{ 30 | error:"Token doesn't exist" 31 | } 32 | } 33 | 34 | //create new token 35 | const hasExpired = new Date(existingToken.expires) < new Date() 36 | if(hasExpired){ 37 | return{ 38 | error:"Token has expired" 39 | } 40 | } 41 | 42 | const existingUser = await getUserByEmail(existingToken.email); 43 | if(!existingUser){ 44 | return{ 45 | error:"Email does not exist" 46 | } 47 | } 48 | const hashPassword = await bcrypt.hash(password,10); 49 | 50 | await db.user.update({ 51 | where:{ 52 | id:existingUser.id, 53 | }, 54 | data:{ 55 | password:hashPassword 56 | } 57 | }) 58 | 59 | await db.passwordResetToken.delete({ 60 | where:{ 61 | id: existingToken.id 62 | } 63 | }) 64 | 65 | return { 66 | success:"Password updated" 67 | } 68 | } -------------------------------------------------------------------------------- /actions/new-verification.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@/lib/db" 4 | import { getUserByEmail } from "@/data/user"; 5 | import { getVerificationTokenByToken } from "@/data/verification-token"; 6 | 7 | export const NewVerification = async(token:string) =>{ 8 | const existingToken = await getVerificationTokenByToken(token); 9 | 10 | if(!existingToken){ 11 | return{ 12 | error:"Token doesn't exist" 13 | } 14 | } 15 | 16 | const hasExpired = new Date(existingToken.expires) < new Date(); 17 | if(hasExpired){ 18 | return{ 19 | error:"Token has expired" 20 | } 21 | } 22 | 23 | const existingUser = await getUserByEmail(existingToken.email); 24 | if(!existingUser){ 25 | return{ 26 | error:"Email doesnt exist" 27 | } 28 | } 29 | 30 | await db.user.update({ 31 | where:{ 32 | id:existingUser.id 33 | }, 34 | data:{ 35 | emailVerified: new Date(), 36 | email: existingToken.email 37 | } 38 | }) 39 | 40 | await db.verificationToken.delete({ 41 | where:{ 42 | id:existingToken.id 43 | } 44 | }) 45 | 46 | return{ 47 | success:"Email verified" 48 | } 49 | } -------------------------------------------------------------------------------- /actions/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/db"; 7 | import { getUserByEmail } from "@/data/user"; 8 | import { generateVerificationToken } from "@/lib/token"; 9 | import { sendVerificationEmail } from "@/lib/mail"; 10 | 11 | export const Register = async(values:z.infer) =>{ 12 | 13 | const validatedFields = RegisterSchema.safeParse(values); 14 | if(!validatedFields.success){ 15 | return{ 16 | error:"Invalid Fields" 17 | } 18 | } 19 | const {email,name,password,confirm_password} = validatedFields.data; 20 | 21 | if(confirm_password!==password){ 22 | return{ 23 | error:"Invalid Credentials" 24 | } 25 | } 26 | 27 | const hashedPassword = await bcrypt.hash(password,10); 28 | const hashedConfirmPassword = await bcrypt.hash(confirm_password,10); 29 | const existingUser = await getUserByEmail(email); 30 | 31 | if(existingUser){ 32 | return{ 33 | error:"Email Already exists" 34 | } 35 | } 36 | 37 | 38 | await db.user.create({ 39 | data:{ 40 | name, 41 | email, 42 | password:hashedPassword, 43 | confirm_password:hashedConfirmPassword, 44 | } 45 | }) 46 | 47 | const verificationToken = await generateVerificationToken(email); 48 | 49 | //sending verification email 50 | await sendVerificationEmail( 51 | verificationToken.email, 52 | verificationToken.token 53 | ) 54 | 55 | 56 | 57 | //If the details are validated then we have as 58 | return { 59 | success: "Confirmation email sent" 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /actions/reset.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import * as z from "zod"; 3 | import { db } from "@/lib/db"; 4 | import { ResetSchema } from "@/schema"; 5 | import { getUserByEmail } from "@/data/user"; 6 | import { generatePasswordResetToken } from "@/lib/token"; 7 | import { sendPasswordResetEmail } from "@/lib/mail"; 8 | 9 | export const reset = async(values:z.infer) => { 10 | const validatedFields = ResetSchema.safeParse(values) 11 | if(!validatedFields.success){ 12 | return{ 13 | error:"Invalid fields" 14 | } 15 | } 16 | //but if they are validated then 17 | const {email} = validatedFields.data; 18 | const existingUser = await getUserByEmail(email); 19 | if(!existingUser){ 20 | return{ 21 | error:"Email doesn't exist" 22 | } 23 | } 24 | //send the emails 25 | const passwordResetToken = await generatePasswordResetToken(email); 26 | await sendPasswordResetEmail( 27 | passwordResetToken.email, 28 | passwordResetToken.token 29 | ) 30 | 31 | return{ 32 | success:"Email sent!" 33 | } 34 | } -------------------------------------------------------------------------------- /actions/settings.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { getUserByEmail, getUserById } from "@/data/user"; 4 | import { getVerificationTokenByEmail } from "@/data/verification-token"; 5 | import { CurrentUser } from "@/lib/auth"; 6 | import { db } from "@/lib/db"; 7 | import { sendVerificationEmail } from "@/lib/mail"; 8 | import { generateVerificationToken } from "@/lib/token"; 9 | import { SettingsSchema } from "@/schema"; 10 | import { error } from "console"; 11 | import * as z from "zod"; 12 | import bcrypt from "bcryptjs"; 13 | 14 | export const settings = async(values:z.infer) =>{ 15 | // console.log("This takes the click") 16 | const user = await CurrentUser(); 17 | if(!user){ 18 | return{ 19 | error:"UnAuthorized" 20 | } 21 | } 22 | 23 | //also check whether it exists in db 24 | const dbUser = await getUserById(user.id); 25 | if(!dbUser){ 26 | return{ 27 | error:"UnAuthorized" 28 | } 29 | } 30 | 31 | //if user is OAuth we will show different settings to him 32 | if(user.isOAuth){ 33 | values.email = undefined 34 | values.password = undefined 35 | values.newPassword = undefined 36 | values.isTwoFactorEnabled = undefined 37 | } 38 | 39 | if(values.email && values.email!==user.email){ 40 | const existingUser = await getUserByEmail(values.email); 41 | if(existingUser && existingUser.id!==user.id){ 42 | return{ 43 | error:"Email Already in use!" 44 | } 45 | } 46 | const verificationToken = await generateVerificationToken(values.email); 47 | await sendVerificationEmail( 48 | verificationToken.email, 49 | verificationToken.token, 50 | ) 51 | return{ 52 | success:"Verification Email Sent!" 53 | } 54 | } 55 | 56 | if(values.password && values.newPassword && dbUser.password){ 57 | const passwordMatch = await bcrypt.compare( 58 | values.password, 59 | dbUser.password 60 | ) 61 | if(!passwordMatch){ 62 | return{ 63 | error:"Password didn't match" 64 | } 65 | } 66 | //store new password as a hash 67 | const passwordHash = await bcrypt.hash(values.newPassword,10) 68 | 69 | values.password = passwordHash; 70 | values.newPassword = undefined 71 | } 72 | 73 | 74 | //finally user does exist 75 | await db.user.update({ 76 | where:{ 77 | id:dbUser.id 78 | }, 79 | data:{ 80 | ...values 81 | } 82 | }) 83 | return{ 84 | success:"Settings Updated" 85 | } 86 | } -------------------------------------------------------------------------------- /app/(design)/heading-text.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neon-20/Authify/6abfe8c763dcc54a9d1d20c4ec4f7e9e816232c3/app/(design)/heading-text.tsx -------------------------------------------------------------------------------- /app/(design)/landing-start-button.tsx: -------------------------------------------------------------------------------- 1 | export const StartNowButton = () => { 2 | return ( 3 |
4 | 15 |
16 | 69 |
70 | ); 71 | } 72 | 73 | -------------------------------------------------------------------------------- /app/(design)/use-3dcard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CardBody, CardContainer, CardItem } from "@/components/3d-card"; 4 | import Image from "next/image"; 5 | import React from "react"; 6 | 7 | export function ThreeDCardDemo() { 8 | return ( 9 | 10 | 11 | 15 | Make things float in air 16 | 17 | 22 | Hover over this card to unleash the power of CSS perspective 23 | 24 | 25 | thumbnail 32 | 33 |
34 | 39 | Try now → 40 | 41 | 46 | Sign up 47 | 48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/(design)/use-sparkles.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import SparklesCore from '../../components/sparkles'; 4 | import { ThreeDCardDemo } from "./use-3dcard"; 5 | import { LoginButton } from "@/components/auth/login-button"; 6 | import { StartNowButton } from "./landing-start-button"; 7 | import Lottie from "lottie-react"; 8 | import animationData from "@/public/lottie/moving-file/Animation1.json"; 9 | import Link from "next/link"; 10 | import {motion} from "framer-motion"; 11 | import Logo from "../(marketing)/_components/logo"; 12 | import { Button } from "@/components/ui/button"; 13 | import { GithubIcon } from "lucide-react"; 14 | import { FaGithub } from "react-icons/fa"; 15 | 16 | export function SparklesPreview() { 17 | return ( 18 |
19 |
20 | 29 |
30 |
31 | 32 | 33 | {/* */} 34 | 43 | 44 |
45 |
46 | 51 | 52 | Authify - Quick Authentication Setup for Devs | Product Hunt 54 | 55 | 56 |
57 |
58 | 63 |
64 |
66 |
67 |

68 | Authify For Devs 69 |

70 | 🔐 71 |
72 |

73 | Authify presents a simple yet robust authentication system designed to meet the specific needs of developers. 👨🏻‍💻

74 |
75 | {/*
*/} 76 | 77 | {/* Can put mode="modal" if wanted later */} 78 | 79 | 80 | {/*
*/} 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /app/(marketing)/_components/logo.tsx: -------------------------------------------------------------------------------- 1 | // writing the logo component over here 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | 5 | const Logo = () => { 6 | return ( 7 |
9 | Logo 16 | 17 |

19 | Authify 20 |

21 | 22 |
23 | ); 24 | } 25 | 26 | export default Logo; -------------------------------------------------------------------------------- /app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | const MarketingLayout = ({ 2 | children 3 | }:{ 4 | children:React.ReactNode 5 | }) =>{ 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export default MarketingLayout; -------------------------------------------------------------------------------- /app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { SparklesPreview } from "../(design)/use-sparkles"; 2 | 3 | export default function Home() { 4 | return ( 5 | <> 6 | {/* */} 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/(protected)/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { UserButton } from "@/components/auth/user-button"; 3 | import { Button } from "@/components/ui/button"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | import {motion} from "framer-motion"; 7 | 8 | 9 | const NavBar = () => { 10 | const pathname = usePathname(); 11 | return ( 12 | 76 | ); 77 | } 78 | 79 | export default NavBar; -------------------------------------------------------------------------------- /app/(protected)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { admin } from "@/actions/admin"; 3 | import { RoleGate } from "@/components/auth/role-gate"; 4 | import { FormSuccess } from "@/components/form-success"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 7 | import { useCurrentRole } from "@/hooks/use-current-role"; 8 | import { UserRole } from "@prisma/client"; 9 | import { toast } from "sonner"; 10 | import {motion} from "framer-motion"; 11 | 12 | const AdminPage = () => { 13 | const onApiRouteClick = async() => { 14 | const response = await fetch("/api/admin") 15 | if(response.ok){ 16 | toast.success("Allowed API Route!") 17 | } 18 | else{ 19 | toast.error("Forbidden API Route!") 20 | } 21 | } 22 | 23 | const onServerActionClick = () => { 24 | admin() 25 | .then((data)=>{ 26 | if(data?.error){ 27 | toast.error("Forbidden Server Action!") 28 | } 29 | else{ 30 | toast.success("Allowed Server Action!") 31 | } 32 | }) 33 | } 34 | 35 | const role = useCurrentRole(); 36 | return ( 37 | 38 | 43 | 44 | 45 |

46 | Admin 🔑 47 |

48 |
49 | 50 |
51 | 52 | 53 | 54 |
56 |

ADMIN-only API Route

57 | 63 |
64 |
66 |

ADMIN-only Server Action

67 | 73 |
74 |
75 |
76 |
77 |
78 | 79 | ); 80 | } 81 | 82 | export default AdminPage; -------------------------------------------------------------------------------- /app/(protected)/client/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import UserInfo from "@/components/user-info"; 4 | import { useCurrentUser } from "@/hooks/use-current-user"; 5 | import {motion} from "framer-motion"; 6 | 7 | const ClientPage = () => { 8 | //fetch current user 9 | const user = useCurrentUser() 10 | return ( 11 | 16 | 20 | 21 | ); 22 | } 23 | 24 | export default ClientPage; -------------------------------------------------------------------------------- /app/(protected)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SparklesPreview2 } from "@/components/sparkles2"; 2 | import NavBar from "./_components/navbar"; 3 | 4 | const ProtectedLayout = ({ 5 | children 6 | }:{ 7 | children:React.ReactNode 8 | }) => { 9 | return ( 10 |
12 | 13 | 14 | {children} 15 | 16 |
17 | ); 18 | } 19 | 20 | export default ProtectedLayout; -------------------------------------------------------------------------------- /app/(protected)/server/page.tsx: -------------------------------------------------------------------------------- 1 | import UserInfo from "@/components/user-info"; 2 | import { CurrentUser } from "@/lib/auth"; 3 | 4 | //Todo: Wrap this component inside a client component 5 | 6 | const ServerPage = async() => { 7 | //fetch current user 8 | const user = await CurrentUser(); 9 | return ( 10 |
11 | 15 |
16 | ); 17 | } 18 | 19 | export default ServerPage; -------------------------------------------------------------------------------- /app/(protected)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | // import { auth, signOut } from "@/auth"; 2 | "use client" 3 | import { Button } from "@/components/ui/button"; 4 | import { useCurrentUser } from "@/hooks/use-current-user"; 5 | import {motion} from "framer-motion"; 6 | import { Asterisk } from 'lucide-react'; 7 | 8 | import{ 9 | Card, 10 | CardContent, 11 | CardDescription, 12 | CardHeader, 13 | CardTitle, 14 | } from "@/components/ui/card" 15 | import { settings } from "@/actions/settings"; 16 | import { useTransition,useState } from "react"; 17 | import { useSession } from "next-auth/react"; 18 | import * as z from "zod"; 19 | import {useForm} from "react-hook-form"; 20 | import { zodResolver } from "@hookform/resolvers/zod"; 21 | import { 22 | Form, 23 | FormControl, 24 | FormDescription, 25 | FormField, 26 | FormLabel, 27 | FormItem, 28 | FormMessage, 29 | } from "@/components/ui/form" 30 | import { SettingsSchema } from "@/schema"; 31 | import { Input } from "@/components/ui/input"; 32 | import { toast } from "sonner"; 33 | import { FormError } from "@/components/form-error"; 34 | import { FormSuccess } from "@/components/form-success"; 35 | import { admin } from '../../../actions/admin'; 36 | import {Switch} from "@/components/ui/switch" 37 | 38 | import { 39 | Select, 40 | SelectContent, 41 | SelectGroup, 42 | SelectItem, 43 | SelectLabel, 44 | SelectScrollDownButton, 45 | SelectScrollUpButton, 46 | SelectTrigger, 47 | SelectValue 48 | } from "@/components/ui/select" 49 | import { UserRole } from "@prisma/client"; 50 | import {IoSettingsOutline} from "react-icons/io5"; 51 | 52 | const SettingsPage = () =>{ 53 | const user = useCurrentUser(); 54 | const{update} = useSession(); 55 | const [isPending,startTransition] = useTransition(); 56 | const[error,setError] = useState(); 57 | const[success,setSuccess] = useState(); 58 | 59 | const form = useForm>({ 60 | resolver:zodResolver(SettingsSchema), 61 | defaultValues:{ 62 | name:user?.name || undefined, 63 | email:user?.email || undefined, 64 | password:undefined, 65 | newPassword:undefined, 66 | role:user?.role || undefined, 67 | isTwoFactorEnabled:user?.isTwoFactorEnabled|| undefined 68 | } 69 | }) 70 | 71 | 72 | const onSubmit = (values:z.infer) =>{ 73 | startTransition(()=>{ 74 | settings(values) 75 | .then((data)=>{ 76 | if(data.error){ 77 | setError(data.error) 78 | toast.error("Something went wrong while updating!") 79 | } 80 | if(data.success){ 81 | update() 82 | setSuccess(data.success) 83 | toast.success("Details got updated!") 84 | } 85 | }) 86 | .catch(()=>{ 87 | setError("Something went wrong") 88 | }) 89 | }) 90 | } 91 | 92 | return( 93 | 98 | 99 | 100 |

Settings 101 | 102 |

103 |
104 | 105 |
106 | 110 |
111 | ( 115 | 116 | 117 | Name 118 | 119 | 120 | 121 | 131 | 132 | 133 | 134 | )} 135 | /> 136 | {user?.isOAuth === false && ( 137 | <> 138 | ( 142 | 143 | Email 144 | 145 | 154 | 155 | 156 | 157 | )} 158 | /> 159 | ( 163 | 164 | Password 165 | 166 | 175 | 176 | 177 | 178 | )} 179 | /> 180 | ( 184 | 185 | New Password 186 | 187 | 196 | 197 | 198 | 199 | )} 200 | /> 201 | 202 | )} 203 | ( 207 | 208 | Role 209 | 232 | 233 | 234 | )} 235 | /> 236 | {user?.isOAuth === false && ( 237 | <> 238 | ( 242 | 245 |
246 | Two Factor Authentication 247 | 248 | Enable Two Factor Authentication for your account. 249 | 250 |
251 | 252 | 257 | 258 |
259 | )} 260 | /> 261 | 262 | )} 263 |
264 |
265 | 269 |
270 |
271 | 272 |
273 |
274 |
275 | ) 276 | } 277 | 278 | export default SettingsPage -------------------------------------------------------------------------------- /app/api/admin/route.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CurrentRole } from "@/lib/auth"; 3 | import { UserRole } from "@prisma/client"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function GET(){ 7 | const role = await CurrentRole(); 8 | if(role === UserRole.ADMIN){ 9 | return new NextResponse(null,{status:200}) 10 | } 11 | return new NextResponse(null,{status:403}) 12 | } -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | 2 | export {GET,POST} from "@/auth"; 3 | // export const runtime = "edge"; -------------------------------------------------------------------------------- /app/api/cron/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export async function GET() { 4 | return NextResponse.json({ ok: true }); 5 | } -------------------------------------------------------------------------------- /app/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from "@/components/auth/error-card"; 2 | import { notFound } from "next/navigation"; 3 | 4 | const ErrorPage = () => { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default ErrorPage; -------------------------------------------------------------------------------- /app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SparklesPreview2 } from "@/components/sparkles2"; 2 | 3 | const AuthLayout = ({ 4 | children 5 | }:{ 6 | children:React.ReactNode 7 | }) => { 8 | return ( 9 | <> 10 | 11 | {children} 12 | 13 | 14 | ); 15 | } 16 | 17 | export default AuthLayout; -------------------------------------------------------------------------------- /app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import {LoginForm} from "@/components/auth/login-form"; 2 | 3 | const LoginPage = () => { 4 | return ( 5 | 6 | ); 7 | } 8 | 9 | export default LoginPage; 10 | -------------------------------------------------------------------------------- /app/auth/new-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { NewPasswordForm } from "@/components/auth/new-password-form" 2 | 3 | const NewPassword = () =>{ 4 | return( 5 |
6 | 7 |
8 | ) 9 | } 10 | export default NewPassword -------------------------------------------------------------------------------- /app/auth/new-verification/page.tsx: -------------------------------------------------------------------------------- 1 | import { NewVerificationForm } from "@/components/auth/new-verification-form" 2 | 3 | const NewVerificationPage = () =>{ 4 | return( 5 |
6 | 7 |
8 | ) 9 | } 10 | export default NewVerificationPage -------------------------------------------------------------------------------- /app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { RegisterForm } from "@/components/auth/register-form"; 2 | 3 | const RegisterPage = () => { 4 | return ( 5 | 6 | ); 7 | } 8 | 9 | export default RegisterPage; 10 | -------------------------------------------------------------------------------- /app/auth/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetForm } from "@/components/auth/reset-form"; 2 | 3 | 4 | const ResetPassword = () => { 5 | return( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default ResetPassword; -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neon-20/Authify/6abfe8c763dcc54a9d1d20c4ec4f7e9e816232c3/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 | 12 | @layer base { 13 | :root { 14 | --background: 0 0% 100%; 15 | --foreground: 222.2 84% 4.9%; 16 | 17 | --card: 0 0% 100%; 18 | --card-foreground: 222.2 84% 4.9%; 19 | 20 | --popover: 0 0% 100%; 21 | --popover-foreground: 222.2 84% 4.9%; 22 | 23 | --primary: 222.2 47.4% 11.2%; 24 | --primary-foreground: 210 40% 98%; 25 | 26 | --secondary: 210 40% 96.1%; 27 | --secondary-foreground: 222.2 47.4% 11.2%; 28 | 29 | --muted: 210 40% 96.1%; 30 | --muted-foreground: 215.4 16.3% 46.9%; 31 | 32 | --accent: 210 40% 96.1%; 33 | --accent-foreground: 222.2 47.4% 11.2%; 34 | 35 | --destructive: 0 84.2% 60.2%; 36 | --destructive-foreground: 210 40% 98%; 37 | 38 | --border: 214.3 31.8% 91.4%; 39 | --input: 214.3 31.8% 91.4%; 40 | --ring: 222.2 84% 4.9%; 41 | 42 | --radius: 0.5rem; 43 | } 44 | 45 | .dark { 46 | --background: 222.2 84% 4.9%; 47 | --foreground: 210 40% 98%; 48 | 49 | --card: 222.2 84% 4.9%; 50 | --card-foreground: 210 40% 98%; 51 | 52 | --popover: 222.2 84% 4.9%; 53 | --popover-foreground: 210 40% 98%; 54 | 55 | --primary: 210 40% 98%; 56 | --primary-foreground: 222.2 47.4% 11.2%; 57 | 58 | --secondary: 217.2 32.6% 17.5%; 59 | --secondary-foreground: 210 40% 98%; 60 | 61 | --muted: 217.2 32.6% 17.5%; 62 | --muted-foreground: 215 20.2% 65.1%; 63 | 64 | --accent: 217.2 32.6% 17.5%; 65 | --accent-foreground: 210 40% 98%; 66 | 67 | --destructive: 0 62.8% 30.6%; 68 | --destructive-foreground: 210 40% 98%; 69 | 70 | --border: 217.2 32.6% 17.5%; 71 | --input: 217.2 32.6% 17.5%; 72 | --ring: 212.7 26.8% 83.9%; 73 | } 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | @apply bg-background text-foreground; 82 | } 83 | } 84 | body{ 85 | margin: 0%; 86 | padding: 0%; 87 | background-color: black; 88 | } 89 | 90 | 91 | .sparkle-button { 92 | --active: 0; 93 | --bg: radial-gradient( 94 | 40% 50% at center 100%, 95 | hsl(270 calc(var(--active) * 97%) 72% / var(--active)), 96 | transparent 97 | ), 98 | radial-gradient( 99 | 80% 100% at center 120%, 100 | hsl(260 calc(var(--active) * 97%) 70% / var(--active)), 101 | transparent 102 | ), 103 | hsl(260 calc(var(--active) * 97%) calc((var(--active) * 44%) + 12%)); 104 | background: var(--bg); 105 | font-size: 1.2rem; 106 | font-weight: 500; 107 | border: 0; 108 | cursor: pointer; 109 | padding: 0.7em 1.1em; 110 | display: flex; 111 | align-items: center; 112 | gap: 0.25em; 113 | white-space: nowrap; 114 | border-radius: 100px; 115 | position: relative; 116 | box-shadow: 0 0 calc(var(--active) * 3em) calc(var(--active) * 1em) hsl(260 97% 61% / 0.75), 117 | 0 0em 0 0 hsl(260 calc(var(--active) * 97%) calc((var(--active) * 50%) + 30%)) inset, 118 | 0 -0.05em 0 0 hsl(260 calc(var(--active) * 97%) calc(var(--active) * 60%)) inset; 119 | transition: box-shadow var(--transition), scale var(--transition), background var(--transition); 120 | scale: calc(1 + (var(--active) * 0.1)); 121 | transition: .5s; 122 | } 123 | 124 | .sparkle-button:active { 125 | scale: 1; 126 | transition: .3s; 127 | } 128 | 129 | .sparkle path { 130 | color: hsl(0 0% calc((var(--active, 0) * 70%) + var(--base))); 131 | transform-box: fill-box; 132 | transform-origin: center; 133 | fill: currentColor; 134 | stroke: currentColor; 135 | animation-delay: calc((var(--transition) * 1.5) + (var(--delay) * 1s)); 136 | animation-duration: 0.6s; 137 | transition: color var(--transition); 138 | } 139 | 140 | .sparkle-button:is(:hover, :focus-visible) path { 141 | animation-name: bounce; 142 | } 143 | 144 | @keyframes bounce { 145 | 35%, 65% { 146 | scale: var(--scale); 147 | } 148 | } 149 | 150 | .sparkle path:nth-of-type(1) { 151 | --scale: 0.5; 152 | --delay: 0.1; 153 | --base: 40%; 154 | } 155 | 156 | .sparkle path:nth-of-type(2) { 157 | --scale: 1.5; 158 | --delay: 0.2; 159 | --base: 20%; 160 | } 161 | 162 | .sparkle path:nth-of-type(3) { 163 | --scale: 2.5; 164 | --delay: 0.35; 165 | --base: 30%; 166 | } 167 | 168 | .sparkle-button:before { 169 | content: ""; 170 | position: absolute; 171 | inset: -0.2em; 172 | z-index: -1; 173 | border: 0.25em solid hsl(260 97% 50% / 0.5); 174 | border-radius: 100px; 175 | opacity: var(--active, 0); 176 | transition: opacity var(--transition); 177 | } 178 | 179 | .spark { 180 | position: absolute; 181 | inset: 0; 182 | border-radius: 100px; 183 | rotate: 0deg; 184 | overflow: hidden; 185 | mask: linear-gradient(white, transparent 50%); 186 | animation: flip calc(var(--spark) * 2) infinite steps(2, end); 187 | } 188 | 189 | @keyframes flip { 190 | to { 191 | rotate: 360deg; 192 | } 193 | } 194 | 195 | .spark:before { 196 | content: ""; 197 | position: absolute; 198 | width: 200%; 199 | aspect-ratio: 1; 200 | top: 0%; 201 | left: 50%; 202 | z-index: -1; 203 | translate: -50% -15%; 204 | rotate: 0; 205 | transform: rotate(-90deg); 206 | opacity: calc((var(--active)) + 0.4); 207 | background: conic-gradient( 208 | from 0deg, 209 | transparent 0 340deg, 210 | white 360deg 211 | ); 212 | transition: opacity var(--transition); 213 | animation: rotate var(--spark) linear infinite both; 214 | } 215 | 216 | .spark:after { 217 | content: ""; 218 | position: absolute; 219 | inset: var(--cut); 220 | border-radius: 100px; 221 | } 222 | 223 | .backdrop { 224 | position: absolute; 225 | inset: var(--cut); 226 | background: var(--bg); 227 | border-radius: 100px; 228 | transition: background var(--transition); 229 | } 230 | 231 | @keyframes rotate { 232 | to { 233 | transform: rotate(90deg); 234 | } 235 | } 236 | 237 | @supports(selector(:has(:is(+ *)))) { 238 | body:has(button:is(:hover, :focus-visible)) { 239 | --active: 1; 240 | --play-state: running; 241 | } 242 | 243 | .bodydrop { 244 | display: none; 245 | } 246 | } 247 | 248 | .sparkle-button:is(:hover, :focus-visible) ~ :is(.bodydrop, .particle-pen) { 249 | --active: 1; 250 | --play-state: runnin; 251 | } 252 | 253 | .sparkle-button:is(:hover, :focus-visible) { 254 | --active: 1; 255 | --play-state: running; 256 | } 257 | 258 | .sp { 259 | position: relative; 260 | } 261 | 262 | .particle-pen { 263 | position: absolute; 264 | width: 200%; 265 | aspect-ratio: 1; 266 | top: 50%; 267 | left: 50%; 268 | translate: -50% -50%; 269 | -webkit-mask: radial-gradient(white, transparent 65%); 270 | z-index: -1; 271 | opacity: var(--active, 0); 272 | transition: opacity var(--transition); 273 | } 274 | 275 | .particle { 276 | fill: white; 277 | width: calc(var(--size, 0.25) * 1rem); 278 | aspect-ratio: 1; 279 | position: absolute; 280 | top: calc(var(--y) * 1%); 281 | left: calc(var(--x) * 1%); 282 | opacity: var(--alpha, 1); 283 | animation: float-out calc(var(--duration, 1) * 1s) calc(var(--delay) * -1s) infinite linear; 284 | transform-origin: var(--origin-x, 1000%) var(--origin-y, 1000%); 285 | z-index: -1; 286 | animation-play-state: var(--play-state, paused); 287 | } 288 | 289 | .particle path { 290 | fill: hsl(0 0% 90%); 291 | stroke: none; 292 | } 293 | 294 | .particle:nth-of-type(even) { 295 | animation-direction: reverse; 296 | } 297 | 298 | @keyframes float-out { 299 | to { 300 | rotate: 360deg; 301 | } 302 | } 303 | 304 | .text { 305 | translate: 2% -6%; 306 | letter-spacing: 0.01ch; 307 | background: linear-gradient(90deg, hsl(0 0% calc((var(--active) * 100%) + 65%)), hsl(0 0% calc((var(--active) * 100%) + 26%))); 308 | -webkit-background-clip: text; 309 | color: transparent; 310 | transition: background var(--transition); 311 | } 312 | 313 | .sparkle-button svg { 314 | inline-size: 1.25em; 315 | translate: -25% -5%; 316 | } 317 | 318 | .Btn { 319 | width: 140px; 320 | height: 35px; 321 | display: flex; 322 | align-items: center; 323 | justify-content: flex-start; 324 | border: none; 325 | border-radius: 5px; 326 | overflow: hidden; 327 | box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.089); 328 | cursor: pointer; 329 | background-color: transparent; 330 | } 331 | 332 | .leftContainer { 333 | width: 60%; 334 | height: 100%; 335 | background-color: rgb(123, 94, 197); 336 | display: flex; 337 | align-items: center; 338 | justify-content: center; 339 | gap: 8px; 340 | } 341 | 342 | .leftContainer .like { 343 | color: white; 344 | font-weight: 600; 345 | } 346 | 347 | .likeCount { 348 | width: 40%; 349 | height: 100%; 350 | display: flex; 351 | align-items: center; 352 | justify-content: center; 353 | color: rgb(238, 0, 0); 354 | font-weight: 600; 355 | position: relative; 356 | background-color: white; 357 | } 358 | 359 | .likeCount::before { 360 | height: 8px; 361 | width: 8px; 362 | position: absolute; 363 | content: ""; 364 | background-color: rgb(255, 255, 255); 365 | transform: rotate(45deg); 366 | left: -4px; 367 | } 368 | 369 | .Btn:hover .leftContainer { 370 | background-color: rgb(219, 0, 0); 371 | } 372 | 373 | .Btn:active .leftContainer { 374 | background-color: rgb(201, 0, 0); 375 | } 376 | 377 | .Btn:active .leftContainer svg { 378 | transform: scale(1.15); 379 | transform-origin: top; 380 | } 381 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Inter } from 'next/font/google' 3 | import './globals.css' 4 | import { ThemeProvider } from "@/components/theme-provider" 5 | import { Toaster, toast } from 'sonner' 6 | import { auth } from '@/auth' 7 | import { SessionProvider } from 'next-auth/react' 8 | import { Analytics } from '@vercel/analytics/react'; 9 | import { SpeedInsights } from "@vercel/speed-insights/next" 10 | 11 | 12 | 13 | const inter = Inter({ subsets: ['latin'] }) 14 | 15 | export const metadata: Metadata = { 16 | title: 'Authify', 17 | description: 'One Source for all authentication requirements', 18 | } 19 | 20 | export default async function RootLayout({ 21 | children, 22 | }: { 23 | children: React.ReactNode 24 | }) { 25 | const session = await auth(); 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 38 | {children} 39 | 40 | 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from "next-auth"; 2 | import Credentials from "next-auth/providers/credentials" 3 | 4 | import { LoginSchema } from "./schema"; 5 | import { getUserByEmail } from "./data/user"; 6 | import bcrypt from "bcryptjs" 7 | import Github from "next-auth/providers/github"; 8 | import Google from "next-auth/providers/google"; 9 | 10 | export default { 11 | providers: [ 12 | Github({ 13 | clientId:process.env.GITHUB_CLIENT_ID, 14 | clientSecret: process.env.GITHUB_CLIENT_SECRET 15 | }), 16 | Google({ 17 | clientId:process.env.GOOGLE_CLIENT_ID, 18 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 19 | }), 20 | Credentials({ 21 | async authorize(credentials){ 22 | const validatedFields = LoginSchema.safeParse(credentials) 23 | if(validatedFields.success){ 24 | const {email,password} = validatedFields.data; 25 | 26 | const user = await getUserByEmail(email); 27 | if(!user || !user.password){ 28 | return null; 29 | } 30 | const passwordMatch = await bcrypt.compare( 31 | password, 32 | user.password 33 | ); 34 | if(passwordMatch){ 35 | return user; 36 | } 37 | } 38 | return null; 39 | } 40 | }) 41 | ], 42 | } satisfies NextAuthConfig -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth" 2 | import { PrismaAdapter } from "@auth/prisma-adapter" 3 | import { PrismaClient, UserRole } from "@prisma/client" 4 | import authConfig from "./auth.config" 5 | import { db } from "./lib/db" 6 | import { getUserById } from './data/user'; 7 | import { getTwoFactorConfirmationByUserId } from "./data/two-factor-confirmation" 8 | import { getAccountByUserId } from "./data/account" 9 | 10 | const prisma = new PrismaClient() 11 | 12 | export const { 13 | handlers:{GET,POST}, 14 | auth, 15 | signIn, 16 | signOut 17 | } = NextAuth({ 18 | pages:{ 19 | signIn:"/auth/login", 20 | error:"/auth/error" 21 | }, 22 | //automatically populate the email-verified fields if we login with Oauth 23 | events:{ 24 | async linkAccount({user}){ 25 | await db.user.update({ 26 | where:{ 27 | id:user.id, 28 | }, 29 | data:{ 30 | emailVerified: new Date() 31 | } 32 | }) 33 | } 34 | }, 35 | 36 | //match for second checking. 37 | 38 | callbacks:{ 39 | //Just trying to block myself from signing in xD if I am not verified 40 | async signIn({user,account}){ 41 | // console.log({ 42 | // user, 43 | // account 44 | // }) 45 | 46 | //Allow OAuth without email verification 47 | if(account?.provider!=="credentials") return true; 48 | 49 | const existingUser = await getUserById(user.id); 50 | 51 | //prevent signIn without email verification 52 | if(!existingUser?.emailVerified) return false; 53 | 54 | //do 2FA callback 55 | if(existingUser.isTwoFactorEnabled && existingUser.email){ 56 | const twoFactorConfirmation = await getTwoFactorConfirmationByUserId(existingUser.id) 57 | if(!twoFactorConfirmation){ 58 | return false; 59 | } 60 | await db.twoFactorConfirmation.delete({ 61 | where:{ 62 | id:twoFactorConfirmation.id 63 | } 64 | }) 65 | } 66 | 67 | return true; 68 | }, 69 | 70 | async session({token,session}){ 71 | if(token.sub && session.user){ 72 | session.user.id = token.sub 73 | } 74 | if(token.role && session.user){ 75 | session.user.role = token.role as UserRole 76 | } 77 | 78 | if(session.user){ 79 | session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean 80 | } 81 | 82 | if(session.user){ 83 | session.user.name = token.name 84 | session.user.email = token.email 85 | session.user.isOAuth = token.isOAuth as boolean 86 | } 87 | return session 88 | }, 89 | 90 | async jwt({token}){ 91 | if(!token.sub){ 92 | return token 93 | } 94 | const existingUser = await getUserById(token.sub); 95 | if(!existingUser){ 96 | return token 97 | } 98 | 99 | const existingAccount = await getAccountByUserId(existingUser.id) 100 | 101 | token.isOAuth = !!existingAccount 102 | token.name = existingUser.name 103 | token.email = existingUser.email 104 | token.role = existingUser.role 105 | token.isTwoFactorEnabled = existingUser.isTwoFactorEnabled; 106 | return token 107 | } 108 | 109 | }, 110 | adapter: PrismaAdapter(db), 111 | session: { strategy: "jwt" }, 112 | ...authConfig, 113 | }) -------------------------------------------------------------------------------- /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.js", 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/3d-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import Image from "next/image"; 5 | import React, { 6 | createContext, 7 | useState, 8 | useContext, 9 | useRef, 10 | useEffect, 11 | } from "react"; 12 | 13 | interface CardContainerProps{ 14 | children: React.ReactNode 15 | className?:string 16 | containerClassName?:string 17 | } 18 | 19 | const MouseEnterContext = createContext< 20 | [boolean, React.Dispatch>] | undefined 21 | >(undefined); 22 | 23 | export const CardContainer = ({ 24 | children, 25 | className, 26 | containerClassName, 27 | }:CardContainerProps) => { 28 | const containerRef = useRef(null); 29 | const [isMouseEntered, setIsMouseEntered] = useState(false); 30 | 31 | const handleMouseMove = (e: React.MouseEvent) => { 32 | if (!containerRef.current) return; 33 | const { left, top, width, height } = 34 | containerRef.current.getBoundingClientRect(); 35 | const x = (e.clientX - left - width / 2) / 25; 36 | const y = (e.clientY - top - height / 2) / 25; 37 | containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`; 38 | }; 39 | 40 | const handleMouseEnter = (e: React.MouseEvent) => { 41 | setIsMouseEntered(true); 42 | if (!containerRef.current) return; 43 | }; 44 | 45 | const handleMouseLeave = (e: React.MouseEvent) => { 46 | if (!containerRef.current) return; 47 | setIsMouseEntered(false); 48 | containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`; 49 | }; 50 | return ( 51 | 52 |
61 |
74 | {children} 75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | interface CardBodyProps{ 82 | children: React.ReactNode 83 | className?:string 84 | } 85 | 86 | export const CardBody = ({ 87 | children, 88 | className, 89 | }: CardBodyProps) => { 90 | return ( 91 |
*]:[transform-style:preserve-3d]", 94 | className 95 | )} 96 | > 97 | {children} 98 |
99 | ); 100 | }; 101 | 102 | export const CardItem = ({ 103 | as: Tag = "div", 104 | children, 105 | className, 106 | translateX = 0, 107 | translateY = 0, 108 | translateZ = 0, 109 | rotateX = 0, 110 | rotateY = 0, 111 | rotateZ = 0, 112 | ...rest 113 | }: { 114 | as?: React.ElementType; 115 | children: React.ReactNode; 116 | className?: string; 117 | translateX?: number | string; 118 | translateY?: number | string; 119 | translateZ?: number | string; 120 | rotateX?: number | string; 121 | rotateY?: number | string; 122 | rotateZ?: number | string; 123 | }) => { 124 | const ref = useRef(null); 125 | const [isMouseEntered] = useMouseEnter(); 126 | 127 | useEffect(() => { 128 | handleAnimations(); 129 | }, [isMouseEntered]); 130 | 131 | const handleAnimations = () => { 132 | if (!ref.current) return; 133 | if (isMouseEntered) { 134 | ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`; 135 | } else { 136 | ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`; 137 | } 138 | }; 139 | 140 | return ( 141 | 146 | {children} 147 | 148 | ); 149 | }; 150 | 151 | // Create a hook to use the context 152 | export const useMouseEnter = () => { 153 | const context = useContext(MouseEnterContext); 154 | if (context === undefined) { 155 | throw new Error("useMouseEnter must be used within a MouseEnterProvider"); 156 | } 157 | return context; 158 | }; 159 | -------------------------------------------------------------------------------- /components/auth/back-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { Button } from "../ui/button"; 4 | import { BiArrowBack } from "react-icons/bi"; 5 | 6 | 7 | interface BackButtonProps{ 8 | label:string, 9 | href:string 10 | } 11 | 12 | export const BackButton = ({ 13 | label, 14 | href 15 | }:BackButtonProps) =>{ 16 | return( 17 | 28 | ) 29 | } -------------------------------------------------------------------------------- /components/auth/card-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardFooter, CardHeader } from "../ui/card"; 4 | import { BackButton } from "./back-button"; 5 | import { HeaderSection } from "./header"; 6 | import { Social } from "./social"; 7 | import {motion} from "framer-motion"; 8 | 9 | interface CardWrapperProps{ 10 | children:React.ReactNode 11 | headerLabel:string 12 | backButtonlabel:string 13 | backButtonHref:string 14 | showSocial?:boolean 15 | } 16 | 17 | export const CardWrapper = ({ 18 | children, 19 | headerLabel, 20 | backButtonlabel, 21 | backButtonHref, 22 | showSocial 23 | }:CardWrapperProps) => { 24 | return ( 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | {showSocial && ( 39 | 40 | 41 | 42 | )} 43 | 44 | 48 | 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /components/auth/error-card.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardFooter, CardHeader } from "../ui/card" 2 | import { BackButton } from './back-button'; 3 | import { HeaderSection } from './header'; 4 | import { FaExclamationTriangle } from 'react-icons/fa'; 5 | 6 | 7 | export const ErrorCard = () =>{ 8 | return( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 |
25 | ) 26 | } -------------------------------------------------------------------------------- /components/auth/header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import { Poppins } from "next/font/google" 3 | 4 | const font = Poppins({ 5 | subsets:["latin"], 6 | weight:["600"] 7 | }) 8 | 9 | interface HeaderSectionProps{ 10 | label:string 11 | } 12 | 13 | export const HeaderSection = ({ 14 | label 15 | }:HeaderSectionProps) =>{ 16 | return( 17 |
19 |
20 |

21 | Authify 22 |

23 | 🔐 24 |
25 |

26 | {label} 27 |

28 |
29 | ) 30 | } -------------------------------------------------------------------------------- /components/auth/login-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import{ 4 | Dialog, 5 | DialogClose, 6 | DialogContent, 7 | DialogDescription, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle, 11 | DialogTrigger 12 | } from "@/components/ui/dialog"; 13 | import { LoginForm } from "./login-form"; 14 | 15 | interface LoginButtonProps{ 16 | children:React.ReactNode, 17 | mode?:"modal" | "redirect" 18 | asChild?:boolean; 19 | } 20 | export const LoginButton = ({ 21 | children, 22 | mode="redirect", 23 | asChild 24 | }:LoginButtonProps ) => { 25 | const router = useRouter(); 26 | 27 | const onClick = () =>{ 28 | router.push("/auth/login") 29 | } 30 | 31 | if(mode === "modal"){ 32 | return( 33 | 34 | {children} 35 | 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | return ( 44 | 45 | {children} 46 | 47 | ); 48 | } 49 | 50 | 51 | // //Only for checking whether the current user is logged in or not 52 | 53 | 54 | //auth wale button ko wrap krne k liye 55 | 56 | -------------------------------------------------------------------------------- /components/auth/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CardWrapper } from "./card-wrapper"; 3 | 4 | import {useForm} from "react-hook-form"; 5 | import {zodResolver} from "@hookform/resolvers/zod"; 6 | import * as z from "zod"; 7 | import {LoginSchema} from "@/schema"; 8 | import { useSearchParams } from "next/navigation"; 9 | import {motion} from "framer-motion"; 10 | 11 | import { 12 | Form, 13 | FormControl, 14 | FormDescription, 15 | FormField, 16 | FormItem, 17 | FormLabel, 18 | FormMessage 19 | } from "@/components/ui/form" 20 | 21 | import { Input } from "../ui/input"; 22 | import { Button } from "../ui/button"; 23 | import { FormError } from "../form-error"; 24 | import { FormSuccess } from "../form-success"; 25 | import { Login } from "@/actions/login"; 26 | import { useState, useTransition, useEffect } from 'react'; 27 | import Link from "next/link"; 28 | 29 | export const LoginForm = () => { 30 | const searchParams = useSearchParams(); 31 | const callbackUrl = searchParams.get("callbackUrl") 32 | const urlError = searchParams.get("error") === "OAuthAccountNotLinked" ? 33 | "Email already in use with different provider!" : "" 34 | const[error,setError] = useState(""); 35 | const[success,setSuccess] = useState(""); 36 | const[isPending,startTransition] = useTransition(); 37 | const[showTwoFactor,setShowTwoFactor] = useState(false); 38 | const[isClient,setIsClient] = useState(false) 39 | 40 | const form = useForm>({ 41 | resolver:zodResolver(LoginSchema), 42 | defaultValues:{ 43 | email:"", 44 | password:"" 45 | } 46 | }) 47 | 48 | useEffect(()=>{ 49 | setIsClient(true); 50 | },[]) 51 | 52 | if(!isClient){ 53 | return null; 54 | } 55 | 56 | const onSubmit = (values:z.infer) => { 57 | startTransition(()=>{ 58 | Login(values,callbackUrl). 59 | then((data) => { 60 | if(data?.error){ 61 | form.reset(); 62 | setError(data?.error) 63 | } 64 | if(data?.success){ 65 | form.reset(); 66 | setSuccess(data?.success) 67 | } 68 | if(data?.twoFactor){ 69 | setShowTwoFactor(true); 70 | } 71 | }) 72 | .catch(() => setError("Something went wrong")) 73 | }) 74 | } 75 | 76 | return ( 77 | 83 |
84 | 87 | {showTwoFactor && ( 88 | ( 92 | 93 | 94 | Two Factor Code 95 | 96 | 97 | 106 | 107 | 108 | 109 | )} 110 | /> 111 | )} 112 | {!showTwoFactor && (<> 113 | ( 117 | 118 | 119 | Email 120 | 121 | 122 | 131 | 132 | 133 | 134 | )} 135 | /> 136 | ( 140 | 141 | 142 | Password 143 | 144 | 145 | 154 | 155 | 165 | 166 | 167 | )} 168 | /> 169 | )} 170 | {/* */} 171 | 172 | 173 | {showTwoFactor && ( 174 | )} 190 | 195 | {!showTwoFactor && ( 196 | )} 212 | 213 | 214 | 215 |
216 | ); 217 | } 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /components/auth/logout-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { logout } from "@/actions/logout"; 4 | 5 | interface LogOutButtonProps{ 6 | children:React.ReactNode, 7 | } 8 | 9 | export const LogoutButton = ({ 10 | children, 11 | }:LogOutButtonProps) =>{ 12 | const onClick = () =>{ 13 | logout(); 14 | } 15 | 16 | return( 17 | 18 | {children} 19 | 20 | ) 21 | } -------------------------------------------------------------------------------- /components/auth/new-password-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CardWrapper } from "./card-wrapper"; 3 | 4 | import {useForm} from "react-hook-form"; 5 | import {zodResolver} from "@hookform/resolvers/zod"; 6 | import * as z from "zod"; 7 | import {NewPasswordSchema} from "@/schema"; 8 | import { useSearchParams } from "next/navigation"; 9 | 10 | import { 11 | Form, 12 | FormControl, 13 | FormDescription, 14 | FormField, 15 | FormItem, 16 | FormLabel, 17 | FormMessage 18 | } from "@/components/ui/form" 19 | 20 | import { Input } from "../ui/input"; 21 | import { Button } from "../ui/button"; 22 | import { FormError } from "../form-error"; 23 | import { FormSuccess } from "../form-success"; 24 | import { useState, useTransition } from "react"; 25 | import { newPassword } from "@/actions/new-password"; 26 | import {useRouter} from "next/navigation" 27 | 28 | export const NewPasswordForm = () => { 29 | const searchParams = useSearchParams(); 30 | const token = searchParams.get("token"); 31 | const[error,setError] = useState(""); 32 | const[success,setSuccess] = useState(""); 33 | const[isPending,startTransition] = useTransition(); 34 | const form = useForm>({ 35 | resolver:zodResolver(NewPasswordSchema), 36 | defaultValues:{ 37 | password:"", 38 | } 39 | }) 40 | const router = useRouter(); 41 | 42 | const onSubmit = (values:z.infer) => { 43 | setError(""); 44 | setSuccess(""); 45 | startTransition(()=>{ 46 | newPassword(values,token). 47 | then((data) => { 48 | setError(data?.error); 49 | setSuccess(data?.success) 50 | setTimeout(()=>{ 51 | router.push("/auth/login") 52 | },1000) 53 | }) 54 | .catch((error)=>{ 55 | setError("Something went wrong") 56 | }) 57 | }) 58 | } 59 | 60 | return ( 61 | 66 |
67 | 70 | ( 74 | 75 | 76 | Password 77 | 78 | 79 | 88 | 89 | 90 | 91 | )} 92 | /> 93 | {/* */} 94 | 95 | 96 | 112 | 113 | 114 |
115 | ); 116 | } 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /components/auth/new-verification-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CardWrapper } from "./card-wrapper"; 3 | import {BeatLoader} from "react-spinners"; 4 | import { useEffect,useCallback, useState } from "react"; 5 | import { useSearchParams } from "next/navigation"; 6 | import { NewVerification } from "@/actions/new-verification"; 7 | import { FormSuccess } from "../form-success"; 8 | import { FormError } from "../form-error"; 9 | import { useRouter } from "next/navigation"; 10 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; 11 | 12 | export const NewVerificationForm = () =>{ 13 | const searchParams = useSearchParams(); 14 | const token = searchParams.get("token"); 15 | const [error,setError] = useState(""); 16 | const [success,setSuccess] = useState(""); 17 | 18 | // console.log(token); 19 | const onSubmit = useCallback(()=>{ 20 | if(success || error) return; 21 | if(!token){ 22 | setError("Missing Token") 23 | return 24 | } 25 | NewVerification(token) 26 | .then((data) => { 27 | setSuccess(data.success) 28 | setError(data.error) 29 | }) 30 | .catch(()=>{ 31 | setError("Something went wrong") 32 | }) 33 | },[token,success,error]); 34 | 35 | useEffect(()=>{ 36 | onSubmit(); 37 | },[onSubmit]) 38 | 39 | 40 | 41 | return( 42 | 47 |
48 | {!success && !error && ( 49 | 50 | )} 51 | 52 | {!success && ( 53 | 54 | )} 55 |
56 |
57 | ) 58 | } -------------------------------------------------------------------------------- /components/auth/register-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CardWrapper } from "./card-wrapper"; 3 | 4 | import {useForm} from "react-hook-form"; 5 | import {zodResolver} from "@hookform/resolvers/zod"; 6 | import * as z from "zod"; 7 | import {RegisterSchema} from "@/schema"; 8 | import {motion} from "framer-motion"; 9 | 10 | import { 11 | Form, 12 | FormControl, 13 | FormDescription, 14 | FormField, 15 | FormItem, 16 | FormLabel, 17 | FormMessage 18 | } from "@/components/ui/form" 19 | import { Input } from "../ui/input"; 20 | import { Button } from "../ui/button"; 21 | import { FormError } from "../form-error"; 22 | import { FormSuccess } from "../form-success"; 23 | import { Register } from "@/actions/register"; 24 | import { useState, useTransition } from "react"; 25 | 26 | export const RegisterForm = () => { 27 | const[error,setError] = useState(""); 28 | const[success,setSuccess] = useState(""); 29 | const[isPending,startTransition] = useTransition(); 30 | const form = useForm>({ 31 | resolver:zodResolver(RegisterSchema), 32 | defaultValues:{ 33 | email:"", 34 | password:"", 35 | confirm_password:"", 36 | name:"" 37 | } 38 | }) 39 | 40 | const onSubmit = (values:z.infer) => { 41 | setError(""); 42 | setSuccess(""); 43 | startTransition(()=>{ 44 | Register(values). 45 | then((data) => { 46 | setError(data.error); 47 | setSuccess(data.success) 48 | }) 49 | }) 50 | } 51 | 52 | return ( 53 | 59 |
60 | 63 | ( 67 | 68 | 69 | Email 70 | 71 | 72 | 81 | 82 | 83 | 84 | )} 85 | /> 86 | ( 90 | 91 | 92 | Name 93 | 94 | 95 | 104 | 105 | 106 | 107 | )} 108 | /> 109 | ( 113 | 114 | 115 | Password 116 | 117 | 118 | 127 | 128 | 129 | 130 | )} 131 | /> 132 | ( 136 | 137 | 138 | Confirm Password 139 | 140 | 141 | 150 | 151 | 152 | 153 | )} 154 | /> 155 | {/* */} 156 | 157 | 158 | 163 | 180 | 181 | 182 | 183 |
184 | ); 185 | } 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /components/auth/reset-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CardWrapper } from "./card-wrapper"; 3 | 4 | import {useForm} from "react-hook-form"; 5 | import {zodResolver} from "@hookform/resolvers/zod"; 6 | import * as z from "zod"; 7 | import {ResetSchema} from "@/schema"; 8 | import { useSearchParams } from "next/navigation"; 9 | 10 | import { 11 | Form, 12 | FormControl, 13 | FormDescription, 14 | FormField, 15 | FormItem, 16 | FormLabel, 17 | FormMessage 18 | } from "@/components/ui/form" 19 | 20 | import { Input } from "../ui/input"; 21 | import { Button } from "../ui/button"; 22 | import { FormError } from "../form-error"; 23 | import { FormSuccess } from "../form-success"; 24 | import { Login } from "@/actions/login"; 25 | import { useState, useTransition } from "react"; 26 | import Link from "next/link"; 27 | import { reset } from "@/actions/reset"; 28 | 29 | export const ResetForm = () => { 30 | const[error,setError] = useState(""); 31 | const[success,setSuccess] = useState(""); 32 | const[isPending,startTransition] = useTransition(); 33 | const form = useForm>({ 34 | resolver:zodResolver(ResetSchema), 35 | defaultValues:{ 36 | email:"", 37 | } 38 | }) 39 | 40 | const onSubmit = (values:z.infer) => { 41 | setError(""); 42 | setSuccess(""); 43 | startTransition(()=>{ 44 | reset(values). 45 | then((data) => { 46 | setError(data?.error); 47 | setSuccess(data?.success) 48 | }) 49 | }) 50 | } 51 | 52 | return ( 53 | 58 |
59 | 62 | 63 | ( 67 | 68 | 69 | Email 70 | 71 | 72 | 81 | 82 | 83 | 84 | )} 85 | /> 86 | {/* */} 87 | 88 | 89 | 105 | 106 | 107 |
108 | ); 109 | } 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /components/auth/role-gate.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useCurrentRole } from "@/hooks/use-current-role"; 3 | import { UserRole } from "@prisma/client"; 4 | import { FormError } from "../form-error"; 5 | 6 | interface RoleGateProps{ 7 | children:React.ReactNode, 8 | allowedRole:UserRole 9 | } 10 | 11 | export const RoleGate = ({ 12 | children, 13 | allowedRole 14 | }:RoleGateProps) =>{ 15 | const role = useCurrentRole(); 16 | if(role!==allowedRole){ 17 | return( 18 | 19 | ) 20 | } 21 | 22 | return( 23 | <> 24 | {children} 25 | 26 | ) 27 | } -------------------------------------------------------------------------------- /components/auth/social.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "../ui/button"; 4 | import {FcGoogle} from "react-icons/fc"; 5 | import { FaGithub } from "react-icons/fa"; 6 | import { signIn } from "next-auth/react"; 7 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; 8 | import {motion} from "framer-motion"; 9 | import { useSearchParams } from 'next/navigation'; 10 | 11 | 12 | export const Social = () => { 13 | const searchParams = useSearchParams(); 14 | const callbackUrl = searchParams.get("callbackUrl"); 15 | const onClick = (providers:"google" | "github") => { 16 | signIn(providers,{ 17 | callbackUrl:callbackUrl || DEFAULT_LOGIN_REDIRECT, 18 | }) 19 | } 20 | 21 | return( 22 |
23 | 28 | 36 | 37 | 42 | 50 | 51 |
52 | ) 53 | } -------------------------------------------------------------------------------- /components/auth/user-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { LogOut } from 'lucide-react'; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuGroup, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu"; 12 | import { 13 | Avatar, 14 | AvatarImage, 15 | AvatarFallback 16 | } from "@/components/ui/avatar"; 17 | import { FaUser } from "react-icons/fa"; 18 | import { Button } from "../ui/button"; 19 | import { useCurrentUser } from "@/hooks/use-current-user"; 20 | import { LogoutButton } from "./logout-button"; 21 | 22 | 23 | export const UserButton = () =>{ 24 | const user = useCurrentUser(); 25 | return( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | LogOut 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | 47 | -------------------------------------------------------------------------------- /components/email-template.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface EmailTemplateProps { 4 | firstName: string; 5 | } 6 | 7 | export const EmailTemplate: React.FC> = ({ 8 | firstName, 9 | }) => ( 10 |
11 |

Welcome, {firstName}!

12 |
13 | ); 14 | -------------------------------------------------------------------------------- /components/form-error.tsx: -------------------------------------------------------------------------------- 1 | import { FaExclamation, FaExclamationTriangle } from "react-icons/fa"; 2 | 3 | 4 | interface FormErrorProps{ 5 | message?:string 6 | } 7 | 8 | export const FormError = ({ 9 | message, 10 | }:FormErrorProps) =>{ 11 | if(!message) return null; 12 | 13 | return( 14 |
16 | 17 |

{message}

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

{message}

18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /components/sparkles.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { NextPage } from "next"; 3 | import React from "react"; 4 | import { useEffect, useState } from "react"; 5 | import Particles, { initParticlesEngine } from "@tsparticles/react"; 6 | import type { Container, Engine } from "@tsparticles/engine"; 7 | import { loadSlim } from "@tsparticles/slim"; 8 | import { cn } from "@/lib/utils"; 9 | import { motion, useAnimation } from "framer-motion"; 10 | 11 | type ParticlesProps = { 12 | id?: string; 13 | className?: string; 14 | background?: string; 15 | particleSize?: number; 16 | minSize?: number; 17 | maxSize?: number; 18 | speed?: number; 19 | particleColor?: string; 20 | particleDensity?: number; 21 | }; 22 | const SparklesCore = (props: ParticlesProps) => { 23 | const { 24 | id, 25 | className, 26 | background, 27 | minSize, 28 | maxSize, 29 | speed, 30 | particleColor, 31 | particleDensity, 32 | } = props; 33 | const [init, setInit] = useState(false); 34 | useEffect(() => { 35 | initParticlesEngine(async (engine) => { 36 | await loadSlim(engine); 37 | }).then(() => { 38 | setInit(true); 39 | }); 40 | }, []); 41 | const controls = useAnimation(); 42 | 43 | const particlesLoaded = async (container?: Container) => { 44 | if (container) { 45 | console.log(container); 46 | controls.start({ 47 | opacity: 1, 48 | transition: { 49 | duration: 1, 50 | }, 51 | }); 52 | } 53 | }; 54 | 55 | return ( 56 | 57 | {init && ( 58 | 432 | )} 433 | 434 | ); 435 | }; 436 | 437 | export default SparklesCore; 438 | -------------------------------------------------------------------------------- /components/sparkles2.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import SparklesCore from "./sparkles"; 4 | interface SparklesPreviewProps{ 5 | children:React.ReactNode 6 | } 7 | 8 | export function SparklesPreview2({ 9 | children, 10 | }:SparklesPreviewProps) { 11 | return ( 12 |
13 |
14 | 23 |
24 | {children} 25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /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-full 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 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 hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /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/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 |

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

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

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /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 { X } from "lucide-react" 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 | DialogClose, 116 | DialogTrigger, 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 { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /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 |