From 3caf53d4c1c080f95b33dce588eb9e08ab0cc808 Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Mon, 8 Jan 2024 02:17:41 +0530 Subject: [PATCH 01/19] feat: google, github auth added --- .env | 8 +++- README.md | 76 ++++++++++++++++++++++++++++++++++++++ auth.config.ts | 11 +++++- components/auth/social.tsx | 10 ++--- 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 94dc7e3..b009cfc 100644 --- a/.env +++ b/.env @@ -10,4 +10,10 @@ DATABASE_URL="postgresql://CodeMaster17:qc8Qn7kBDrGb@ep-falling-pine-75509530-pooler.us-east-2.aws.neon.tech/auth?sslmode=require&pgbouncer=true" DIRECT_URL="postgresql://CodeMaster17:qc8Qn7kBDrGb@ep-falling-pine-75509530.us-east-2.aws.neon.tech/auth?sslmode=require" -AUTH_SECRET = 'secret' \ No newline at end of file +AUTH_SECRET = 'secret' + +GITHUB_CLIENT_ID = d511843ebd5faaeea0ca +GITHUB_CLIENT_SECRET = a19869d6eaf080604b75ec4acfd192c352c2c761 + +GOOGLE_CLIENT_ID = 830559216547-e0mijf2nkv3c958t65gn0p83ri9hg325.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET = 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 \ No newline at end of file diff --git a/README.md b/README.md index ca55a62..34572a9 100644 --- a/README.md +++ b/README.md @@ -414,5 +414,81 @@ interface Session { } ``` + #### Now you can see the role of the user in the session +## Adding Google and Github providers + +56. Add google and github providers inside providers in `auth.config.ts` file from + +``` +import Github from "next-auth/providers/github"; +import Google from "next-auth/providers/google"; +``` + +``` + Github({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), +``` + +57. Add secrets to env file + + - `GOOGLE_CLIENT_ID` + - `GOOGLE_CLIENT_SECRET` + - `GITHUB_CLIENT_ID` + - `GITHUB_CLIENT_SECRET` + +58. Test google and github in `social.tsx` + +``` +"use client"; + +import { signIn } from "next-auth/react"; // this is we have to import when in client side +import { FcGoogle } from "react-icons/fc"; +import { FaGithub } from "react-icons/fa"; +// import { useSearchParams } from "next/navigation"; + +import { Button } from "@/components/ui/button"; +import { DEFAULT_LOGIN_REDIRECT } from "@/route"; + +export const Social = () => { + // const searchParams = useSearchParams(); + // const callbackUrl = searchParams.get("callbackUrl"); + + const onClick = (provider: "google" | "github") => { + signIn(provider, { + callbackUrl: DEFAULT_LOGIN_REDIRECT, + }); + } + + return ( +
+ + +
+ ); +}; + +``` + +59. \ No newline at end of file diff --git a/auth.config.ts b/auth.config.ts index 15092a7..2da2380 100644 --- a/auth.config.ts +++ b/auth.config.ts @@ -3,13 +3,22 @@ import bcrypt from "bcryptjs"; import Credentials from "next-auth/providers/credentials"; - +import Github from "next-auth/providers/github"; +import Google from "next-auth/providers/google"; import type { NextAuthConfig } from "next-auth"; import { LoginSchema } from "./schema"; import { getUserByEmail } from "./lib/actions/user.action"; export default { providers: [ + Github({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }), + Google({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), Credentials({ async authorize(credentials) { const validatedFields = LoginSchema.safeParse(credentials); // again doing validation diff --git a/components/auth/social.tsx b/components/auth/social.tsx index aa7a7b6..5e7d366 100644 --- a/components/auth/social.tsx +++ b/components/auth/social.tsx @@ -1,21 +1,21 @@ "use client"; -// import { signIn } from "next-auth/react"; +import { signIn } from "next-auth/react"; // this is we have to import when in client side import { FcGoogle } from "react-icons/fc"; import { FaGithub } from "react-icons/fa"; // import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; -// import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; +import { DEFAULT_LOGIN_REDIRECT } from "@/route"; export const Social = () => { // const searchParams = useSearchParams(); // const callbackUrl = searchParams.get("callbackUrl"); const onClick = (provider: "google" | "github") => { - // signIn(provider, { - // callbackUrl: callbackUrl || DEFAULT_LOGIN_REDIRECT, - // }); + signIn(provider, { + callbackUrl: DEFAULT_LOGIN_REDIRECT, + }); } return ( From 7384a3812293e557e3dfb348ab175dc611706ee1 Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Thu, 11 Jan 2024 02:08:19 +0530 Subject: [PATCH 02/19] feat: O-Auth added, custom error page for same acc login added --- README.md | 47 +++++++++++++++++++++++++++++++++- app/auth/error/page.tsx | 9 +++++++ auth.ts | 17 +++++++++++- components/auth/error-card.tsx | 17 ++++++++++++ components/auth/login-form.tsx | 11 +++++++- route.ts | 4 +-- 6 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 app/auth/error/page.tsx create mode 100644 components/auth/error-card.tsx diff --git a/README.md b/README.md index 34572a9..25de840 100644 --- a/README.md +++ b/README.md @@ -491,4 +491,49 @@ export const Social = () => { ``` -59. \ No newline at end of file +59. linkAccount from Auth.js site can be used to directly register in case of google and github when the user does not exist and and there will be no need for register page + +- update auth.ts `events` + +``` + // * This is for linkAccount feature + events:{ + async linkAccount({user}){ + await db.user.update({ + where :{id : user.id}, + data : {emailVerified: new Date()} + }) + } + }, +``` + +60. Add this too + +``` + // * This is for solving errors when using linkAccount feature + pages: { + signIn: "/auth/login", + error: "/auth/error", + }, +``` + +61. Create 2 new files, error-card.tsx and error.tsx in `auth`folder and `components/auth` folder respectively + +62. Update `routes.ts` file to + +``` +export const authRoutes = ["/auth/login", "/auth/register", "/auth/error"]; +``` + +63. Get the error of 59 and display it in login form itself in `LoginForm.tsx` file + +``` + // * for getting search params + const searchParams = useSearchParams() + const urlError = searchParams.get("error") + +``` + +#### Oauth completed here. + +## Email verification diff --git a/app/auth/error/page.tsx b/app/auth/error/page.tsx new file mode 100644 index 0000000..752f4b1 --- /dev/null +++ b/app/auth/error/page.tsx @@ -0,0 +1,9 @@ +import { ErrorCard } from "@/components/auth/error-card"; + +const AuthErrorPage = () => { + return ( + + ); +}; + +export default AuthErrorPage; diff --git a/auth.ts b/auth.ts index 57ff8ea..c76ddef 100644 --- a/auth.ts +++ b/auth.ts @@ -12,8 +12,23 @@ export const { signIn, signOut, } = NextAuth({ + // * This is for solving errors when using linkAccount feature + pages: { + signIn: "/auth/login", + error: "/auth/error", + }, + + // * This is for linkAccount feature + events: { + async linkAccount({ user }) { + await db.user.update({ + where: { id: user.id }, + data: { emailVerified: new Date() }, + }); + }, + }, + callbacks: { - async session({ token, session }) { if (token.sub && session.user) { session.user.id = token.sub; diff --git a/components/auth/error-card.tsx b/components/auth/error-card.tsx new file mode 100644 index 0000000..7c78e4e --- /dev/null +++ b/components/auth/error-card.tsx @@ -0,0 +1,17 @@ +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; + +import { CardWrapper } from "@/components/auth/card-wrapper"; + +export const ErrorCard = () => { + return ( + +
+ +
+
+ ); +}; diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index 70f4376..61f3ed6 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -19,8 +19,15 @@ import Link from 'next/link'; import { FormSuccess } from '../form-sucess'; import { FormError } from '../form-error'; import { Login } from '@/actions/auth/login'; +import { useSearchParams } from 'next/navigation'; + + const LoginForm = () => { + // * for getting search params for error in case not logged in + const searchParams = useSearchParams() + const urlError = searchParams.get("error") === "OAuthAccountNotLinked" ? "Email already with different mail provider" : "" + const [error, setError] = useState(""); const [success, setSuccess] = useState(""); @@ -45,6 +52,8 @@ const LoginForm = () => { if (data?.success) { form.reset(); + + // TODO: when 2FA is implemented setSuccess(data.success); } }) @@ -107,7 +116,7 @@ const LoginForm = () => { )} /> - + + + + + ); +}; +``` + +84. Also create a new schema in `schema/index.ts` file + +``` +// reset schema +export const ResetSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), +}); + +``` + +85. Add the schema in `schema.prisma` file + +``` +// * for password reset +model PasswordResetToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + + +``` + +86. Now run the following command + `npx prisma generate` and `npx prisma db push` + +87. Create a new action in `lib/actions/auth` folder i.e `password-reset-token.ts` file using email and token + +``` +import { db } from "@/lib/database.connection"; + +export const getPasswordResetTokenByToken = async (token: string) => { +try { + const passwordResetToken = await db.passwordResetToken.findUnique({ + where: { token }, + }); + + return passwordResetToken; +} catch { + return null; +} +}; + +export const getPasswordResetTokenByEmail = async (email: string) => { +try { + const passwordResetToken = await db.passwordResetToken.findFirst({ + where: { email }, + }); + + return passwordResetToken; +} catch { + return null; +} +}; +``` + +88. Create a new file `token.ts` in `lib` folder + +``` +import crypto from "crypto"; +import { v4 as uuidv4 } from "uuid"; +import { db } from "./database.connection"; +import { getVerificationTokenByEmail } from "./actions/auth/verification-token"; +import { getPasswordResetTokenByEmail } from "./actions/auth/password-reset-token"; + +export const generateVerificationToken = async (email: string) => { + const token = uuidv4(); + const expires = new Date(new Date().getTime() + 3600 * 1000); // * expiring in 1 hour + + const existingToken = await getVerificationTokenByEmail(email); + + if (existingToken) { + await db.verificationToken.delete({ + where: { + id: existingToken.id, + }, + }); + } + + const verficationToken = await db.verificationToken.create({ + data: { + email, + token, + expires, + }, + }); + + return verficationToken; +}; + + +// generating password reset token +export const generatePasswordResetToken = async (email: string) => { + const token = uuidv4(); + const expires = new Date(new Date().getTime() + 3600 * 1000); + + const existingToken = await getPasswordResetTokenByEmail(email); + + if (existingToken) { + await db.passwordResetToken.delete({ + where: { id: existingToken.id }, + }); + } + + const passwordResetToken = await db.passwordResetToken.create({ + data: { + email, + token, + expires, + }, + }); + + return passwordResetToken; +}; +``` +89. Write mail to send mail verification in `mail.ts` file + +``` +// sending password reset email +export const sendPasswordResetEmail = async (email: string, token: string) => { + const resetLink = `http://localhost:3000/auth/new-password?token=${token}`; + + await resend.emails.send({ + from: "onboarding@resend.dev", + to: email, + subject: "Reset your password", + html: `

Click here to reset password.

`, + }); +}; +``` + +90. Uncomment the code in `reset.ts` file + diff --git a/actions/auth/reset.ts b/actions/auth/reset.ts new file mode 100644 index 0000000..60c1c9c --- /dev/null +++ b/actions/auth/reset.ts @@ -0,0 +1,31 @@ +"use server"; + +import { getUserByEmail } from "@/lib/actions/user.action"; +import { sendPasswordResetEmail } from "@/lib/mail"; +import { generatePasswordResetToken } from "@/lib/token"; +import { ResetSchema } from "@/schema"; +import * as z from "zod"; + +export const reset = async (values: z.infer) => { + const validatedFields = ResetSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid emaiL!" }; + } + + const { email } = validatedFields.data; + + const existingUser = await getUserByEmail(email); + + if (!existingUser) { + return { error: "Email not found!" }; + } + + const passwordResetToken = await generatePasswordResetToken(email); + await sendPasswordResetEmail( + passwordResetToken.email, + passwordResetToken.token + ); + + return { success: "Reset email sent!" }; +}; diff --git a/app/auth/reset/page.tsx b/app/auth/reset/page.tsx new file mode 100644 index 0000000..b18b1aa --- /dev/null +++ b/app/auth/reset/page.tsx @@ -0,0 +1,9 @@ +import { ResetForm } from "@/components/auth/reset-form"; + +const ResetPage = () => { + return ( + + ); +} + +export default ResetPage; \ No newline at end of file diff --git a/components/auth/reset-form.tsx b/components/auth/reset-form.tsx new file mode 100644 index 0000000..d53e60b --- /dev/null +++ b/components/auth/reset-form.tsx @@ -0,0 +1,94 @@ +"use client"; + +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { useState, useTransition } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { Input } from "@/components/ui/input"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { CardWrapper } from "@/components/auth/card-wrapper" +import { Button } from "@/components/ui/button"; +import { FormError } from "@/components/form-error"; +import { ResetSchema } from "@/schema"; +import { FormSuccess } from "../form-sucess"; +import { reset } from "@/actions/auth/reset"; + + +export const ResetForm = () => { + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(ResetSchema), + defaultValues: { + email: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + reset(values) + .then((data) => { + setError(data?.error); + setSuccess(data?.success); + }); + }); + }; + + return ( + +
+ +
+ ( + + Email + + + + + + )} + /> +
+ + + + + +
+ ); +}; diff --git a/lib/actions/auth/password-reset-token.ts b/lib/actions/auth/password-reset-token.ts new file mode 100644 index 0000000..1acc4d6 --- /dev/null +++ b/lib/actions/auth/password-reset-token.ts @@ -0,0 +1,25 @@ +import { db } from "@/lib/database.connection"; + +export const getPasswordResetTokenByToken = async (token: string) => { + try { + const passwordResetToken = await db.passwordResetToken.findUnique({ + where: { token }, + }); + + return passwordResetToken; + } catch { + return null; + } +}; + +export const getPasswordResetTokenByEmail = async (email: string) => { + try { + const passwordResetToken = await db.passwordResetToken.findFirst({ + where: { email }, + }); + + return passwordResetToken; + } catch { + return null; + } +}; diff --git a/lib/mail.ts b/lib/mail.ts index 6d4bf68..c1df569 100644 --- a/lib/mail.ts +++ b/lib/mail.ts @@ -10,3 +10,15 @@ export const sendVerificationEmail = async (email: string, token: string) => { html: `

Click here to confirm email.

`, }); }; + +// sending password reset email +export const sendPasswordResetEmail = async (email: string, token: string) => { + const resetLink = `http://localhost:3000/auth/new-password?token=${token}`; + + await resend.emails.send({ + from: "onboarding@resend.dev", + to: email, + subject: "Reset your password", + html: `

