├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── bun.lockb ├── components.json ├── next.config.js ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── favicon.svg └── images │ ├── auth │ ├── github.svg │ └── google.svg │ ├── error.svg │ └── success.svg ├── src ├── actions │ ├── login.ts │ └── register.ts ├── app │ ├── (protected) │ │ └── settings │ │ │ └── page.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextAuth] │ │ │ └── route.ts │ ├── auth │ │ ├── layout.tsx │ │ ├── login │ │ │ └── page.tsx │ │ └── register │ │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── auth.config.ts ├── auth.ts ├── components │ ├── auth │ │ ├── back-button.tsx │ │ ├── card-wrapper.tsx │ │ ├── header.tsx │ │ ├── login-button.tsx │ │ ├── login-form.tsx │ │ ├── register-form.tsx │ │ └── social.tsx │ ├── form-error.tsx │ ├── form-success.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ └── label.tsx ├── db │ └── types.ts ├── lib │ └── utils.ts ├── middleware.ts ├── routes.ts ├── schema │ └── index.ts └── server │ ├── database.ts │ └── service │ └── user.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | .vscode 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowPares": "always", 3 | "bracketSameLine": false, 4 | "bracketSpacing": true, 5 | "embeddedLanguageFormatting": "auto", 6 | "endOfLine": "lf", 7 | "htmlWhitespaceSensitivity": "css", 8 | "insertPragma": false, 9 | "jsxBracketSameLine": false, 10 | "jsxSingleQuote": false, 11 | "printWidth": 80, 12 | "proseWrap": "preserve", 13 | "quoteProps": "as-needed", 14 | "requirePragma": false, 15 | "semi": true, 16 | "singleQuote": false, 17 | "tabWidth": 2, 18 | "trailingComma": "es5", 19 | "useTabs": false, 20 | "vueIndentScriptAndStyle": false, 21 | "importOrder": [ 22 | "(.*)/__mocks__/(.*)", 23 | "", 24 | "^@lib/(.*)$", 25 | "^@components/(.*)$", 26 | "^~/(.*)$", 27 | "^[./]" 28 | ], 29 | "importOrderSeparation": true 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 imskanand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next Auth 2 | 3 | Key Features: 4 | 5 | - 🔐 Next-auth v5 (Auth.js) 6 | - 🚀 Next.js 14 with server actions 7 | - 🔑 Credentials Provider 8 | - 🌐 OAuth Provider (Social login with Google & GitHub) 9 | - 🔒 Forgot password functionality 10 | - ✉️ Email verification 11 | - 📱 Two factor verification 12 | - 👥 User roles (Admin & User) 13 | - 🔓 Login component (Opens in redirect or modal) 14 | - 📝 Register component 15 | - 🤔 Forgot password component 16 | - ✅ Verification component 17 | - ⚠️ Error component 18 | - 🔘 Login button 19 | - 🚪 Logout button 20 | - 🚧 Role Gate 21 | - 🔍 Exploring next.js middleware 22 | - 📈 Extending & Exploring next-auth session 23 | - 🔄 Exploring next-auth callbacks 24 | - 👤 useCurrentUser hook 25 | - 🛂 useRole hook 26 | - 🧑 currentUser utility 27 | - 👮 currentRole utility 28 | - 🖥️ Example with server component 29 | - 💻 Example with client component 30 | - 👑 Render content for admins using RoleGate component 31 | - 🛡️ Protect API Routes for admins only 32 | - 🔐 Protect Server Actions for admins only 33 | - 📧 Change email with new verification in Settings page 34 | - 🔑 Change password with old password confirmation in Settings page 35 | - 🔔 Enable/disable two-factor auth in Settings page 36 | - 🔄 Change user role in Settings page (for development purposes only) 37 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShubhamkumarAnand/next-auth/d27a5614907d207e9fd656bb7815406fe5e6b41e/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-auth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:generate": "prisma generate", 11 | "db:push": "prisma db push", 12 | "db:migrate": "prisma migrate dev --name", 13 | "db:studio": "bunx prisma studio" 14 | }, 15 | "dependencies": { 16 | "@auth/kysely-adapter": "^0.1.13", 17 | "@hookform/resolvers": "^3.3.4", 18 | "@radix-ui/react-dialog": "^1.0.5", 19 | "@radix-ui/react-icons": "^1.3.0", 20 | "@radix-ui/react-label": "^2.0.2", 21 | "@radix-ui/react-slot": "^1.0.2", 22 | "bcrypt": "^5.1.1", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.0", 25 | "kysely": "^0.27.2", 26 | "next": "14.0.4", 27 | "next-auth": "beta", 28 | "pg": "^8.11.3", 29 | "prisma-kysely": "^1.7.1", 30 | "react": "^18", 31 | "react-dom": "^18", 32 | "react-hook-form": "^7.49.2", 33 | "tailwind-merge": "^2.2.0", 34 | "tailwindcss-animate": "^1.0.7", 35 | "zod": "^3.22.4" 36 | }, 37 | "devDependencies": { 38 | "@types/bcrypt": "^5.0.2", 39 | "@types/node": "^20", 40 | "@types/pg": "^8.10.9", 41 | "@types/react": "^18", 42 | "@types/react-dom": "^18", 43 | "autoprefixer": "^10.0.1", 44 | "eslint": "^8", 45 | "eslint-config-next": "14.0.4", 46 | "postcss": "^8", 47 | "prisma": "^5.7.1", 48 | "tailwindcss": "^3.3.0", 49 | "typescript": "^5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-kysely" 3 | output = "../src/db" 4 | fileName = "types.ts" 5 | enumFileName = "enums.ts" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | directUrl = env("DIRECT_URL") 12 | } 13 | 14 | model Account { 15 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 16 | userId String @db.Uuid 17 | type String 18 | provider String 19 | providerAccountId String 20 | refresh_token String? @db.Text 21 | access_token String? @db.Text 22 | expires_at Int? 23 | token_type String? 24 | scope String? 25 | id_token String? @db.Text 26 | session_state String? 27 | 28 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 29 | 30 | @@unique([provider, providerAccountId]) 31 | } 32 | 33 | model User { 34 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid 35 | name String? 36 | email String? @unique 37 | emailVerified DateTime? 38 | image String? 39 | password String? 40 | accounts Account[] 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/auth/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/auth/google.svg: -------------------------------------------------------------------------------- 1 | Google-color Created with Sketch. -------------------------------------------------------------------------------- /public/images/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/actions/login.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import * as z from "zod"; 4 | import { LoginSchema } from "~/schema"; 5 | 6 | export const login = async (values: z.infer) => { 7 | const validatedSchemas = LoginSchema.safeParse(values); 8 | if (!validatedSchemas) return { error: "Invalid Fields!" }; 9 | return { success: "Logged In Successfully!" }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/actions/register.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import * as z from "zod"; 4 | import bcrypt from "bcrypt"; 5 | import { RegisterSchema } from "~/schema"; 6 | import { db } from "~/server/database"; 7 | import { getUserByEmail } from "~/server/service/user"; 8 | 9 | export const register = async (values: z.infer) => { 10 | const validatedFields = RegisterSchema.safeParse(values); 11 | if (!validatedFields.success) { 12 | return { error: "Invalids Fields!" }; 13 | } 14 | 15 | const { name, email, password } = validatedFields.data; 16 | 17 | const existingUser = await getUserByEmail(email); 18 | if (existingUser.find((user) => user.email === email)) 19 | return { error: "User already exists, Try Different Email!" }; 20 | 21 | const hashedPassword = await bcrypt.hash(password, 10); 22 | await db 23 | .insertInto("User") 24 | .values({ 25 | email: email, 26 | password: hashedPassword, 27 | name: name, 28 | }) 29 | .returning("id") 30 | .execute(); 31 | 32 | // TODO: send verification email token 33 | return { success: "User Created" }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/(protected)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "~/auth"; 2 | 3 | const SettingsPage = async () => { 4 | const sessions = await auth(); 5 | return ( 6 | <> 7 |

Settings Page

8 | {JSON.stringify(sessions)} 9 | 10 | ); 11 | }; 12 | 13 | export default SettingsPage; 14 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextAuth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "~/auth"; 2 | -------------------------------------------------------------------------------- /src/app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | interface AuthLayoutProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | const AuthLayout: FC> = ({ children }) => { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | }; 14 | 15 | export default AuthLayout; 16 | -------------------------------------------------------------------------------- /src/app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import LoginForm from "~/components/auth/login-form"; 3 | 4 | interface LoginPageProps {} 5 | 6 | const LoginPage: FC> = ({}) => { 7 | return ; 8 | }; 9 | 10 | export default LoginPage; 11 | -------------------------------------------------------------------------------- /src/app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { RegisterForm } from "~/components/auth/register-form"; 2 | 3 | const RegisterPage = () => { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default RegisterPage; 12 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, body{ 6 | height: 100%; 7 | } 8 | @layer base { 9 | :root { 10 | --background: 0 0% 100%; 11 | --foreground: 222.2 84% 4.9%; 12 | 13 | --card: 0 0% 100%; 14 | --card-foreground: 222.2 84% 4.9%; 15 | 16 | --popover: 0 0% 100%; 17 | --popover-foreground: 222.2 84% 4.9%; 18 | 19 | --primary: 222.2 47.4% 11.2%; 20 | --primary-foreground: 210 40% 98%; 21 | 22 | --secondary: 210 40% 96.1%; 23 | --secondary-foreground: 222.2 47.4% 11.2%; 24 | 25 | --muted: 210 40% 96.1%; 26 | --muted-foreground: 215.4 16.3% 46.9%; 27 | 28 | --accent: 210 40% 96.1%; 29 | --accent-foreground: 222.2 47.4% 11.2%; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --border: 214.3 31.8% 91.4%; 35 | --input: 214.3 31.8% 91.4%; 36 | --ring: 222.2 84% 4.9%; 37 | 38 | --radius: 0.5rem; 39 | } 40 | 41 | .dark { 42 | --background: 222.2 84% 4.9%; 43 | --foreground: 210 40% 98%; 44 | 45 | --card: 222.2 84% 4.9%; 46 | --card-foreground: 210 40% 98%; 47 | 48 | --popover: 222.2 84% 4.9%; 49 | --popover-foreground: 210 40% 98%; 50 | 51 | --primary: 210 40% 98%; 52 | --primary-foreground: 222.2 47.4% 11.2%; 53 | 54 | --secondary: 217.2 32.6% 17.5%; 55 | --secondary-foreground: 210 40% 98%; 56 | 57 | --muted: 217.2 32.6% 17.5%; 58 | --muted-foreground: 215 20.2% 65.1%; 59 | 60 | --accent: 217.2 32.6% 17.5%; 61 | --accent-foreground: 210 40% 98%; 62 | 63 | --destructive: 0 62.8% 30.6%; 64 | --destructive-foreground: 210 40% 98%; 65 | 66 | --border: 217.2 32.6% 17.5%; 67 | --input: 217.2 32.6% 17.5%; 68 | --ring: 212.7 26.8% 83.9%; 69 | } 70 | } 71 | 72 | @layer base { 73 | * { 74 | @apply border-border; 75 | } 76 | body { 77 | @apply bg-background text-foreground; 78 | } 79 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Next Auth", 9 | description: "End to End Authentication using Next_Auth", 10 | icons: { 11 | icon: "/favicon.svg", 12 | }, 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginButton } from "~/components/auth/login-button"; 2 | import { Button } from "~/components/ui/button"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |

9 | 🔐 Auth 10 |

11 |
12 |

A simple authentication service

13 |
14 | 15 | 18 | 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from "next-auth"; 2 | import Github from "next-auth/providers/github"; 3 | import Google from "next-auth/providers/google"; 4 | 5 | export default { 6 | providers: [ 7 | Google({ 8 | clientId: process.env.GOOGLE_CLIENT_ID, 9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 10 | }), 11 | Github({ 12 | clientId: process.env.GITHUB_CLIENT_ID, 13 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 14 | }), 15 | ], 16 | } satisfies NextAuthConfig; 17 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import authConfig from "~/auth.config"; 3 | 4 | export const { 5 | handlers: { GET, POST }, 6 | auth, 7 | } = NextAuth({ 8 | session: { strategy: "jwt" }, 9 | ...authConfig, 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/auth/back-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import type { FC } from "react"; 5 | import { Button } from "~/components/ui/button"; 6 | 7 | interface BackButtonProps { 8 | href: string; 9 | label: string; 10 | } 11 | 12 | const BackButton: FC> = ({ href, label }) => { 13 | return ( 14 | 17 | ); 18 | }; 19 | 20 | export default BackButton; 21 | -------------------------------------------------------------------------------- /src/components/auth/card-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { FC } from "react"; 3 | import BackButton from "~/components/auth/back-button"; 4 | import { Header } from "~/components/auth/header"; 5 | import Social from "~/components/auth/social"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardFooter, 10 | CardHeader, 11 | } from "~/components/ui/card"; 12 | 13 | interface CardWrapperProps { 14 | children: React.ReactNode; 15 | headerLabel: string; 16 | backButtonLabel: string; 17 | backButtonHref: string; 18 | showSocial?: boolean; 19 | } 20 | 21 | const CardWrapper: FC> = ({ 22 | children, 23 | headerLabel, 24 | backButtonLabel, 25 | backButtonHref, 26 | showSocial, 27 | }) => { 28 | return ( 29 | 30 | 31 |
32 | 33 | {children} 34 | {showSocial && ( 35 | 36 | 37 | 38 | )} 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default CardWrapper; 47 | -------------------------------------------------------------------------------- /src/components/auth/header.tsx: -------------------------------------------------------------------------------- 1 | import { Poppins } from "next/font/google"; 2 | 3 | import { cn } from "~/lib/utils"; 4 | 5 | const font = Poppins({ 6 | subsets: ["latin"], 7 | weight: ["600"], 8 | }); 9 | 10 | interface HeaderProps { 11 | label: string; 12 | } 13 | 14 | export const Header = ({ label }: HeaderProps) => { 15 | return ( 16 |
17 |

🔐 Auth

18 |

{label}

19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/auth/login-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import LoginForm from "~/components/auth/login-form"; 5 | 6 | import { Dialog, DialogContent, DialogTrigger } from "~/components/ui/dialog"; 7 | 8 | interface LoginButtonProps { 9 | children: React.ReactNode; 10 | mode?: "modal" | "redirect"; 11 | asChild?: boolean; 12 | } 13 | 14 | export const LoginButton = ({ 15 | children, 16 | mode = "redirect", 17 | asChild, 18 | }: LoginButtonProps) => { 19 | const router = useRouter(); 20 | 21 | const onClick = () => { 22 | router.push("/auth/login"); 23 | }; 24 | 25 | if (mode === "modal") { 26 | return ( 27 | 28 | {children} 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/auth/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useForm } from "react-hook-form"; 3 | import * as z from "zod"; 4 | 5 | import { LoginSchema } from "~/schema"; 6 | 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormLabel, 13 | FormMessage, 14 | } from "~/components/ui/form"; 15 | import { Button } from "~/components/ui/button"; 16 | import FormError from "~/components/form-error"; 17 | import FormSuccess from "~/components/form-success"; 18 | import CardWrapper from "~/components/auth/card-wrapper"; 19 | import { zodResolver } from "@hookform/resolvers/zod"; 20 | import { Input } from "~/components/ui/input"; 21 | import { login } from "~/actions/login"; 22 | import { useState, useTransition } from "react"; 23 | 24 | const LoginForm = () => { 25 | const [error, setError] = useState(""); 26 | const [success, setSuccess] = useState(""); 27 | 28 | const [isPending, startTransition] = useTransition(); 29 | const form = useForm>({ 30 | resolver: zodResolver(LoginSchema), 31 | defaultValues: { 32 | email: "", 33 | password: "", 34 | }, 35 | }); 36 | 37 | const onSubmit = (values: z.infer) => { 38 | setError(""); 39 | setSuccess(""); 40 | startTransition(() => { 41 | login(values).then((data) => { 42 | setError(data.error); 43 | setSuccess(data.success); 44 | }); 45 | }); 46 | }; 47 | return ( 48 | 54 |
55 | 56 |
57 | ( 61 | 62 | Email 63 | 64 | 70 | 71 | 72 | 73 | )} 74 | /> 75 | ( 79 | 80 | Password 81 | 82 | 88 | 89 | 90 | 91 | )} 92 | /> 93 |
94 | 95 | 96 | 99 | 100 | 101 |
102 | ); 103 | }; 104 | 105 | export default LoginForm; 106 | -------------------------------------------------------------------------------- /src/components/auth/register-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { useState, useTransition } from "react"; 5 | import { useForm } from "react-hook-form"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | 8 | import { RegisterSchema } from "~/schema"; 9 | import { Input } from "~/components/ui/input"; 10 | import { 11 | Form, 12 | FormControl, 13 | FormField, 14 | FormItem, 15 | FormLabel, 16 | FormMessage, 17 | } from "~/components/ui/form"; 18 | import CardWrapper from "~/components/auth/card-wrapper"; 19 | import { Button } from "~/components/ui/button"; 20 | import FormError from "~/components/form-error"; 21 | import FormSuccess from "~/components/form-success"; 22 | import { register } from "~/actions/register"; 23 | 24 | export const RegisterForm = () => { 25 | const [error, setError] = useState(""); 26 | const [success, setSuccess] = useState(""); 27 | const [isPending, startTransition] = useTransition(); 28 | 29 | const form = useForm>({ 30 | resolver: zodResolver(RegisterSchema), 31 | defaultValues: { 32 | email: "", 33 | password: "", 34 | name: "", 35 | }, 36 | }); 37 | 38 | const onSubmit = (values: z.infer) => { 39 | setError(""); 40 | setSuccess(""); 41 | 42 | startTransition(() => { 43 | register(values).then((data) => { 44 | setError(data.error); 45 | setSuccess(data.success); 46 | }); 47 | }); 48 | }; 49 | 50 | return ( 51 | 57 |
58 | 59 |
60 | ( 64 | 65 | Name 66 | 67 | 72 | 73 | 74 | 75 | )} 76 | /> 77 | ( 81 | 82 | Email 83 | 84 | 90 | 91 | 92 | 93 | )} 94 | /> 95 | ( 99 | 100 | Password 101 | 102 | 108 | 109 | 110 | 111 | )} 112 | /> 113 |
114 | 115 | 116 | 119 | 120 | 121 |
122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /src/components/auth/social.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { Button } from "~/components/ui/button"; 3 | 4 | const Social = () => { 5 | // const searchParams = useSearchParams(); 6 | // const callbackUrl = searchParams.get("callbackUrl"); 7 | 8 | const onClick = (provider: "google" | "github") => { 9 | console.log("Hello All"); 10 | }; 11 | 12 | return ( 13 |
14 | 28 | 42 |
43 | ); 44 | }; 45 | 46 | export default Social; 47 | -------------------------------------------------------------------------------- /src/components/form-error.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import type { FC } from "react"; 3 | 4 | interface FormErrorProps { 5 | message?: string; 6 | } 7 | 8 | const FormError: FC> = ({ message }) => { 9 | if (!message) return null; 10 | return ( 11 |
12 | error icon 19 |

{message}

20 |
21 | ); 22 | }; 23 | 24 | export default FormError; 25 | -------------------------------------------------------------------------------- /src/components/form-success.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import type { FC } from "react"; 3 | 4 | interface FormSuccessProps { 5 | message?: string; 6 | } 7 | 8 | const FormSuccess: FC> = ({ message }) => { 9 | if (!message) return null; 10 | return ( 11 |
12 | error icon 19 |

{message}

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

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

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

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "~/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /src/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 |