├── .env copy ├── .eslintrc.json ├── .gitignore ├── README.md ├── actions └── auth-action.ts ├── app ├── (auth) │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── (protected) │ ├── admin │ │ └── page.tsx │ └── dashboard │ │ └── page.tsx ├── api │ └── auth │ │ ├── [...nextauth] │ │ └── route.ts │ │ └── verify-email │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── auth.config.ts ├── auth.ts ├── components.json ├── components ├── button-social.tsx ├── form-login.tsx ├── form-register.tsx ├── logout-button.tsx └── ui │ ├── button.tsx │ ├── form.tsx │ ├── input.tsx │ └── label.tsx ├── lib ├── db.ts ├── mail.ts ├── utils.ts └── zod.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── types └── next-auth.d.ts /.env copy: -------------------------------------------------------------------------------- 1 | # 🔥 ESTE ARCHIVO DEBE LLAMARSE .env 🔥 2 | # 🔥 NO OLVIDAR AGREGAR EL ARCHIVO .env AL .gitignore 🔥 3 | AUTH_SECRET="" 4 | 5 | DATABASE_URL="postgresql://postgres:root@localhost:5432/auth_yt?schema=public" 6 | 7 | AUTH_RESEND_KEY="" 8 | 9 | NEXTAUTH_URL="http://localhost:3000" 10 | 11 | # Credenciales de autenticación video #02 12 | AUTH_GOOGLE_ID= 13 | AUTH_GOOGLE_SECRET= 14 | 15 | AUTH_GITHUB_ID= 16 | AUTH_GITHUB_SECRET= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /actions/auth-action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { signIn } from "@/auth"; 4 | import { db } from "@/lib/db"; 5 | import { loginSchema, registerSchema } from "@/lib/zod"; 6 | import bcrypt from "bcryptjs"; 7 | import { AuthError } from "next-auth"; 8 | import { z } from "zod"; 9 | 10 | export const loginAction = async (values: z.infer) => { 11 | try { 12 | await signIn("credentials", { 13 | email: values.email, 14 | password: values.password, 15 | redirect: false, 16 | }); 17 | return { success: true }; 18 | } catch (error) { 19 | if (error instanceof AuthError) { 20 | return { error: error.cause?.err?.message }; 21 | } 22 | return { error: "error 500" }; 23 | } 24 | }; 25 | 26 | export const registerAction = async ( 27 | values: z.infer 28 | ) => { 29 | try { 30 | const { data, success } = registerSchema.safeParse(values); 31 | if (!success) { 32 | return { 33 | error: "Invalid data", 34 | }; 35 | } 36 | 37 | // verificar si el usuario ya existe 38 | const user = await db.user.findUnique({ 39 | where: { 40 | email: data.email, 41 | }, 42 | include: { 43 | accounts: true, // Incluir las cuentas asociadas 44 | }, 45 | }); 46 | 47 | if (user) { 48 | // Verificar si tiene cuentas OAuth vinculadas 49 | const oauthAccounts = user.accounts.filter( 50 | (account) => account.type === "oauth" 51 | ); 52 | if (oauthAccounts.length > 0) { 53 | return { 54 | error: 55 | "To confirm your identity, sign in with the same account you used originally.", 56 | }; 57 | } 58 | return { 59 | error: "User already exists", 60 | }; 61 | } 62 | 63 | // hash de la contraseña 64 | const passwordHash = await bcrypt.hash(data.password, 10); 65 | 66 | // crear el usuario 67 | await db.user.create({ 68 | data: { 69 | email: data.email, 70 | name: data.name, 71 | password: passwordHash, 72 | }, 73 | }); 74 | 75 | await signIn("credentials", { 76 | email: data.email, 77 | password: data.password, 78 | redirect: false, 79 | }); 80 | 81 | return { success: true }; 82 | } catch (error) { 83 | if (error instanceof AuthError) { 84 | return { error: error.cause?.err?.message }; 85 | } 86 | return { error: "error 500" }; 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode; 5 | }>) => { 6 | return
{children}
; 7 | }; 8 | export default AuthLayout; 9 | -------------------------------------------------------------------------------- /app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import FormLogin from "@/components/form-login"; 2 | 3 | const LoginPage = ({ 4 | searchParams, 5 | }: { 6 | searchParams: { verified: string; error: string }; 7 | }) => { 8 | const isVerified = searchParams.verified === "true"; 9 | const OAuthAccountNotLinked = searchParams.error === "OAuthAccountNotLinked"; 10 | 11 | return ( 12 | 16 | ); 17 | }; 18 | export default LoginPage; 19 | -------------------------------------------------------------------------------- /app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import FormRegister from "@/components/form-register"; 2 | 3 | const RegisterPage = () => { 4 | return ; 5 | }; 6 | export default RegisterPage; 7 | -------------------------------------------------------------------------------- /app/(protected)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | import LogoutButton from "@/components/logout-button"; 3 | 4 | const AdminPage = async () => { 5 | const session = await auth(); 6 | 7 | console.log(session); 8 | 9 | if (session?.user?.role !== "admin") { 10 | return
You are not admin
; 11 | } 12 | 13 | return ( 14 |
15 |
{JSON.stringify(session, null, 2)}
16 | 17 |
18 | ); 19 | }; 20 | export default AdminPage; 21 | -------------------------------------------------------------------------------- /app/(protected)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | import LogoutButton from "@/components/logout-button"; 3 | 4 | export default async function DashboardPage() { 5 | const session = await auth(); 6 | 7 | if (!session) { 8 | return
Not authenticated
; 9 | } 10 | 11 | return ( 12 |
13 |
{JSON.stringify(session, null, 2)}
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth"; // Referring to the auth.ts we just created 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /app/api/auth/verify-email/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { redirect } from "next/navigation"; 3 | import { type NextRequest } from "next/server"; 4 | 5 | export async function GET(request: NextRequest) { 6 | const searchParams = request.nextUrl.searchParams; 7 | const token = searchParams.get("token"); 8 | 9 | if (!token) { 10 | return new Response("Token not found", { status: 400 }); 11 | } 12 | 13 | // verificar si existe un token en la base de datos 14 | const verifyToken = await db.verificationToken.findFirst({ 15 | where: { 16 | token, 17 | }, 18 | }); 19 | 20 | if (!verifyToken) { 21 | return new Response("Token not found", { status: 400 }); 22 | } 23 | 24 | // verificar si el token ya expiró 25 | if (verifyToken.expires < new Date()) { 26 | return new Response("Token expired", { status: 400 }); 27 | } 28 | 29 | // verificar si el email ya esta verificado 30 | const user = await db.user.findUnique({ 31 | where: { 32 | email: verifyToken.identifier, 33 | }, 34 | }); 35 | 36 | if (user?.emailVerified) { 37 | return new Response("Email already verified", { status: 400 }); 38 | } 39 | 40 | // marcar el email como verificado 41 | await db.user.update({ 42 | where: { 43 | email: verifyToken.identifier, 44 | }, 45 | data: { 46 | emailVerified: new Date(), 47 | }, 48 | }); 49 | 50 | // eliminar el token 51 | await db.verificationToken.delete({ 52 | where: { 53 | identifier: verifyToken.identifier, 54 | }, 55 | }); 56 | 57 | // return Response.json({ token }); 58 | redirect("/login?verified=true"); 59 | } 60 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluuweb/example-next-auth-v5/1bff70ebb9bff09c37f4843de6279210f7e415a0/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | const HomePage = () => { 2 | return
HomePage
; 3 | }; 4 | export default HomePage; 5 | -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { loginSchema } from "@/lib/zod"; 3 | import bcrypt from "bcryptjs"; 4 | import { nanoid } from "nanoid"; 5 | import type { NextAuthConfig } from "next-auth"; 6 | import Credentials from "next-auth/providers/credentials"; 7 | import { sendEmailVerification } from "./lib/mail"; 8 | 9 | import GitHub from "next-auth/providers/github"; 10 | import Google from "next-auth/providers/google"; 11 | 12 | // Notice this is only an object, not a full Auth.js instance 13 | export default { 14 | providers: [ 15 | Google, 16 | GitHub, 17 | Credentials({ 18 | authorize: async (credentials) => { 19 | const { data, success } = loginSchema.safeParse(credentials); 20 | 21 | if (!success) { 22 | throw new Error("Invalid credentials"); 23 | } 24 | 25 | // verificar si existe el usuario en la base de datos 26 | const user = await db.user.findUnique({ 27 | where: { 28 | email: data.email, 29 | }, 30 | }); 31 | 32 | if (!user || !user.password) { 33 | throw new Error("No user found"); 34 | } 35 | 36 | // verificar si la contraseña es correcta 37 | const isValid = await bcrypt.compare(data.password, user.password); 38 | 39 | if (!isValid) { 40 | throw new Error("Incorrect password"); 41 | } 42 | 43 | // verificación de email 44 | if (!user.emailVerified) { 45 | const verifyTokenExits = await db.verificationToken.findFirst({ 46 | where: { 47 | identifier: user.email, 48 | }, 49 | }); 50 | 51 | // si existe un token, lo eliminamos 52 | if (verifyTokenExits?.identifier) { 53 | await db.verificationToken.delete({ 54 | where: { 55 | identifier: user.email, 56 | }, 57 | }); 58 | } 59 | 60 | const token = nanoid(); 61 | 62 | await db.verificationToken.create({ 63 | data: { 64 | identifier: user.email, 65 | token, 66 | expires: new Date(Date.now() + 1000 * 60 * 60 * 24), 67 | }, 68 | }); 69 | 70 | // enviar email de verificación 71 | await sendEmailVerification(user.email, token); 72 | 73 | throw new Error("Please check Email send verification"); 74 | } 75 | 76 | return user; 77 | }, 78 | }), 79 | ], 80 | } satisfies NextAuthConfig; 81 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | 3 | import { PrismaAdapter } from "@auth/prisma-adapter"; 4 | 5 | import authConfig from "@/auth.config"; 6 | import { db } from "@/lib/db"; 7 | 8 | export const { handlers, signIn, signOut, auth } = NextAuth({ 9 | adapter: PrismaAdapter(db), 10 | ...authConfig, 11 | session: { strategy: "jwt" }, 12 | callbacks: { 13 | // jwt() se ejecuta cada vez que se crea o actualiza un token JWT. 14 | // Aquí es donde puedes agregar información adicional al token. 15 | jwt({ token, user }) { 16 | if (user) { 17 | token.role = user.role; 18 | } 19 | return token; 20 | }, 21 | // session() se utiliza para agregar la información del token a la sesión del usuario, 22 | // lo que hace que esté disponible en el cliente. 23 | session({ session, token }) { 24 | if (session.user) { 25 | session.user.role = token.role; 26 | } 27 | return session; 28 | }, 29 | }, 30 | events: { 31 | // El evento linkAccount se dispara cuando una cuenta (proveedor OAuth: GitHub, Google, Facebook, etc.) se vincula a un usuario existente en tu base de datos. 32 | async linkAccount({ user }) { 33 | await db.user.update({ 34 | where: { id: user.id }, 35 | data: { 36 | emailVerified: new Date(), 37 | }, 38 | }); 39 | }, 40 | }, 41 | pages: { 42 | signIn: "/login", 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/button-social.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { signIn } from "next-auth/react"; 5 | 6 | interface ButtonSocialProps { 7 | children: React.ReactNode; 8 | provider: string; 9 | } 10 | 11 | const ButtonSocial = ({ children, provider }: ButtonSocialProps) => { 12 | const handleClick = async () => { 13 | await signIn(provider); 14 | }; 15 | 16 | return ; 17 | }; 18 | export default ButtonSocial; 19 | -------------------------------------------------------------------------------- /components/form-login.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { loginSchema } from "@/lib/zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import { z } from "zod"; 7 | 8 | import { loginAction } from "@/actions/auth-action"; 9 | import { Button } from "@/components/ui/button"; 10 | import { 11 | Form, 12 | FormControl, 13 | FormField, 14 | FormItem, 15 | FormLabel, 16 | FormMessage, 17 | } from "@/components/ui/form"; 18 | import { Input } from "@/components/ui/input"; 19 | import { useRouter } from "next/navigation"; 20 | import { useState, useTransition } from "react"; 21 | 22 | import { FaGithub, FaGoogle } from "react-icons/fa6"; 23 | import ButtonSocial from "./button-social"; 24 | 25 | interface FormLoginProps { 26 | isVerified: boolean; 27 | OAuthAccountNotLinked: boolean; 28 | } 29 | 30 | const FormLogin = ({ isVerified, OAuthAccountNotLinked }: FormLoginProps) => { 31 | const [error, setError] = useState(null); 32 | const [isPending, startTransition] = useTransition(); 33 | const router = useRouter(); 34 | 35 | const form = useForm>({ 36 | resolver: zodResolver(loginSchema), 37 | defaultValues: { 38 | email: "", 39 | password: "", 40 | }, 41 | }); 42 | 43 | async function onSubmit(values: z.infer) { 44 | setError(null); 45 | startTransition(async () => { 46 | const response = await loginAction(values); 47 | if (response.error) { 48 | setError(response.error); 49 | } else { 50 | router.push("/dashboard"); 51 | } 52 | }); 53 | } 54 | 55 | return ( 56 |
57 |

Login

58 | {isVerified && ( 59 |

60 | Email verified, you can now login 61 |

62 | )} 63 | {OAuthAccountNotLinked && ( 64 |

65 | To confirm your identity, sign in with the same account you used 66 | originally. 67 |

68 | )} 69 |
70 | 74 | ( 78 | 79 | Email 80 | 81 | 86 | 87 | 88 | 89 | )} 90 | /> 91 | ( 95 | 96 | Password 97 | 98 | 103 | 104 | 105 | 106 | )} 107 | /> 108 | {error && {error}} 109 | 115 | 116 | 117 |
118 | 119 | 120 | Sign in with Github 121 | 122 | 123 | 124 | Sign in with Google 125 | 126 |
127 |
128 | ); 129 | }; 130 | export default FormLogin; 131 | -------------------------------------------------------------------------------- /components/form-register.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { registerSchema } from "@/lib/zod"; 4 | import { zodResolver } from "@hookform/resolvers/zod"; 5 | import { useForm } from "react-hook-form"; 6 | import { z } from "zod"; 7 | 8 | import { registerAction } from "@/actions/auth-action"; 9 | import { Button } from "@/components/ui/button"; 10 | import { 11 | Form, 12 | FormControl, 13 | FormField, 14 | FormItem, 15 | FormLabel, 16 | FormMessage, 17 | } from "@/components/ui/form"; 18 | import { Input } from "@/components/ui/input"; 19 | import { useRouter } from "next/navigation"; 20 | import { useState, useTransition } from "react"; 21 | 22 | const FormRegister = () => { 23 | const [error, setError] = useState(null); 24 | const [isPending, startTransition] = useTransition(); 25 | const router = useRouter(); 26 | 27 | const form = useForm>({ 28 | resolver: zodResolver(registerSchema), 29 | defaultValues: { 30 | email: "", 31 | password: "", 32 | name: "", 33 | }, 34 | }); 35 | 36 | async function onSubmit(values: z.infer) { 37 | setError(null); 38 | startTransition(async () => { 39 | const response = await registerAction(values); 40 | if (response.error) { 41 | setError(response.error); 42 | } else { 43 | router.push("/dashboard"); 44 | } 45 | }); 46 | } 47 | 48 | return ( 49 |
50 |

Register

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