Click here to reset password.

`, + }); +}; diff --git a/lib/token.ts b/lib/token.ts index a11fddb..71bff0f 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -2,6 +2,7 @@ import crypto from "crypto"; import { v4 as uuidv4 } from "uuid"; import { db } from "./database.connection"; import { getVerificationTokenByEmail } from "./actions/auth/verification-token"; +import { getPasswordResetTokenByEmail } from "./actions/auth/password-reset-token"; export const generateVerificationToken = async (email: string) => { const token = uuidv4(); @@ -9,7 +10,7 @@ export const generateVerificationToken = async (email: string) => { const existingToken = await getVerificationTokenByEmail(email); - if (existingToken) { + if (existingToken) { // * if token exists, delete it await db.verificationToken.delete({ where: { id: existingToken.id, @@ -27,3 +28,28 @@ export const generateVerificationToken = async (email: string) => { return verficationToken; }; + + +// generating password reset token +export const generatePasswordResetToken = async (email: string) => { + const token = uuidv4(); + const expires = new Date(new Date().getTime() + 3600 * 1000); + + const existingToken = await getPasswordResetTokenByEmail(email); + + if (existingToken) { + await db.passwordResetToken.delete({ + where: { id: existingToken.id }, + }); + } + + const passwordResetToken = await db.passwordResetToken.create({ + data: { + email, + token, + expires, + }, + }); + + return passwordResetToken; +}; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 843af5f..7b7fb33 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -59,3 +59,14 @@ model VerificationToken { expires DateTime @@unique([email, token]) // only unique token for specific email } + +// * for password reset +model PasswordResetToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + diff --git a/route.ts b/route.ts index 8db0acc..e332e9d 100644 --- a/route.ts +++ b/route.ts @@ -6,7 +6,12 @@ export const publicRoutes = ["/", "/auth/new-verification"]; // * an array of roues that are used for authentication // * These routes will redirect logged in users to /settings // @ @type {string[]} -export const authRoutes = ["/auth/login", "/auth/register", "/auth/error"]; +export const authRoutes = [ + "/auth/login", + "/auth/register", + "/auth/error", + "/auth/reset", +]; /** * The prefix for API authentication routes diff --git a/schema/index.ts b/schema/index.ts index 0e3d63f..64436a5 100644 --- a/schema/index.ts +++ b/schema/index.ts @@ -20,3 +20,10 @@ export const RegisterSchema = z.object({ message: "Name is required", }), }); + +// reset schema +export const ResetSchema = z.object({ + email: z.string().email({ + message: "Email is required", + }), +}); From 73b72c00f23e56d73a16f5ae54d6490d6d1f942e Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Sun, 4 Feb 2024 08:58:45 +0530 Subject: [PATCH 09/19] feat: reset password feature implemented --- README.md | 176 ++++++++++++++++++++++++++ actions/auth/new-password.ts | 57 +++++++++ app/auth/new-password/page.tsx | 12 ++ components/auth/new-password-form.tsx | 91 +++++++++++++ route.ts | 1 + schema/index.ts | 7 + 6 files changed, 344 insertions(+) create mode 100644 actions/auth/new-password.ts create mode 100644 app/auth/new-password/page.tsx create mode 100644 components/auth/new-password-form.tsx diff --git a/README.md b/README.md index a59db59..c314f67 100644 --- a/README.md +++ b/README.md @@ -1082,6 +1082,7 @@ export const generatePasswordResetToken = async (email: string) => { return passwordResetToken; }; ``` + 89. Write mail to send mail verification in `mail.ts` file ``` @@ -1100,3 +1101,178 @@ export const sendPasswordResetEmail = async (email: string, token: string) => { 90. Uncomment the code in `reset.ts` file +## Reset Password Form + +91. Add new route in `routes.ts` file i.e `export const resetRoutes = ["/auth/reset", "/auth/new-password"];` +92. Create the new schema in `schema/index.ts` file + +``` +// new password schema +export const NewPasswordSchema = z.object({ + password: z.string().min(6, { + message: "Minimum of 6 characters required", + }), +}); + +``` + +93. Create a new file in `components/auth` folder i.e `new-password-form.tsx` file which will be imported to `/auth/new-password.tsx` file + +``` +'use client' +import React, { useState, useTransition } from 'react' +import { CardWrapper } from './card-wrapper'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; +import { Input } from "@/components/ui/input"; +import { useForm } from 'react-hook-form'; +import { NewPasswordSchema } from '@/schema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +import { FormError } from '../form-error'; +import { FormSuccess } from '../form-sucess'; +import { Button } from '../ui/button'; +import { useSearchParams } from 'next/navigation'; +import { newPassword } from '@/actions/auth/new-password'; + +const NewPasswordForm = () => { + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(NewPasswordSchema), + defaultValues: { + password: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + newPassword(values, token) + .then((data) => { + setError(data?.error); + setSuccess(data?.success); + }); + }); + }; + + return ( + +
+ +
+ ( + + Password + + + + + + )} + /> +
+ + + + + +
+ ) +} + +export default NewPasswordForm + +``` + +94. Create a new action in `/actions/auth` folder i.e `new-password.ts` file + +``` +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; +import { NewPasswordSchema } from "@/schema"; +import { getPasswordResetTokenByToken } from "@/lib/actions/auth/password-reset-token"; +import { getUserByEmail } from "@/lib/actions/user.action"; +import { db } from "@/lib/database.connection"; + + +export const newPassword = async ( + values: z.infer, + token?: string | null +) => { + if (!token) { + return { error: "Missing token!" }; + } + + const validatedFields = NewPasswordSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { password } = validatedFields.data; + + const existingToken = await getPasswordResetTokenByToken(token); + + if (!existingToken) { + return { error: "Invalid token!" }; + } + + const hasExpired = new Date(existingToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Token has expired!" }; + } + + const existingUser = await getUserByEmail(existingToken.email); + + if (!existingUser) { + return { error: "Email does not exist!" }; + } + + const hashedPassword = await bcrypt.hash(password, 10); + + await db.user.update({ + where: { id: existingUser.id }, + data: { password: hashedPassword }, + }); + + await db.passwordResetToken.delete({ + where: { id: existingToken.id }, + }); + + return { success: "Password updated!" }; +}; +``` + +It shoul now start working. diff --git a/actions/auth/new-password.ts b/actions/auth/new-password.ts new file mode 100644 index 0000000..6614186 --- /dev/null +++ b/actions/auth/new-password.ts @@ -0,0 +1,57 @@ +"use server"; + +import * as z from "zod"; +import bcrypt from "bcryptjs"; +import { NewPasswordSchema } from "@/schema"; +import { getPasswordResetTokenByToken } from "@/lib/actions/auth/password-reset-token"; +import { getUserByEmail } from "@/lib/actions/user.action"; +import { db } from "@/lib/database.connection"; + + +export const newPassword = async ( + values: z.infer, + token?: string | null +) => { + if (!token) { + return { error: "Missing token!" }; + } + + const validatedFields = NewPasswordSchema.safeParse(values); + + if (!validatedFields.success) { + return { error: "Invalid fields!" }; + } + + const { password } = validatedFields.data; + + const existingToken = await getPasswordResetTokenByToken(token); + + if (!existingToken) { + return { error: "Invalid token!" }; + } + + const hasExpired = new Date(existingToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Token has expired!" }; + } + + const existingUser = await getUserByEmail(existingToken.email); + + if (!existingUser) { + return { error: "Email does not exist!" }; + } + + const hashedPassword = await bcrypt.hash(password, 10); + + await db.user.update({ + where: { id: existingUser.id }, + data: { password: hashedPassword }, + }); + + await db.passwordResetToken.delete({ + where: { id: existingToken.id }, + }); + + return { success: "Password updated!" }; +}; diff --git a/app/auth/new-password/page.tsx b/app/auth/new-password/page.tsx new file mode 100644 index 0000000..cc59ab1 --- /dev/null +++ b/app/auth/new-password/page.tsx @@ -0,0 +1,12 @@ +import NewPasswordForm from '@/components/auth/new-password-form' +import React from 'react' + +const NewPassword = () => { + return ( +
+ +
+ ) +} + +export default NewPassword diff --git a/components/auth/new-password-form.tsx b/components/auth/new-password-form.tsx new file mode 100644 index 0000000..b1aa60f --- /dev/null +++ b/components/auth/new-password-form.tsx @@ -0,0 +1,91 @@ +'use client' +import React, { useState, useTransition } from 'react' +import { CardWrapper } from './card-wrapper'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; +import { Input } from "@/components/ui/input"; +import { useForm } from 'react-hook-form'; +import { NewPasswordSchema } from '@/schema'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +import { FormError } from '../form-error'; +import { FormSuccess } from '../form-sucess'; +import { Button } from '../ui/button'; +import { useSearchParams } from 'next/navigation'; +import { newPassword } from '@/actions/auth/new-password'; + +const NewPasswordForm = () => { + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(NewPasswordSchema), + defaultValues: { + password: "", + }, + }); + + const onSubmit = (values: z.infer) => { + setError(""); + setSuccess(""); + + startTransition(() => { + newPassword(values, token) + .then((data) => { + setError(data?.error); + setSuccess(data?.success); + }); + }); + }; + + return ( + +
+ +
+ ( + + Password + + + + + + )} + /> +
+ + + + + +
+ ) +} + +export default NewPasswordForm diff --git a/route.ts b/route.ts index e332e9d..d3394eb 100644 --- a/route.ts +++ b/route.ts @@ -11,6 +11,7 @@ export const authRoutes = [ "/auth/register", "/auth/error", "/auth/reset", + "/auth/new-password", ]; /** diff --git a/schema/index.ts b/schema/index.ts index 64436a5..87c9773 100644 --- a/schema/index.ts +++ b/schema/index.ts @@ -27,3 +27,10 @@ export const ResetSchema = z.object({ message: "Email is required", }), }); + +// new password schema +export const NewPasswordSchema = z.object({ + password: z.string().min(6, { + message: "Minimum of 6 characters required", + }), +}); From 4577eb73146d83e1e811f936876df2f5716d682e Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Mon, 5 Feb 2024 09:42:51 +0530 Subject: [PATCH 10/19] feat: 2FA implemented --- README.md | 239 ++++++++++++++++++++ actions/auth/login.ts | 65 +++++- app/auth/login/page.tsx | 3 +- auth.ts | 22 +- components/auth/login-form.tsx | 189 +++++++++------- lib/actions/auth/two-factor-confirmation.ts | 13 ++ lib/actions/auth/two-factor-token.ts | 25 ++ lib/mail.ts | 10 + lib/token.ts | 31 ++- prisma/schema.prisma | 19 ++ schema/index.ts | 1 + 11 files changed, 524 insertions(+), 93 deletions(-) create mode 100644 lib/actions/auth/two-factor-confirmation.ts create mode 100644 lib/actions/auth/two-factor-token.ts diff --git a/README.md b/README.md index c314f67..5dfd9c9 100644 --- a/README.md +++ b/README.md @@ -1276,3 +1276,242 @@ export const newPassword = async ( ``` It shoul now start working. + +## 2Factor Authentication + +95. Update `schema.prisma` file + +- user model + +``` +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + password String? + accounts Account[] + role UserRole @default(USER) + isTwoFactorEnabled Boolean @default(false) + twoFactorConfirmation TwoFactorConfirmation? +} +``` + +- isTwoFactorEnabled model + +``` +model TwoFactorToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} +``` + +- TwoFactorConfirmation model + +``` +model TwoFactorConfirmation { + id String @id @default(cuid()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId]) +} +``` + +96. Now run the following command + `npx prisma generate` and `npx prisma db push` + +97. # Setting up two-factor-token + +- Create a new file in `lib/actions/auth` folder i.e `two-factor-token.ts` file + +``` +import { db } from "@/lib/database.connection"; + +export const getTwoFactorTokenByToken = async (token: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findUnique({ + where: { token }, + }); + + return twoFactorToken; + } catch { + return null; + } +}; + +export const getTwoFactorTokenByEmail = async (email: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findFirst({ + where: { email }, + }); + + return twoFactorToken; + } catch { + return null; + } +}; +``` + +- Setting up the action for confirming two-factor token in `lib/actions/auth` folder `two-factor-confirmation.ts` file + +``` +import { db } from "@/lib/database.connection"; + +export const getTwoFactorConfirmationByUserId = async (userId: string) => { + try { + const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({ + where: { userId }, + }); + + return twoFactorConfirmation; + } catch { + return null; + } +}; + +``` + +- Generating Two factor token in `lib/token` file + +``` +export const generateTwoFactorToken = async (email: string) => { + const token = crypto.randomInt(100_000, 1_000_000).toString(); + const expires = new Date(new Date().getTime() + 5 * 60 * 1000); + + const existingToken = await getTwoFactorTokenByEmail(email); + + if (existingToken) { + await db.twoFactorToken.delete({ + where: { + id: existingToken.id, + } + }); + } + + const twoFactorToken = await db.twoFactorToken.create({ + data: { + email, + token, + expires, + } + }); + + return twoFactorToken; +} +``` + +- Sned two factor token email + +``` +export const sendTwoFactorTokenEmail = async ( + email: string, + token: string +) => { + await resend.emails.send({ + from: "mail@auth-masterclass-tutorial.com", + to: email, + subject: "2FA Code", + html: `

Your 2FA code: ${token}

` + }); +}; +``` + +98. Go to prisma studio and enable the 2FA for a user + +99. Modify the login function in `auth.ts` file + +``` + + // * Prevent sign in without two factor confirmation (99) + if (existingUser.isTwoFactorEnabled) { + const twoFactorConfirmation = await getTwoFactorConfirmationByUserId( + existingUser.id + ); + + if (!twoFactorConfirmation) return false; + + // Delete two factor confirmation for next sign in + await db.twoFactorConfirmation.delete({ + where: { id: twoFactorConfirmation.id }, + }); + } +``` + +100. Add 2FA Verification in `login.ts` file + +``` + //* 2FA verification + if (exisitingUser.isTwoFactorEnabled && exisitingUser.email) { + if (code) { + const twoFactorToken = await getTwoFactorTokenByEmail(exisitingUser.email); + + if (!twoFactorToken) { + return { error: "Invalid code!" }; + } + + if (twoFactorToken.token !== code) { + return { error: "Invalid code!" }; + } + + const hasExpired = new Date(twoFactorToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Code expired!" }; + } + + await db.twoFactorToken.delete({ + where: { id: twoFactorToken.id }, + }); + + const existingConfirmation = await getTwoFactorConfirmationByUserId( + exisitingUser.id + ); + + if (existingConfirmation) { + await db.twoFactorConfirmation.delete({ + where: { id: existingConfirmation.id }, + }); + } + + await db.twoFactorConfirmation.create({ + data: { + userId: exisitingUser.id, + }, + }); + } else { + const twoFactorToken = await generateTwoFactorToken(exisitingUser.email); + await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token); + + return { twoFactor: true }; + } + } +``` + +101. Update the login schema + +``` +export const LoginSchema = z.object({ +email: z.string().email({ + message: "Email is required", +}), +password: z.string().min(1, { + message: "Password is required", +}), +code: z.optional(z.string()), +}); + +``` +102. Update the login form, based on the conditions (see the code directly from file) +- concept is to show 2FA code, when after the login button is clicked, and two factor is enabled +- (might be incomplete) + + + + diff --git a/actions/auth/login.ts b/actions/auth/login.ts index 093d11e..c5c45f8 100644 --- a/actions/auth/login.ts +++ b/actions/auth/login.ts @@ -6,15 +6,18 @@ import { signIn } from "@/auth"; import { DEFAULT_LOGIN_REDIRECT } from "@/route"; import { AuthError } from "next-auth"; import { getUserByEmail } from "@/lib/actions/user.action"; -import { generateVerificationToken } from "@/lib/token"; -import { sendVerificationEmail } from "@/lib/mail"; +import { generateTwoFactorToken, generateVerificationToken } from "@/lib/token"; +import { sendTwoFactorTokenEmail, sendVerificationEmail } from "@/lib/mail"; +import { getTwoFactorConfirmationByUserId } from "@/lib/actions/auth/two-factor-confirmation"; +import { db } from "@/lib/database.connection"; +import { getTwoFactorTokenByEmail } from "@/lib/actions/auth/two-factor-token"; export const Login = async (values: z.infer) => { const validatedFields = LoginSchema.safeParse(values); // valdiating the input values if (!validatedFields.success) { return { error: "Invalid fields! " }; } - const { email, password } = validatedFields.data; + const { email, password, code } = validatedFields.data; // * not allowing the user to login if the email is not verified (69) const exisitingUser = await getUserByEmail(email); @@ -29,14 +32,60 @@ export const Login = async (values: z.infer) => { ); // * sending mail while logging in if email is not verified (72) - await sendVerificationEmail( - verificationToken.email, - verificationToken.token - ); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token + ); return { success: "Confirmation Email sent!" }; } - // * + //* 2FA verification + if (exisitingUser.isTwoFactorEnabled && exisitingUser.email) { + if (code) { + const twoFactorToken = await getTwoFactorTokenByEmail( + exisitingUser.email + ); + + if (!twoFactorToken) { + return { error: "Invalid code!" }; + } + + if (twoFactorToken.token !== code) { + return { error: "Invalid code!" }; + } + + const hasExpired = new Date(twoFactorToken.expires) < new Date(); + + if (hasExpired) { + return { error: "Code expired!" }; + } + + await db.twoFactorToken.delete({ + where: { id: twoFactorToken.id }, + }); + + const existingConfirmation = await getTwoFactorConfirmationByUserId( + exisitingUser.id + ); + + if (existingConfirmation) { + await db.twoFactorConfirmation.delete({ + where: { id: existingConfirmation.id }, + }); + } + + await db.twoFactorConfirmation.create({ + data: { + userId: exisitingUser.id, + }, + }); + } else { + const twoFactorToken = await generateTwoFactorToken(exisitingUser.email); + await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token); + + return { twoFactor: true }; + } + } try { await signIn("credentials", { diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 2bcc751..5ac4dbe 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -1,4 +1,5 @@ -import LoginForm from '@/components/auth/login-form' + +import { LoginForm } from '@/components/auth/login-form' import React from 'react' const LoginPage = () => { diff --git a/auth.ts b/auth.ts index d010f88..7e5c0c9 100644 --- a/auth.ts +++ b/auth.ts @@ -5,6 +5,7 @@ import { db } from "./lib/database.connection"; import { PrismaAdapter } from "@auth/prisma-adapter"; import { getUserById } from "./lib/actions/user.action"; import { UserRole } from "@prisma/client"; +import { getTwoFactorConfirmationByUserId } from "./lib/actions/auth/two-factor-confirmation"; export const { handlers: { GET, POST }, @@ -31,14 +32,27 @@ export const { callbacks: { // * (70) async signIn({ user, account }) { + // Allow OAuth without email verification if (account?.provider !== "credentials") return true; - const exisitingUser = await getUserById(user.id); + const existingUser = await getUserById(user.id); - //* prevent sign in without email verification - if (!exisitingUser?.emailVerified) return false; + // Prevent sign in without email verification + if (!existingUser?.emailVerified) return false; - // TODO : Add 2FA check + // * Prevent sign in without two factor confirmation (99) + if (existingUser.isTwoFactorEnabled) { + const twoFactorConfirmation = await getTwoFactorConfirmationByUserId( + existingUser.id + ); + + if (!twoFactorConfirmation) return false; + + // Delete two factor confirmation for next sign in + await db.twoFactorConfirmation.delete({ + where: { id: twoFactorConfirmation.id }, + }); + } return true; }, diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index 6900ffc..7949bfb 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -1,6 +1,12 @@ -'use client' -import React, { startTransition, useState } from 'react' -import { CardWrapper } from './card-wrapper' +"use client"; + +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { useState, useTransition } from "react"; +import { useSearchParams } from "next/navigation"; +import { zodResolver } from "@hookform/resolvers/zod"; +import Link from "next/link"; + import { Input } from "@/components/ui/input"; import { Form, @@ -10,124 +16,153 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { LoginSchema } from '@/schema'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { Button } from '../ui/button'; -import Link from 'next/link'; -import { FormSuccess } from '../form-sucess'; -import { FormError } from '../form-error'; -import { Login } from '@/actions/auth/login'; -import { useSearchParams } from 'next/navigation'; +import { CardWrapper } from "@/components/auth/card-wrapper" +import { Button } from "@/components/ui/button"; +import { FormError } from "@/components/form-error"; +import { LoginSchema } from "@/schema"; +import { Login } from "@/actions/auth/login"; +import { FormSuccess } from "../form-sucess"; -const LoginForm = () => { - - // * for getting search params for error in case not logged in +export const LoginForm = () => { const searchParams = useSearchParams() - const urlError = searchParams.get("error") === "OAuthAccountNotLinked" ? "Email already with different mail provider" : "" + const callbackUrl = searchParams.get("callbackUrl"); + const urlError = searchParams.get("error") === "OAuthAccountNotLinked" + ? "Email already in use with different provider!" + : ""; + const [showTwoFactor, setShowTwoFactor] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); + const [isPending, startTransition] = useTransition(); const form = useForm>({ resolver: zodResolver(LoginSchema), defaultValues: { email: "", - password: "" - } - }) + password: "", + }, + }); const onSubmit = (values: z.infer) => { setError(""); setSuccess(""); + startTransition(() => { Login(values) .then((data) => { if (data?.error) { form.reset(); - setError(data?.error); + setError(data.error); } if (data?.success) { form.reset(); setSuccess(data.success); } + + if (data?.twoFactor) { + setShowTwoFactor(true); + } }) .catch(() => setError("Something went wrong")); }); - } + }; return ( - + showSocial + >
- ( - - Email - - - - - +
+ {showTwoFactor && ( + ( + + Two Factor Code + + + + + + )} + /> )} - /> - ( - - Password - - - - - - + {!showTwoFactor && ( + <> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + + )} + /> + )} - /> - +
- - +
- ) -} - -export default LoginForm + ); +}; diff --git a/lib/actions/auth/two-factor-confirmation.ts b/lib/actions/auth/two-factor-confirmation.ts new file mode 100644 index 0000000..e29ad2d --- /dev/null +++ b/lib/actions/auth/two-factor-confirmation.ts @@ -0,0 +1,13 @@ +import { db } from "@/lib/database.connection"; + +export const getTwoFactorConfirmationByUserId = async (userId: string) => { + try { + const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({ + where: { userId }, + }); + + return twoFactorConfirmation; + } catch { + return null; + } +}; diff --git a/lib/actions/auth/two-factor-token.ts b/lib/actions/auth/two-factor-token.ts new file mode 100644 index 0000000..a49b985 --- /dev/null +++ b/lib/actions/auth/two-factor-token.ts @@ -0,0 +1,25 @@ +import { db } from "@/lib/database.connection"; + +export const getTwoFactorTokenByToken = async (token: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findUnique({ + where: { token }, + }); + + return twoFactorToken; + } catch { + return null; + } +}; + +export const getTwoFactorTokenByEmail = async (email: string) => { + try { + const twoFactorToken = await db.twoFactorToken.findFirst({ + where: { email }, + }); + + return twoFactorToken; + } catch { + return null; + } +}; diff --git a/lib/mail.ts b/lib/mail.ts index c1df569..08a504c 100644 --- a/lib/mail.ts +++ b/lib/mail.ts @@ -22,3 +22,13 @@ export const sendPasswordResetEmail = async (email: string, token: string) => { html: `

Click here to reset password.

`, }); }; + +// sending two factor token email +export const sendTwoFactorTokenEmail = async (email: string, token: string) => { + await resend.emails.send({ + from: "onboarding@resend.dev", + to: email, + subject: "2FA Code", + html: `

Your 2FA code: ${token}

`, + }); +}; diff --git a/lib/token.ts b/lib/token.ts index 71bff0f..016b95b 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -3,6 +3,31 @@ import { v4 as uuidv4 } from "uuid"; import { db } from "./database.connection"; import { getVerificationTokenByEmail } from "./actions/auth/verification-token"; import { getPasswordResetTokenByEmail } from "./actions/auth/password-reset-token"; +import { getTwoFactorTokenByEmail } from "./actions/auth/two-factor-token"; +export const generateTwoFactorToken = async (email: string) => { + const token = crypto.randomInt(100_000, 1_000_000).toString(); + const expires = new Date(new Date().getTime() + 5 * 60 * 1000); + + const existingToken = await getTwoFactorTokenByEmail(email); + + if (existingToken) { + await db.twoFactorToken.delete({ + where: { + id: existingToken.id, + }, + }); + } + + const twoFactorToken = await db.twoFactorToken.create({ + data: { + email, + token, + expires, + }, + }); + + return twoFactorToken; +}; export const generateVerificationToken = async (email: string) => { const token = uuidv4(); @@ -10,7 +35,8 @@ export const generateVerificationToken = async (email: string) => { const existingToken = await getVerificationTokenByEmail(email); - if (existingToken) { // * if token exists, delete it + if (existingToken) { + // * if token exists, delete it await db.verificationToken.delete({ where: { id: existingToken.id, @@ -29,7 +55,6 @@ export const generateVerificationToken = async (email: string) => { return verficationToken; }; - // generating password reset token export const generatePasswordResetToken = async (email: string) => { const token = uuidv4(); @@ -52,4 +77,4 @@ export const generatePasswordResetToken = async (email: string) => { }); return passwordResetToken; -}; \ No newline at end of file +}; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7b7fb33..24a6947 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,6 +27,8 @@ model User { password String? accounts Account[] role UserRole @default(USER) + isTwoFactorEnabled Boolean @default(false) + twoFactorConfirmation TwoFactorConfirmation? } @@ -70,3 +72,20 @@ model PasswordResetToken { @@unique([email, token]) } +model TwoFactorToken { + id String @id @default(cuid()) + email String + token String @unique + expires DateTime + + @@unique([email, token]) +} + +model TwoFactorConfirmation { + id String @id @default(cuid()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId]) +} \ No newline at end of file diff --git a/schema/index.ts b/schema/index.ts index 87c9773..a34b432 100644 --- a/schema/index.ts +++ b/schema/index.ts @@ -7,6 +7,7 @@ export const LoginSchema = z.object({ password: z.string().min(1, { message: "Password is required", }), + code: z.optional(z.string()), }); export const RegisterSchema = z.object({ From ccf9c8ff6de1ac18479a119ff813f317d89d90b2 Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Mon, 5 Feb 2024 10:30:35 +0530 Subject: [PATCH 11/19] feat: userButton added to the settings page --- .gitignore | 5 +- README.md | 1517 ------------------------ actions/auth/logout.ts | 7 + app/(protected)/_components/navbar.tsx | 52 + app/(protected)/layout.tsx | 16 + app/(protected)/settings/page.tsx | 13 +- app/layout.tsx | 15 +- components/auth/logout-button.tsx | 22 + components/auth/user-button.tsx | 43 + components/ui/avatar.tsx | 50 + components/ui/dropdown-menu.tsx | 205 ++++ hooks/use-current-user.ts | 7 + package-lock.json | 665 +++++++++++ package.json | 2 + 14 files changed, 1089 insertions(+), 1530 deletions(-) create mode 100644 actions/auth/logout.ts create mode 100644 app/(protected)/_components/navbar.tsx create mode 100644 app/(protected)/layout.tsx create mode 100644 components/auth/logout-button.tsx create mode 100644 components/auth/user-button.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 hooks/use-current-user.ts diff --git a/.gitignore b/.gitignore index 393e608..d1eed89 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts -.env \ No newline at end of file +.env + +# hiding tutorial readme +hiddenReadme.md \ No newline at end of file diff --git a/README.md b/README.md index 5dfd9c9..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,1517 +0,0 @@ -# NextJs V5 Authentication - -![image](https://github.com/CodeMaster17/role-based-authentication-Authjs/assets/96763776/7d84ec51-ca1b-4a20-839b-1262523fc51b) - -7. Create Register page UI -8. Install prisma - `npm i -D prisma` -9. install prisma client - `npm i @prisma/client` -10. Go to `database.connection.ts` and add following code - -``` -import { PrismaClient } from "@prisma/client"; -declare global { - var prisma: PrismaClient | undefined; -} -export const db = globalThis.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== "production") globalThis.prisma = db - -``` - -11. Run command - `npx prisma init` -12. We used `Neon DB` as our database -13. Got to `Neon DB ` to create a new database -14. Paste connection string in `schema.prisma` and `.env` file -15. Start creating schema in `schema.prisma` file -16. With help of `db` in `database.connection.ts` file we can access our models -17. Create `User` model in `schema.prisma` file -18. Should also run following command to access `User` model in `database.connection.ts` file - `npx prisma generate` -19. To psuh your schema to database - `npx prisma db push` - -20. Move to `Auth JS site` -21. Select database adapter as `Prisma` -22. Install `@auth/prisma-adapter` -23. Comand to install `@auth/prisma-adapter` - `npm i @auth/prisma-adapter` -24. Copy model `User` and paste in `schema.prisma` file -25. Copy model `Account` and paste in `schema.prisma` file ( We are not using session model from `Auth JS site`) -26. Push again to database - `npx prisma generate` and `npx prisma db push` -27. Auth does not use `password` field in `User` model. So we need to add it to user model as optional because google aut h providers do not require password in `schema.prisma` file. -28. Add the code below to validate fields in `register.ts` - -``` -"use server"; - -import { RegisterSchema } from "@/schema"; -import * as z from "zod"; - -export const register = async (values: z.infer) => { -const validatedFields = RegisterSchema.safeParse(values); // safeParse returns a ZodResult object, and it is used to validate the input values -if (!validatedFields.success) { - return { error: "Invalid fields!" }; -} -return { success: "Email sent!" }; -}; - -``` - -29. Install `bcrypt` to hash password - `npm i bcrypt` and `npm i -D @types/bcrypt` -30. Create register function - -``` -export const register = async (values: z.infer) => { - - // * check and store user in database - - const validatedFields = RegisterSchema.safeParse(values); // safeParse returns a ZodResult object, and it is used to validate the input values - if (!validatedFields.success) { - return { error: "Invalid fields!" }; - } - - const { email, password, name } = validatedFields.data; - const hashedPassword = await bcrypt.hash(password, 10); // 10 is the number of salt rounds - - //finding the email in database - const exisitingUser = await db.user.findUnique({ - where: { - email, - }, - }); - - // if user already exists, return error - if (exisitingUser) { - return { error: "Email already exists!" }; - } - - // if not, create and save it in database - await db.user.create({ - data: { - name, - email, - password: hashedPassword, - }, - }); - - // TODO: send verification email - - return { success: "Email sent!" }; -}; - -``` - -31. Create user actions in `user.action.ts` file, to get user by email and id - -``` -export const getUserByEmail = async (email: string) => { - try { - const user = await db.user.findUnique({ where: { email } }); - return user; - } catch { - return null; - } -}; - -export const getUserById = async (id: string) => { - try { - const user = await db.user.findUnique({ where: { id } }); - return user; - } catch { - return null; - } -}; - -``` - -32. Use it in `register.ts` function - -``` - //finding the email in database - const exisitingUser = await getUserByEmail(email) - -``` - -33. Now, for `login` we have to install nextauth v5 - `npm i next-auth@beta` -34. Create `auth.ts` file in root directory for configuration -35. Add following code to `auth.ts` file - - paste the code from website -36. Create `app/api/auth/[...nextauth].ts` file and paste the code from the wesbite - -- remove the edge because prisma does not support it - -37. Add `AUTH_SECRET` in `.env` file - -- for the development mode we can use any string - -38. Go to see logs `http://localhost:3000/api/auth/providers` - -## middlewares and login - -39. Create middleware in `middleware.ts` in root directory - -- `middleware.ts` file is nextjs specific file -- update matcher to ` matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],` from clerk - -40. Create `auth.config.ts` file in root directory - -- paste the code from website - -41. Update `auth.ts` file - -``` -// * Comnfiguration for authentication -import NextAuth from "next-auth"; -import authConfig from "@/auth.config"; -import { db } from "./lib/database.connection"; -import { PrismaAdapter } from "@auth/prisma-adapter"; -export const { - handlers: { GET, POST }, - auth, -} = NextAuth({ - adapter: PrismaAdapter(db), // prisma adapter is supported on non edge - session: { strategy: "jwt" }, - ...authConfig, -}); - - -``` - -42. Update `api/auth/[...nextauth].ts` file - -``` -// * middleware works on the edge - -import authConfig from "./auth.config"; -import NextAuth from "next-auth"; - -const { auth } = NextAuth(authConfig); - -export default auth((req) => { - // req.auth -}); - -// Optionally, don't invoke Middleware on some paths -export const config = { - matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], -}; - -``` - -43. Create `route.ts` file in root directory - -- this file will contain all types of routes - -44. Edit middleware.ts file - - - add routes condition in `middleware.ts` file - -45. Do valdiations in `auth.config.ts` file - -46. Edit `auth.ts` file - -``` -export const { - handlers: { GET, POST }, - auth, - signIn, - signOut, -} = NextAuth({ - adapter: PrismaAdapter(db), // prisma adapter is supported on non edge - session: { strategy: "jwt" }, - ...authConfig, -}); - -``` - -47. Implement functionality in `login.ts` file - -``` -"use server"; // necessary in every auth action - -import * as z from "zod"; -import { LoginSchema } from "@/schema"; -import { signIn } from "@/auth"; -import { DEFAULT_LOGIN_REDIRECT } from "@/route"; -import { AuthError } from "next-auth"; - -export const Login = async (values: z.infer) => { - const validatedFields = LoginSchema.safeParse(values); // valdiating the input values - if (!validatedFields.success) { - return { error: "Invalid fields! " }; - } - const { email, password } = validatedFields.data; - try { - await signIn("credentials", { - email, - password, - redirectTo: DEFAULT_LOGIN_REDIRECT, - }); - } catch (error) { - if (error instanceof AuthError) { - switch (error.type) { - case "CredentialsSignin": - return { error: "Invalid credentials!" }; - default: - return { error: "Something went wrong!" }; - } - } - throw error; - } -}; - -``` - -48. Go to settings page and add logout button, and its functionality in `settings.tsx` file - -``` -import { auth, signOut } from '@/auth' -import React from 'react' - -const Settings = async () => { - const user = await auth() - return ( -
- {JSON.stringify(user)} -
{ - 'use server' - await signOut() - }}> - -
-
- ) -} - -export default Settings - -``` - -#### Login/Logout completed succesfully - -## Callbacks - -49. add callbacks in `auth.ts` - to check for tokens - `export const { -handlers: { GET, POST }, -auth, -signIn, -signOut, -} = NextAuth({ -callbacks: { -async jwt({ token }) { -console.log({ token }); -return token; -}, -}, -adapter: PrismaAdapter(db), // prisma adapter is supported on non edge -session: { strategy: "jwt" }, -...authConfig, -});` - -50. update callback in `auth.ts` file - -``` - async session({ token, session }) { - if (token.sub && session.user) { - session.user.id = token.sub; - } - - return session; - }, -``` - -51. Update schema in `schema.prisma` file - -``` -role UserRole @default(USER) -``` - -52. Close the server - - run command `npx prisma generate` and then `npx prisma migrate reset` and then `npx prisma db push` - - you can check the db status, users would be 0 - -### Role based authentication is developed with help of middleware and token in callback - -### Query is much faste in case of finding by id rather than by an email - -53. Update the callback in `auth.ts` file - -``` -async session({ token, session }) { - console.log({ - sessionToken: token, - }); - - if (token.sub && session.user) { - session.user.id = token.sub; - } - - return session; - }, - - async jwt({ token }) { - // fecthing the user - - if (!token.sub) return token; - const exisitingUser = await getUserById(token.sub); - if (!exisitingUser) return token; - token.role = exisitingUser.role; - - return token; - }, - -``` - -#### you must be start seeing the role in the token - -54. Update session function - -``` -async session({ token, session }) { - - if (token.sub && session.user) { - session.user.id = token.sub; - } - - if(token.role && session.user){ - session.user.role = token.role // you will be seeing here a typescript error - } - - return session; - }, -``` - -55. Update in `auth.ts` file - -``` -if (token.role && session.user) { - session.user.role = token.role as UserRole; - } -``` - -- and create a `next-auth.d.ts` file in root directory -- and paste the code - -``` -import { UserRole } from "@prisma/client"; -import NextAuth from "next-auth"; - -export type ExtendedUser = DefaultSession["user"] & { -role: UserRole; // this is the type of role -}; - -declare module "next-auth" { -interface Session { - user: ExtendedUser; -} -} - -``` - -#### Now you can see the role of the user in the session - -## Adding Google and Github providers - -56. Add google and github providers inside providers in `auth.config.ts` file from - -``` -import Github from "next-auth/providers/github"; -import Google from "next-auth/providers/google"; -``` - -``` - Github({ - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - }), - Google({ - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - }), -``` - -57. Add secrets to env file - - - `GOOGLE_CLIENT_ID` - - `GOOGLE_CLIENT_SECRET` - - `GITHUB_CLIENT_ID` - - `GITHUB_CLIENT_SECRET` - -58. Test google and github in `social.tsx` - -``` -"use client"; - -import { signIn } from "next-auth/react"; // this is we have to import when in client side -import { FcGoogle } from "react-icons/fc"; -import { FaGithub } from "react-icons/fa"; -// import { useSearchParams } from "next/navigation"; - -import { Button } from "@/components/ui/button"; -import { DEFAULT_LOGIN_REDIRECT } from "@/route"; - -export const Social = () => { - // const searchParams = useSearchParams(); - // const callbackUrl = searchParams.get("callbackUrl"); - - const onClick = (provider: "google" | "github") => { - signIn(provider, { - callbackUrl: DEFAULT_LOGIN_REDIRECT, - }); - } - - return ( -
- - -
- ); -}; - -``` - -59. linkAccount from Auth.js site can be used to directly register in case of google and github when the user does not exist and and there will be no need for register page - -- update auth.ts `events` - -``` - // * This is for linkAccount feature - events:{ - async linkAccount({user}){ - await db.user.update({ - where :{id : user.id}, - data : {emailVerified: new Date()} - }) - } - }, -``` - -60. Add this too - -``` - // * This is for solving errors when using linkAccount feature - pages: { - signIn: "/auth/login", - error: "/auth/error", - }, -``` - -61. Create 2 new files, error-card.tsx and error.tsx in `auth`folder and `components/auth` folder respectively - -62. Update `routes.ts` file to - -``` -export const authRoutes = ["/auth/login", "/auth/register", "/auth/error"]; -``` - -63. Get the error of 59 and display it in login form itself in `LoginForm.tsx` file - -``` - // * for getting search params for error in case not logged in - const searchParams = useSearchParams() - const urlError = searchParams.get("error") === "OAuthAccountNotLinked" ? "Email already with different mail provider" : "" - -``` - -and - -``` - -``` - -#### Oauth completed here. - -## Email verification for OAuth users - -64. Go to `schema.prisma` to add new model for `VerificiationToken` - -65. Push the schema to database - `npx prisma generate` and `npx prisma db push` - -66. create `verification-token.ts` file in `lib/actions/auth` folder - -``` -import { db } from "@/lib/database.connection"; - -export const getVerificationTokenByToken = async (token: string) => { - try { - const verificationToken = await db.verificationToken.findUnique({ - where: { token }, - }); - - return verificationToken; - } catch { - return null; - } -}; - -export const getVerificationTokenByEmail = async (email: string) => { - try { - const verificationToken = await db.verificationToken.findFirst({ - where: { email }, - }); - - return verificationToken; - } catch { - return null; - } -}; -``` - -67. Create a `lib` to generate token in `lib/token` file - -- For generating token we use `uuid` package -- `npm i uuid` and `npm i --save-dev @types/uuid` - -``` -import crypto from "crypto"; -import { v4 as uuidv4 } from "uuid"; -import { db } from "./database.connection"; -import { getVerificationTokenByEmail } from "./actions/auth/verification-token"; - -export const generateVerificationToken = async (email: string) => { - const token = uuidv4(); - const expires = new Date(new Date().getTime() + 3600 * 1000); - - - const existingToken = await getVerificationTokenByEmail(email); - - if (existingToken) { - await db.verificationToken.delete({ - where: { - id: existingToken.id, - }, - }); - } - - const verficationToken = await db.verificationToken.create({ - data: { - email, - token, - expires, - }, - }); - - return verficationToken; -}; - - -``` - -68. Now generate the token when user is created - -- Do it in `register.ts` file - -``` -const verificationToken = await generateVerificationToken(email); -``` - -69. Not allowing the user to login until he verifies his email - for that: - -``` - // * not allowing the user to login if the email is not verified - const exisitingUser = await getUserByEmail(email); - if (!exisitingUser || !exisitingUser.password || !exisitingUser.email) { - return { error: "Email does not exist" }; - } - - if (!exisitingUser.emailVerified) { - const verificationToken = await generateVerificationToken( - exisitingUser.email - ); - return { success: "Confirmation Email sent!" }; - } - // * -``` - -70. adding callback to `auth.ts` file - -``` - async signIn({ user, account }) { - if (account?.provider !== "credentials") return true; - - const exisitingUser = await getUserById(user.id); - - if (!exisitingUser?.emailVerified) return false; - - // TODO : Add 2FA check - - return true; - }, -``` - -71. Setting up mail provider - -- using `resend` -- `npm install resned` -- add api key in .env file -- before hosting, we can only send mail to oursleves, by the account which we have created in resend -- after hosting and adding domain of '.com' we can send mail to anyone, but that also has some limitations -- create `mail.ts` file in `lib` folder - -``` -import { Resend } from "resend"; -const resend = new Resend(process.env.RESEND_API_KEY); -export const sendVerificationEmail = async (email: string, token: string) => { - const confirmLink = `http://localhost:3000/auth/new-verification?token=${token}`; - - await resend.emails.send({ - from: "onboarding@resend.dev", - to: email, - subject: "Confirm your email", - html: `

Click here to confirm email.

`, - }); -}; - -``` - -73. Sending mail while logging in too, if email is not verfied in `login.ts` file - -``` -// * sending mail while logging in if email is not verified - await sendVerificationEmail( - verificationToken.email, - verificationToken.token - ); -``` - -74. Add new route to `routes.ts` file i.e. `export const publicRoutes = ["/", "/auth/new-verfiiation"];` -75. Create `new-verification.tsx` file in `pages/auth` folder -76. Create `new-verification-form.tsx` file in `components/auth` folder - -``` -'use client' -import React, { useCallback, useEffect, useState } from 'react' -import { CardWrapper } from './card-wrapper' -import { BeatLoader } from 'react-spinners' -import { useSearchParams } from 'next/navigation' - -const NewVerficationForm = () => { - - const [error, setError] = useState(); - const [success, setSuccess] = useState(); - - const searchParams = useSearchParams(); - - const token = searchParams.get("token"); - - const onSubmit = useCallback(() => { - if (success || error) return; - - console.log("token", token) - - if (!token) { - setError("Missing token!"); - return; - } - - // newVerification(token) - // .then((data) => { - // setSuccess(data.success); - // setError(data.error); - // }) - // .catch(() => { - // setError("Something went wrong!"); - // }) - }, [token, success, error]); - - useEffect(() => { - onSubmit(); - }, [onSubmit]); - return ( - -
- -
-
- ) -} - -export default NewVerficationForm - -``` - -77. Create new action in `/actions/auth` folder - -``` -"use server"; - -import { getVerificationTokenByToken } from "@/lib/actions/auth/verification-token"; -import { getUserByEmail } from "@/lib/actions/user.action"; -import { db } from "@/lib/database.connection"; - - - -export const newVerification = async (token: string) => { - const existingToken = await getVerificationTokenByToken(token); - - if (!existingToken) { - return { error: "Token does not exist!" }; - } - - const hasExpired = new Date(existingToken.expires) < new Date(); - - if (hasExpired) { - return { error: "Token has expired!" }; - } - - const existingUser = await getUserByEmail(existingToken.email); - - if (!existingUser) { - return { error: "Email does not exist!" }; - } - - // when user updates his email, we create a token and send it to new mail, when user verifies it, we update the email - await db.user.update({ - where: { id: existingUser.id }, - data: { - emailVerified: new Date(), - email: existingToken.email, - }, - }); - - await db.verificationToken.delete({ - where: { id: existingToken.id }, - }); - - return { success: "Email verified!" }; -}; -``` - -78. Uncomment the lines code in `new-verification-form.tsx` file -79. To show loader and message, add the following code in `new-verification-form.tsx` file - -``` -
- {!success && !error && ( - - )} - - {!success && ( - - )} -
- -``` - -## Reset password email functionality - -80. Create a new page `/auth/reset` in which you need to import the form from `components/auth` folder - -81. Create a new form in `components/auth` folder i.e `reset-form.tsx` which will only contain the email field - -82. Create a new action in `/actions/auth` folder i.e `reset.ts` file - -``` -"use server"; - -import { getUserByEmail } from "@/lib/actions/user.action"; -import { ResetSchema } from "@/schema"; -import * as z from "zod"; - -export const reset = async (values: z.infer) => { - const validatedFields = ResetSchema.safeParse(values); - - if (!validatedFields.success) { - return { error: "Invalid emaiL!" }; - } - - const { email } = validatedFields.data; - - const existingUser = await getUserByEmail(email); - - if (!existingUser) { - return { error: "Email not found!" }; - } - - // const passwordResetToken = await generatePasswordResetToken(email); - // await sendPasswordResetEmail( - // passwordResetToken.email, - // passwordResetToken.token - // ); - - return { success: "Reset email sent!" }; -}; - -``` - -83. In `reset-form.tsx` file, add the following code - -``` -"use client"; - -import * as z from "zod"; -import { useForm } from "react-hook-form"; -import { useState, useTransition } from "react"; -import { zodResolver } from "@hookform/resolvers/zod"; - -import { Input } from "@/components/ui/input"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { CardWrapper } from "@/components/auth/card-wrapper" -import { Button } from "@/components/ui/button"; -import { FormError } from "@/components/form-error"; -import { ResetSchema } from "@/schema"; -import { FormSuccess } from "../form-sucess"; -import { reset } from "@/actions/auth/reset"; - - -export const ResetForm = () => { - const [error, setError] = useState(""); - const [success, setSuccess] = useState(""); - const [isPending, startTransition] = useTransition(); - - const form = useForm>({ - resolver: zodResolver(ResetSchema), - defaultValues: { - email: "", - }, - }); - - const onSubmit = (values: z.infer) => { - setError(""); - setSuccess(""); - - startTransition(() => { - reset(values) - .then((data) => { - setError(data?.error); - setSuccess(data?.success); - }); - }); - }; - - return ( - -
- -
- ( - - Email - - - - - - )} - /> -
- - - - - -
- ); -}; -``` - -84. Also create a new schema in `schema/index.ts` file - -``` -// reset schema -export const ResetSchema = z.object({ - email: z.string().email({ - message: "Email is required", - }), -}); - -``` - -85. Add the schema in `schema.prisma` file - -``` -// * for password reset -model PasswordResetToken { - id String @id @default(cuid()) - email String - token String @unique - expires DateTime - - @@unique([email, token]) -} - - -``` - -86. Now run the following command - `npx prisma generate` and `npx prisma db push` - -87. Create a new action in `lib/actions/auth` folder i.e `password-reset-token.ts` file using email and token - -``` -import { db } from "@/lib/database.connection"; - -export const getPasswordResetTokenByToken = async (token: string) => { -try { - const passwordResetToken = await db.passwordResetToken.findUnique({ - where: { token }, - }); - - return passwordResetToken; -} catch { - return null; -} -}; - -export const getPasswordResetTokenByEmail = async (email: string) => { -try { - const passwordResetToken = await db.passwordResetToken.findFirst({ - where: { email }, - }); - - return passwordResetToken; -} catch { - return null; -} -}; -``` - -88. Create a new file `token.ts` in `lib` folder - -``` -import crypto from "crypto"; -import { v4 as uuidv4 } from "uuid"; -import { db } from "./database.connection"; -import { getVerificationTokenByEmail } from "./actions/auth/verification-token"; -import { getPasswordResetTokenByEmail } from "./actions/auth/password-reset-token"; - -export const generateVerificationToken = async (email: string) => { - const token = uuidv4(); - const expires = new Date(new Date().getTime() + 3600 * 1000); // * expiring in 1 hour - - const existingToken = await getVerificationTokenByEmail(email); - - if (existingToken) { - await db.verificationToken.delete({ - where: { - id: existingToken.id, - }, - }); - } - - const verficationToken = await db.verificationToken.create({ - data: { - email, - token, - expires, - }, - }); - - return verficationToken; -}; - - -// generating password reset token -export const generatePasswordResetToken = async (email: string) => { - const token = uuidv4(); - const expires = new Date(new Date().getTime() + 3600 * 1000); - - const existingToken = await getPasswordResetTokenByEmail(email); - - if (existingToken) { - await db.passwordResetToken.delete({ - where: { id: existingToken.id }, - }); - } - - const passwordResetToken = await db.passwordResetToken.create({ - data: { - email, - token, - expires, - }, - }); - - return passwordResetToken; -}; -``` - -89. Write mail to send mail verification in `mail.ts` file - -``` -// sending password reset email -export const sendPasswordResetEmail = async (email: string, token: string) => { - const resetLink = `http://localhost:3000/auth/new-password?token=${token}`; - - await resend.emails.send({ - from: "onboarding@resend.dev", - to: email, - subject: "Reset your password", - html: `

Click here to reset password.

`, - }); -}; -``` - -90. Uncomment the code in `reset.ts` file - -## Reset Password Form - -91. Add new route in `routes.ts` file i.e `export const resetRoutes = ["/auth/reset", "/auth/new-password"];` -92. Create the new schema in `schema/index.ts` file - -``` -// new password schema -export const NewPasswordSchema = z.object({ - password: z.string().min(6, { - message: "Minimum of 6 characters required", - }), -}); - -``` - -93. Create a new file in `components/auth` folder i.e `new-password-form.tsx` file which will be imported to `/auth/new-password.tsx` file - -``` -'use client' -import React, { useState, useTransition } from 'react' -import { CardWrapper } from './card-wrapper'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'; -import { Input } from "@/components/ui/input"; -import { useForm } from 'react-hook-form'; -import { NewPasswordSchema } from '@/schema'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; - -import { FormError } from '../form-error'; -import { FormSuccess } from '../form-sucess'; -import { Button } from '../ui/button'; -import { useSearchParams } from 'next/navigation'; -import { newPassword } from '@/actions/auth/new-password'; - -const NewPasswordForm = () => { - const searchParams = useSearchParams(); - const token = searchParams.get("token"); - - const [error, setError] = useState(""); - const [success, setSuccess] = useState(""); - const [isPending, startTransition] = useTransition(); - - const form = useForm>({ - resolver: zodResolver(NewPasswordSchema), - defaultValues: { - password: "", - }, - }); - - const onSubmit = (values: z.infer) => { - setError(""); - setSuccess(""); - - startTransition(() => { - newPassword(values, token) - .then((data) => { - setError(data?.error); - setSuccess(data?.success); - }); - }); - }; - - return ( - -
- -
- ( - - Password - - - - - - )} - /> -
- - - - - -
- ) -} - -export default NewPasswordForm - -``` - -94. Create a new action in `/actions/auth` folder i.e `new-password.ts` file - -``` -"use server"; - -import * as z from "zod"; -import bcrypt from "bcryptjs"; -import { NewPasswordSchema } from "@/schema"; -import { getPasswordResetTokenByToken } from "@/lib/actions/auth/password-reset-token"; -import { getUserByEmail } from "@/lib/actions/user.action"; -import { db } from "@/lib/database.connection"; - - -export const newPassword = async ( - values: z.infer, - token?: string | null -) => { - if (!token) { - return { error: "Missing token!" }; - } - - const validatedFields = NewPasswordSchema.safeParse(values); - - if (!validatedFields.success) { - return { error: "Invalid fields!" }; - } - - const { password } = validatedFields.data; - - const existingToken = await getPasswordResetTokenByToken(token); - - if (!existingToken) { - return { error: "Invalid token!" }; - } - - const hasExpired = new Date(existingToken.expires) < new Date(); - - if (hasExpired) { - return { error: "Token has expired!" }; - } - - const existingUser = await getUserByEmail(existingToken.email); - - if (!existingUser) { - return { error: "Email does not exist!" }; - } - - const hashedPassword = await bcrypt.hash(password, 10); - - await db.user.update({ - where: { id: existingUser.id }, - data: { password: hashedPassword }, - }); - - await db.passwordResetToken.delete({ - where: { id: existingToken.id }, - }); - - return { success: "Password updated!" }; -}; -``` - -It shoul now start working. - -## 2Factor Authentication - -95. Update `schema.prisma` file - -- user model - -``` -model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - password String? - accounts Account[] - role UserRole @default(USER) - isTwoFactorEnabled Boolean @default(false) - twoFactorConfirmation TwoFactorConfirmation? -} -``` - -- isTwoFactorEnabled model - -``` -model TwoFactorToken { - id String @id @default(cuid()) - email String - token String @unique - expires DateTime - - @@unique([email, token]) -} -``` - -- TwoFactorConfirmation model - -``` -model TwoFactorConfirmation { - id String @id @default(cuid()) - - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([userId]) -} -``` - -96. Now run the following command - `npx prisma generate` and `npx prisma db push` - -97. # Setting up two-factor-token - -- Create a new file in `lib/actions/auth` folder i.e `two-factor-token.ts` file - -``` -import { db } from "@/lib/database.connection"; - -export const getTwoFactorTokenByToken = async (token: string) => { - try { - const twoFactorToken = await db.twoFactorToken.findUnique({ - where: { token }, - }); - - return twoFactorToken; - } catch { - return null; - } -}; - -export const getTwoFactorTokenByEmail = async (email: string) => { - try { - const twoFactorToken = await db.twoFactorToken.findFirst({ - where: { email }, - }); - - return twoFactorToken; - } catch { - return null; - } -}; -``` - -- Setting up the action for confirming two-factor token in `lib/actions/auth` folder `two-factor-confirmation.ts` file - -``` -import { db } from "@/lib/database.connection"; - -export const getTwoFactorConfirmationByUserId = async (userId: string) => { - try { - const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({ - where: { userId }, - }); - - return twoFactorConfirmation; - } catch { - return null; - } -}; - -``` - -- Generating Two factor token in `lib/token` file - -``` -export const generateTwoFactorToken = async (email: string) => { - const token = crypto.randomInt(100_000, 1_000_000).toString(); - const expires = new Date(new Date().getTime() + 5 * 60 * 1000); - - const existingToken = await getTwoFactorTokenByEmail(email); - - if (existingToken) { - await db.twoFactorToken.delete({ - where: { - id: existingToken.id, - } - }); - } - - const twoFactorToken = await db.twoFactorToken.create({ - data: { - email, - token, - expires, - } - }); - - return twoFactorToken; -} -``` - -- Sned two factor token email - -``` -export const sendTwoFactorTokenEmail = async ( - email: string, - token: string -) => { - await resend.emails.send({ - from: "mail@auth-masterclass-tutorial.com", - to: email, - subject: "2FA Code", - html: `

Your 2FA code: ${token}

` - }); -}; -``` - -98. Go to prisma studio and enable the 2FA for a user - -99. Modify the login function in `auth.ts` file - -``` - - // * Prevent sign in without two factor confirmation (99) - if (existingUser.isTwoFactorEnabled) { - const twoFactorConfirmation = await getTwoFactorConfirmationByUserId( - existingUser.id - ); - - if (!twoFactorConfirmation) return false; - - // Delete two factor confirmation for next sign in - await db.twoFactorConfirmation.delete({ - where: { id: twoFactorConfirmation.id }, - }); - } -``` - -100. Add 2FA Verification in `login.ts` file - -``` - //* 2FA verification - if (exisitingUser.isTwoFactorEnabled && exisitingUser.email) { - if (code) { - const twoFactorToken = await getTwoFactorTokenByEmail(exisitingUser.email); - - if (!twoFactorToken) { - return { error: "Invalid code!" }; - } - - if (twoFactorToken.token !== code) { - return { error: "Invalid code!" }; - } - - const hasExpired = new Date(twoFactorToken.expires) < new Date(); - - if (hasExpired) { - return { error: "Code expired!" }; - } - - await db.twoFactorToken.delete({ - where: { id: twoFactorToken.id }, - }); - - const existingConfirmation = await getTwoFactorConfirmationByUserId( - exisitingUser.id - ); - - if (existingConfirmation) { - await db.twoFactorConfirmation.delete({ - where: { id: existingConfirmation.id }, - }); - } - - await db.twoFactorConfirmation.create({ - data: { - userId: exisitingUser.id, - }, - }); - } else { - const twoFactorToken = await generateTwoFactorToken(exisitingUser.email); - await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token); - - return { twoFactor: true }; - } - } -``` - -101. Update the login schema - -``` -export const LoginSchema = z.object({ -email: z.string().email({ - message: "Email is required", -}), -password: z.string().min(1, { - message: "Password is required", -}), -code: z.optional(z.string()), -}); - -``` -102. Update the login form, based on the conditions (see the code directly from file) -- concept is to show 2FA code, when after the login button is clicked, and two factor is enabled -- (might be incomplete) - - - - diff --git a/actions/auth/logout.ts b/actions/auth/logout.ts new file mode 100644 index 0000000..291c72f --- /dev/null +++ b/actions/auth/logout.ts @@ -0,0 +1,7 @@ +"use server"; + +import { signOut } from "@/auth"; + +export const logout = async () => { + await signOut(); +}; diff --git a/app/(protected)/_components/navbar.tsx b/app/(protected)/_components/navbar.tsx new file mode 100644 index 0000000..cde007f --- /dev/null +++ b/app/(protected)/_components/navbar.tsx @@ -0,0 +1,52 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { Button } from "@/components/ui/button"; +import { UserButton } from "@/components/auth/user-button"; + + +export const Navbar = () => { + const pathname = usePathname(); + + return ( + + ); +}; diff --git a/app/(protected)/layout.tsx b/app/(protected)/layout.tsx new file mode 100644 index 0000000..a3e02e9 --- /dev/null +++ b/app/(protected)/layout.tsx @@ -0,0 +1,16 @@ +import { Navbar } from "./_components/navbar"; + +interface ProtectedLayoutProps { + children: React.ReactNode; +}; + +const ProtectedLayout = ({ children }: ProtectedLayoutProps) => { + return ( +
+ + {children} +
+ ); +} + +export default ProtectedLayout; \ No newline at end of file diff --git a/app/(protected)/settings/page.tsx b/app/(protected)/settings/page.tsx index 1c5686a..98c29d4 100644 --- a/app/(protected)/settings/page.tsx +++ b/app/(protected)/settings/page.tsx @@ -7,14 +7,11 @@ const Settings = async () => { return (
{JSON.stringify(session)} -
{ - 'use server' - await signOut() // exclusively for server actions - }}> - -
+ + +
) } diff --git a/app/layout.tsx b/app/layout.tsx index 31adc05..f8eda7a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' +import { SessionProvider } from 'next-auth/react' +import { auth } from '@/auth' const inter = Inter({ subsets: ['latin'] }) @@ -9,15 +11,20 @@ export const metadata: Metadata = { description: 'Generated by create next app', } -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode }) { + + const session = await auth(); return ( - - {children} - + + + + {children} + + ) } diff --git a/components/auth/logout-button.tsx b/components/auth/logout-button.tsx new file mode 100644 index 0000000..32e6716 --- /dev/null +++ b/components/auth/logout-button.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { logout } from "@/actions/auth/logout"; + + +interface LogoutButtonProps { + children?: React.ReactNode; +}; + +export const LogoutButton = ({ + children +}: LogoutButtonProps) => { + const onClick = () => { + logout(); + }; + + return ( + + {children} + + ); +}; diff --git a/components/auth/user-button.tsx b/components/auth/user-button.tsx new file mode 100644 index 0000000..0ad6b0a --- /dev/null +++ b/components/auth/user-button.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { FaUser } from "react-icons/fa"; +import { ExitIcon } from "@radix-ui/react-icons" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Avatar, + AvatarImage, + AvatarFallback, +} from "@/components/ui/avatar"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { LogoutButton } from "@/components/auth/logout-button"; + +export const UserButton = () => { + const user = useCurrentUser(); + + return ( + + + + + + + + + + + + + + Logout + + + + + ); +}; diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..242b07a --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,205 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/hooks/use-current-user.ts b/hooks/use-current-user.ts new file mode 100644 index 0000000..8a00036 --- /dev/null +++ b/hooks/use-current-user.ts @@ -0,0 +1,7 @@ +import { useSession } from "next-auth/react"; + +export const useCurrentUser = () => { + const session = useSession(); + + return session.data?.user; +}; diff --git a/package-lock.json b/package-lock.json index 9e41a1c..3d81397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "@auth/prisma-adapter": "^1.0.13", "@hookform/resolvers": "^3.3.3", "@prisma/client": "^5.7.1", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", @@ -166,6 +168,40 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", + "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@hookform/resolvers": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.3.tgz", @@ -556,6 +592,89 @@ "@prisma/debug": "5.7.1" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", + "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz", + "integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -573,6 +692,138 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", + "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", @@ -581,6 +832,24 @@ "react": "^16.x || ^17.x || ^18.x" } }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-label": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", @@ -604,6 +873,125 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", + "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", + "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-primitive": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", @@ -627,6 +1015,37 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", + "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -645,6 +1064,120 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", + "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", + "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@react-email/render": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.9.tgz", @@ -976,6 +1509,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -2104,6 +2648,11 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -3049,6 +3598,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -4913,6 +5470,51 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", + "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-spinners": { "version": "0.13.8", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", @@ -4922,6 +5524,28 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5861,6 +6485,47 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index eafa576..e38189a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "@auth/prisma-adapter": "^1.0.13", "@hookform/resolvers": "^3.3.3", "@prisma/client": "^5.7.1", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", From 9f828e0f9cf80406b8282d18ed93a7ba20bc8e51 Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Mon, 5 Feb 2024 13:42:00 +0530 Subject: [PATCH 12/19] feat: server client example created --- app/(protected)/client/page.tsx | 16 ++++++++ app/(protected)/server/page.tsx | 16 ++++++++ auth.ts | 10 +++++ components/ui/badge.tsx | 37 +++++++++++++++++ components/user-info.tsx | 73 +++++++++++++++++++++++++++++++++ lib/auth.ts | 12 ++++++ next-auth.d.ts | 1 + 7 files changed, 165 insertions(+) create mode 100644 app/(protected)/client/page.tsx create mode 100644 app/(protected)/server/page.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/user-info.tsx create mode 100644 lib/auth.ts diff --git a/app/(protected)/client/page.tsx b/app/(protected)/client/page.tsx new file mode 100644 index 0000000..bf5a08d --- /dev/null +++ b/app/(protected)/client/page.tsx @@ -0,0 +1,16 @@ +'use client' +import { UserInfo } from '@/components/user-info'; +import { useCurrentUser } from '@/hooks/use-current-user'; +import React from 'react' + +const ClientPage = () => { + const user = useCurrentUser(); + console.log(user); + return ( +
+ +
+ ) +} + +export default ClientPage diff --git a/app/(protected)/server/page.tsx b/app/(protected)/server/page.tsx new file mode 100644 index 0000000..6a2b9e7 --- /dev/null +++ b/app/(protected)/server/page.tsx @@ -0,0 +1,16 @@ +import { UserInfo } from '@/components/user-info' +import { currentUser } from '@/lib/auth'; + +import React from 'react' + +const ServerPage = async () => { + const user = await currentUser(); + console.log(user); + return ( +
+ +
+ ) +} + +export default ServerPage diff --git a/auth.ts b/auth.ts index 7e5c0c9..3ebcc68 100644 --- a/auth.ts +++ b/auth.ts @@ -66,6 +66,16 @@ export const { session.user.role = token.role as UserRole; } + if (session.user) { + session.user.isTwoFactorEnabled = token.isTwoFactorEnabled as boolean; + } + + if (session.user) { + session.user.name = token.name; + session.user.email = token.email; + session.user.isOAuth = token.isOAuth as boolean; + } + return session; }, diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..16f550d --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + success: "border-transparent bg-emerald-500 text-primary-foreground" + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { } + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/components/user-info.tsx b/components/user-info.tsx new file mode 100644 index 0000000..edac097 --- /dev/null +++ b/components/user-info.tsx @@ -0,0 +1,73 @@ +import { ExtendedUser } from "@/next-auth"; +import { + Card, + CardContent, + CardHeader +} from "@/components/ui/card"; +import { Badge } from "./ui/badge"; + +interface UserInfoProps { + user?: ExtendedUser; + label: string; +}; + +export const UserInfo = ({ + user, + label, +}: UserInfoProps) => { + return ( + + +

+ {label} +

+
+ +
+

+ ID +

+

+ {user?.id} + +

+
+
+

+ Name +

+

+ {user?.name} +

+
+
+

+ Email +

+

+ {user?.email} +

+
+
+

+ Role +

+

+ {user?.role} +

+
+ +
+

+ Two Factor Authentication +

+ + {user?.isTwoFactorEnabled ? "ON" : "OFF"} + +
+
+
+ ) +} \ No newline at end of file diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..8949a4b --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,12 @@ +import { auth } from "@/auth"; + +export const currentUser = async () => { + const session = await auth(); + return session?.user; +}; + +export const currentRole = async () => { + const session = await auth(); + + return session?.user?.role; +}; diff --git a/next-auth.d.ts b/next-auth.d.ts index 4e405e0..be6a6c5 100644 --- a/next-auth.d.ts +++ b/next-auth.d.ts @@ -3,6 +3,7 @@ import NextAuth from "next-auth"; export type ExtendedUser = DefaultSession["user"] & { role: UserRole; + isTwoFacorEnabled: boolean; }; declare module "next-auth" { From a2466d098aca90befd154168218b6cb842f56e0f Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Mon, 5 Feb 2024 20:07:08 +0530 Subject: [PATCH 13/19] feat: admin panel added --- actions/auth/admin.ts | 14 +++++++ app/(protected)/admin/page.tsx | 74 ++++++++++++++++++++++++++++++++++ app/api/admin/route.ts | 13 ++++++ app/layout.tsx | 4 +- components/auth/role-gate.tsx | 30 ++++++++++++++ components/ui/sonner.tsx | 31 ++++++++++++++ hooks/use-current-role.ts | 7 ++++ package-lock.json | 21 ++++++++++ package.json | 2 + 9 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 actions/auth/admin.ts create mode 100644 app/(protected)/admin/page.tsx create mode 100644 app/api/admin/route.ts create mode 100644 components/auth/role-gate.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 hooks/use-current-role.ts diff --git a/actions/auth/admin.ts b/actions/auth/admin.ts new file mode 100644 index 0000000..72c78dc --- /dev/null +++ b/actions/auth/admin.ts @@ -0,0 +1,14 @@ +"use server"; + +import { currentRole } from "@/lib/auth"; +import { UserRole } from "@prisma/client"; + +export const admin = async () => { + const role = await currentRole(); + + if (role === UserRole.ADMIN) { + return { success: "Allowed Server Action!" }; + } + + return { error: "Forbidden Server Action!" }; +}; diff --git a/app/(protected)/admin/page.tsx b/app/(protected)/admin/page.tsx new file mode 100644 index 0000000..591eabf --- /dev/null +++ b/app/(protected)/admin/page.tsx @@ -0,0 +1,74 @@ +'use client'; +import { admin } from '@/actions/auth/admin'; +import { RoleGate } from '@/components/auth/role-gate'; +import { FormSuccess } from '@/components/form-sucess'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; +import { UserRole } from '@prisma/client'; +import React from 'react' +import { toast } from "sonner"; +const AdminPage = () => { + + // for server side + const onServerActionClick = () => { + admin() + .then((data) => { + if (data.error) { + toast.error(data.error); + } + + if (data.success) { + toast.success(data.success); + } + }) + } + + // for client side + const onApiRouteClick = () => { + fetch("/api/admin") + .then((response) => { + if (response.ok) { + toast.success("Allowed API Route!"); + } else { + toast.error("Forbidden API Route!"); + } + }) + } + + return ( + + +

+ 🔑 Admin +

+
+ + {/* only admin would be able to see this */} + + + +
+

+ Admin-only API Route +

+ +
+ +
+

+ Admin-only Server Action +

+ +
+
+
+ ); +}; + +export default AdminPage diff --git a/app/api/admin/route.ts b/app/api/admin/route.ts new file mode 100644 index 0000000..7a460e9 --- /dev/null +++ b/app/api/admin/route.ts @@ -0,0 +1,13 @@ +import { currentRole } from "@/lib/auth"; +import { UserRole } from "@prisma/client"; +import { NextResponse } from "next/server"; + +export async function GET() { + const role = await currentRole(); + + if (role === UserRole.ADMIN) { + return new NextResponse(null, { status: 200 }); + } + + return new NextResponse(null, { status: 403 }); +} diff --git a/app/layout.tsx b/app/layout.tsx index f8eda7a..04efa4c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import { Inter } from 'next/font/google' import './globals.css' import { SessionProvider } from 'next-auth/react' import { auth } from '@/auth' +import { Toaster } from "@/components/ui/sonner" const inter = Inter({ subsets: ['latin'] }) @@ -22,7 +23,8 @@ export default async function RootLayout({ - {children} + {children} + diff --git a/components/auth/role-gate.tsx b/components/auth/role-gate.tsx new file mode 100644 index 0000000..0a68dcc --- /dev/null +++ b/components/auth/role-gate.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { UserRole } from "@prisma/client"; + +import { useCurrentRole } from "@/hooks/use-current-role"; +import { FormError } from "@/components/form-error"; + +interface RoleGateProps { + children: React.ReactNode; + allowedRole: UserRole; +}; + +export const RoleGate = ({ + children, + allowedRole, +}: RoleGateProps) => { + const role = useCurrentRole(); + + if (role !== allowedRole) { + return ( + + ) + } + + return ( + <> + {children} + + ); +}; diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx new file mode 100644 index 0000000..452f4d9 --- /dev/null +++ b/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/hooks/use-current-role.ts b/hooks/use-current-role.ts new file mode 100644 index 0000000..e9b87c3 --- /dev/null +++ b/hooks/use-current-role.ts @@ -0,0 +1,7 @@ +import { useSession } from "next-auth/react"; + +export const useCurrentRole = () => { + const session = useSession(); + + return session.data?.user?.role; +}; diff --git a/package-lock.json b/package-lock.json index 3d81397..ce8cea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,12 +23,14 @@ "clsx": "^2.1.0", "next": "14.0.4", "next-auth": "^5.0.0-beta.4", + "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", "react-spinners": "^0.13.8", "resend": "^2.1.0", + "sonner": "^1.4.0", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "uuid": "^9.0.1", @@ -4784,6 +4786,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", + "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", + "peerDependencies": { + "next": "*", + "react": "*", + "react-dom": "*" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5886,6 +5898,15 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.0.tgz", + "integrity": "sha512-nvkTsIuOmi9e5Wz5If8ldasJjZNVfwiXYijBi2dbijvTQnQppvMcXTFNxL/NUFWlI2yJ1JX7TREDsg+gYm9WyA==", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", diff --git a/package.json b/package.json index e38189a..3ced21e 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,14 @@ "clsx": "^2.1.0", "next": "14.0.4", "next-auth": "^5.0.0-beta.4", + "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", "react-spinners": "^0.13.8", "resend": "^2.1.0", + "sonner": "^1.4.0", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", "uuid": "^9.0.1", From 8e148bc36647145e5d90ad713aece00780985cd6 Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Mon, 5 Feb 2024 21:03:13 +0530 Subject: [PATCH 14/19] feat: profile update feature added --- actions/auth/settings.ts | 85 ++++++++++ app/(protected)/settings/page.tsx | 247 ++++++++++++++++++++++++++++-- auth.ts | 9 ++ components/ui/select.tsx | 164 ++++++++++++++++++++ components/ui/switch.tsx | 29 ++++ lib/account.ts | 13 ++ next-auth.d.ts | 1 + package-lock.json | 122 +++++++++++++++ package.json | 2 + schema/index.ts | 12 ++ 10 files changed, 670 insertions(+), 14 deletions(-) create mode 100644 actions/auth/settings.ts create mode 100644 components/ui/select.tsx create mode 100644 components/ui/switch.tsx create mode 100644 lib/account.ts diff --git a/actions/auth/settings.ts b/actions/auth/settings.ts new file mode 100644 index 0000000..720da56 --- /dev/null +++ b/actions/auth/settings.ts @@ -0,0 +1,85 @@ +"use server"; +import { update } from "@/auth"; +import { getUserByEmail, getUserById } from "@/lib/actions/user.action"; +import { currentUser } from "@/lib/auth"; +import { db } from "@/lib/database.connection"; +import { sendVerificationEmail } from "@/lib/mail"; +import { generateVerificationToken } from "@/lib/token"; +import { SettingsSchema } from "@/schema"; +import bcrypt from "bcryptjs"; +import * as z from "zod"; + +export const settings = async (values: z.infer) => { + const user = await currentUser(); + + if (!user) { + return { error: "Unauthorized" }; + } + + const dbUser = await getUserById(user.id); + + if (!dbUser) { + return { error: "Unauthorized" }; + } + + // if the user is signed in from google or another account + if (user.isOAuth) { + values.email = undefined; + values.password = undefined; + values.newPassword = undefined; + values.isTwoFactorEnabled = undefined; + } + + // checking validations for email + if (values.email && values.email !== user.email) { + const existingUser = await getUserByEmail(values.email); + + if (existingUser && existingUser.id !== user.id) { + return { error: "Email already in use!" }; + } + + const verificationToken = await generateVerificationToken(values.email); + await sendVerificationEmail( + verificationToken.email, + verificationToken.token + ); + + return { success: "Verification email sent!" }; + } + + // checking validations for password + if (values.password && values.newPassword && dbUser.password) { + const passwordsMatch = await bcrypt.compare( + values.password, + dbUser.password + ); + + if (!passwordsMatch) { + return { error: "Incorrect password!" }; + } + + const hashedPassword = await bcrypt.hash(values.newPassword, 10); + values.password = hashedPassword; + values.newPassword = undefined; + } + + // updating the user + const updatedUser = await db.user.update({ + where: { id: dbUser.id }, + data: { + ...values, + }, + }); + + // updating in the session + update({ + user: { + name: updatedUser.name, + email: updatedUser.email, + isTwoFactorEnabled: updatedUser.isTwoFactorEnabled, + role: updatedUser.role, + }, + }); + + return { success: "Settings Updated!" }; +}; diff --git a/app/(protected)/settings/page.tsx b/app/(protected)/settings/page.tsx index 98c29d4..7136315 100644 --- a/app/(protected)/settings/page.tsx +++ b/app/(protected)/settings/page.tsx @@ -1,19 +1,238 @@ -import { auth, signOut } from '@/auth' -import React from 'react' +"use client"; -const Settings = async () => { - const session = await auth() - console.log(JSON.stringify(session?.user.role)) // session can be possibly null, so do optional chaining - return ( -
- {JSON.stringify(session)} +import * as z from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTransition, useState } from "react"; +import { useSession } from "next-auth/react"; + +import { Switch } from "@/components/ui/switch"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import { + Card, + CardHeader, + CardContent, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +import { + Form, + FormField, + FormControl, + FormItem, + FormLabel, + FormDescription, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { useCurrentUser } from "@/hooks/use-current-user"; +import { FormError } from "@/components/form-error"; + +import { UserRole } from "@prisma/client"; +import { SettingsSchema } from "@/schema"; +import { settings } from "@/actions/auth/settings"; +import { FormSuccess } from "@/components/form-sucess"; - +const SettingsPage = () => { + const user = useCurrentUser(); -
- ) + const [error, setError] = useState(); + const [success, setSuccess] = useState(); + const { update } = useSession(); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(SettingsSchema), + defaultValues: { + password: undefined, + newPassword: undefined, + name: user?.name || undefined, + email: user?.email || undefined, + role: user?.role || undefined, + isTwoFactorEnabled: user?.isTwoFactorEnabled || undefined, + } + }); + + const onSubmit = (values: z.infer) => { + startTransition(() => { + settings(values) + .then((data) => { + if (data.error) { + setError(data.error); + } + + if (data.success) { + update(); + setSuccess(data.success); + } + }) + .catch(() => setError("Something went wrong!")); + }); + } + + return ( + + +

+ ⚙️ Settings +

+
+ +
+ +
+ ( + + Name + + + + + + )} + /> + {user?.isOAuth === false && ( + <> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + ( + + New Password + + + + + + )} + /> + + )} + ( + + Role + + + + )} + /> + {user?.isOAuth === false && ( + ( + +
+ Two Factor Authentication + + Enable two factor authentication for your account + +
+ + + +
+ )} + /> + )} +
+ + + + + +
+
+ + ); } -export default Settings +export default SettingsPage; \ No newline at end of file diff --git a/auth.ts b/auth.ts index 3ebcc68..2b755c7 100644 --- a/auth.ts +++ b/auth.ts @@ -6,12 +6,14 @@ import { PrismaAdapter } from "@auth/prisma-adapter"; import { getUserById } from "./lib/actions/user.action"; import { UserRole } from "@prisma/client"; import { getTwoFactorConfirmationByUserId } from "./lib/actions/auth/two-factor-confirmation"; +import { getAccountByUserId } from "./lib/account"; export const { handlers: { GET, POST }, auth, signIn, signOut, + update, } = NextAuth({ // * This is for solving errors when using linkAccount feature pages: { @@ -85,7 +87,14 @@ export const { if (!token.sub) return token; const exisitingUser = await getUserById(token.sub); if (!exisitingUser) return token; + + const existingAccount = await getAccountByUserId(exisitingUser.id); + + token.isOAuth = !!existingAccount; token.role = exisitingUser.role; + token.name = exisitingUser.name; + token.email = exisitingUser.email; + token.isTwoFactorEnabled = exisitingUser.isTwoFactorEnabled; return token; }, diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..ac2a8f2 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,164 @@ +"use client" + +import * as React from "react" +import { + CaretSortIcon, + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from "@radix-ui/react-icons" +import * as SelectPrimitive from "@radix-ui/react-select" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx new file mode 100644 index 0000000..5f4117f --- /dev/null +++ b/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/lib/account.ts b/lib/account.ts new file mode 100644 index 0000000..48b4693 --- /dev/null +++ b/lib/account.ts @@ -0,0 +1,13 @@ +import { db } from "./database.connection"; + +export const getAccountByUserId = async (userId: string) => { + try { + const account = await db.account.findFirst({ + where: { userId }, + }); + + return account; + } catch { + return null; + } +}; diff --git a/next-auth.d.ts b/next-auth.d.ts index be6a6c5..d2ed698 100644 --- a/next-auth.d.ts +++ b/next-auth.d.ts @@ -4,6 +4,7 @@ import NextAuth from "next-auth"; export type ExtendedUser = DefaultSession["user"] & { role: UserRole; isTwoFacorEnabled: boolean; + isOAuth: boolean; }; declare module "next-auth" { diff --git a/package-lock.json b/package-lock.json index ce8cea0..304a471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@types/bcryptjs": "^2.4.6", "babel-preset-es2015": "^6.24.1", "bcryptjs": "^2.4.3", @@ -594,6 +596,14 @@ "@prisma/debug": "5.7.1" } }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -1048,6 +1058,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", + "integrity": "sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -1066,6 +1119,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", + "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -1136,6 +1218,23 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", @@ -1172,6 +1271,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", diff --git a/package.json b/package.json index 3ced21e..a418a9a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@types/bcryptjs": "^2.4.6", "babel-preset-es2015": "^6.24.1", "bcryptjs": "^2.4.3", diff --git a/schema/index.ts b/schema/index.ts index a34b432..ca6f75b 100644 --- a/schema/index.ts +++ b/schema/index.ts @@ -1,3 +1,4 @@ +import { UserRole } from "@prisma/client"; import * as z from "zod"; export const LoginSchema = z.object({ @@ -35,3 +36,14 @@ export const NewPasswordSchema = z.object({ message: "Minimum of 6 characters required", }), }); + + +// settings page schema +export const SettingsSchema = z.object({ + name: z.optional(z.string()), + isTwoFactorEnabled: z.optional(z.boolean()), + role: z.enum([UserRole.ADMIN, UserRole.USER]), + email: z.optional(z.string().email()), + password: z.optional(z.string().min(6)), + newPassword: z.optional(z.string().min(6)), +}); \ No newline at end of file From 6ba54c6502d9f5c2c25574c8f52fbffb26261b6d Mon Sep 17 00:00:00 2001 From: Harshit Yadav Date: Mon, 5 Feb 2024 21:20:18 +0530 Subject: [PATCH 15/19] feat: middleware updated - (redirect back to last url) domain updated in env --- actions/auth/login.ts | 7 +- app/page.tsx | 5 +- components/auth/login-button.tsx | 63 +++++++++------- components/auth/login-form.tsx | 2 +- components/auth/social.tsx | 8 +- components/ui/dialog.tsx | 122 +++++++++++++++++++++++++++++++ lib/mail.ts | 6 +- middleware.ts | 13 +++- package-lock.json | 37 ++++++++++ package.json | 1 + 10 files changed, 225 insertions(+), 39 deletions(-) create mode 100644 components/ui/dialog.tsx diff --git a/actions/auth/login.ts b/actions/auth/login.ts index c5c45f8..cbead8f 100644 --- a/actions/auth/login.ts +++ b/actions/auth/login.ts @@ -12,7 +12,10 @@ import { getTwoFactorConfirmationByUserId } from "@/lib/actions/auth/two-factor- import { db } from "@/lib/database.connection"; import { getTwoFactorTokenByEmail } from "@/lib/actions/auth/two-factor-token"; -export const Login = async (values: z.infer) => { +export const Login = async ( + values: z.infer, + callbackUrl?: string | null +) => { const validatedFields = LoginSchema.safeParse(values); // valdiating the input values if (!validatedFields.success) { return { error: "Invalid fields! " }; @@ -91,7 +94,7 @@ export const Login = async (values: z.infer) => { await signIn("credentials", { email, password, - redirectTo: DEFAULT_LOGIN_REDIRECT, + redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT, }); } catch (error) { if (error instanceof AuthError) { diff --git a/app/page.tsx b/app/page.tsx index b858511..0f57262 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,8 @@ import { Poppins } from "next/font/google"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; -import LoginButton from "@/components/auth/login-button"; +import { LoginButton } from "@/components/auth/login-button"; + const font = Poppins({ @@ -24,7 +25,7 @@ export default function Home() {

A simple authentication service

- +