From 421ca810bd1cc61ae867e956339002d0d0cea26e Mon Sep 17 00:00:00 2001 From: Shivansh Date: Sat, 19 Oct 2024 03:30:51 +0530 Subject: [PATCH 1/3] email otp based signup --- app/(Customer)/auth/components/signupPage.tsx | 220 +++++++++++------- app/(Customer)/auth/role/page.tsx | 58 +++++ app/api/auth/signup/route.ts | 13 +- components/Navbar/authButtons.tsx | 17 +- components/ui/form.tsx | 178 ++++++++++++++ components/ui/input-otp.tsx | 71 ++++++ components/ui/label.tsx | 26 +++ lib/auth.ts | 73 +++++- lib/nodemailerConfig.ts | 14 ++ lib/sendEmail.ts | 23 ++ package-lock.json | 74 +++++- package.json | 7 +- prisma/schema.prisma | 53 +++-- tailwind.config.ts | 25 +- 14 files changed, 720 insertions(+), 132 deletions(-) create mode 100644 app/(Customer)/auth/role/page.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/label.tsx create mode 100644 lib/nodemailerConfig.ts create mode 100644 lib/sendEmail.ts diff --git a/app/(Customer)/auth/components/signupPage.tsx b/app/(Customer)/auth/components/signupPage.tsx index 8ecec7b..6b96afb 100644 --- a/app/(Customer)/auth/components/signupPage.tsx +++ b/app/(Customer)/auth/components/signupPage.tsx @@ -1,77 +1,130 @@ -import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { AtSign, Phone, User, Eye, EyeOff } from "lucide-react"; // Import Eye and EyeOff +import { AtSign, Phone, User} from "lucide-react"; // Import Eye and EyeOff import React, { useState } from "react"; import toast from "react-hot-toast"; -import axios from "axios" +import axios from "axios"; import { Spinner } from "@/components/ui/spinner"; +import { z } from "zod"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@/components/ui/input-otp"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { signIn } from "next-auth/react"; interface SignupPageProps { switchCss: boolean; setSwitchCss: (value: boolean) => void; setError: (value: string) => void; - loading:boolean, - setloading:(value:boolean)=>void; + loading: boolean; + setloading: (value: boolean) => void; } -const SignupPage: React.FC =({ switchCss, setSwitchCss,setError ,loading,setloading}) => { +// form schema otp +const FormSchema = z.object({ + pin: z.string().min(6, { + message: "Your one-time password must be 6 characters.", + }), +}); + +const SignupPage: React.FC = ({ + switchCss, + setSwitchCss, + setError, + loading, + setloading, +}) => { + const [otpOpen, setOtpOpen] = useState(false); // State for form fields const [fullname, setFullname] = useState(""); const [mobileNumber, setmobileNumber] = useState(""); const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - // State for showing passwords - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + // form definition otp + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + pin: "", + }, + }); + + async function onOTPSubmit(data: z.infer) { + // console.log(data.pin+email); + setloading(true); + + const result = await signIn("credentials", { + email, + otp: data.pin, + role: "user", + redirect: false, + }); + console.log(result); + if (!result?.ok) { + setError("Invalid email or otp"); + } else { + toast.success(`Welcome!`); + + setTimeout(() => { + window.location.href = "/"; // Redirect on success + }, 2000); + } + setloading(false); + } // Handle form submission - const handleSubmit = async(e: React.FormEvent) => { - setloading(true) + const handleSubmit = async (e: React.FormEvent) => { + setloading(true); e.preventDefault(); // Basic validation - if (!fullname || !mobileNumber || !email || !password || !confirmPassword) { + if (!fullname || !mobileNumber || !email) { setError("All fields are required."); - setloading(false) - return; - } - if (password !== confirmPassword) { - setError("Passwords do not match."); - setloading(false) + setloading(false); return; } - - const result = await axios.post('/api/auth/signup',{ + try { + const result = await axios.post("/api/auth/signup", { email, - password, - name:fullname, - mobileNumber - }) - console.log(result) - if (!result) setError("Invalid email or password"); - else { - toast.success(`Successful, you can login now.`) - // ${session.data?.user?.name} - window.location.href = "/login"; // Redirect on success + name: fullname, + mobileNumber, + }); + console.log(result); + } catch (err) { + if (axios.isAxiosError(err)) { + setError("Invalid email or number, axios error"); + } else { + setError("Invalid email or number"); + } + // + console.log(err); + setloading(false); + return; } - - // Reset form fields after submission + setOtpOpen(true); setFullname(""); setmobileNumber(""); - setEmail(""); - setPassword(""); - setConfirmPassword(""); - - setloading(false) + setloading(false); }; return ( <> - {!switchCss && ( -
+ {!switchCss && !otpOpen && ( +
Sign Up
@@ -109,52 +162,11 @@ const SignupPage: React.FC =({ switchCss, setSwitchCss,setError />
-
- {" "} - {/* Added relative for positioning */} - setPassword(e.target.value)} - /> -
setShowPassword(!showPassword)} - > - {showPassword ? ( - - ) : ( - - )} -
-
-
- {" "} - {/* Added relative for positioning */} - setConfirmPassword(e.target.value)} - /> -
setShowConfirmPassword(!showConfirmPassword)} - > - {showConfirmPassword ? ( - - ) : ( - - )} -
-
+ @@ -170,6 +182,46 @@ const SignupPage: React.FC =({ switchCss, setSwitchCss,setError
)} + {!switchCss && otpOpen && ( +
+ + ( + + + One-Time Password + + + + + + + + + + + + + + + Please enter the one-time password sent to your email. + + + + )} + /> + + + + + )} ); }; diff --git a/app/(Customer)/auth/role/page.tsx b/app/(Customer)/auth/role/page.tsx new file mode 100644 index 0000000..46fd8e8 --- /dev/null +++ b/app/(Customer)/auth/role/page.tsx @@ -0,0 +1,58 @@ +"use client"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import Link from "next/link"; + + +const Login = () => { + return ( +
+ {/*
{switchCss ? "1" : "0"}
+ */} +
+
+ {/* First background div */} +
+ + + Proceed as a + + + + + Role + + + + Customer + + + Seller + + + + + +
+
+ Welcome to EzyShop! +
+
+
+
+
+ ); +}; + +export default Login; diff --git a/app/api/auth/signup/route.ts b/app/api/auth/signup/route.ts index fb6561b..1c651ce 100644 --- a/app/api/auth/signup/route.ts +++ b/app/api/auth/signup/route.ts @@ -1,11 +1,11 @@ import { NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; -import bcrypt from 'bcrypt'; +import { generateAndSendOTP } from '@/lib/auth'; const prisma = new PrismaClient(); export async function POST(request: Request) { - const { email, password , mobileNumber,name } = await request.json(); + const { email, mobileNumber,name } = await request.json(); // Check if the user already exists const existingUser = await prisma.user.findUnique({ @@ -17,19 +17,22 @@ export async function POST(request: Request) { } // Hash the password - const passwordHash = await bcrypt.hash(password, 10); + // const passwordHash = await bcrypt.hash(password, 10); // Create the user const user = await prisma.user.create({ data: { email, - passwordHash, mobileNumber, name }, }); + generateAndSendOTP(email,"user"); - return NextResponse.json({ message: 'Signup successful!',user }); + + + + return NextResponse.json({ message: 'aCCOUNT CREATED, VERIFY EMAIL VIA OTP',user }); } diff --git a/components/Navbar/authButtons.tsx b/components/Navbar/authButtons.tsx index 0551aa4..4e1288a 100644 --- a/components/Navbar/authButtons.tsx +++ b/components/Navbar/authButtons.tsx @@ -1,26 +1,25 @@ import { signOut, useSession } from "next-auth/react"; -import { useState } from "react"; import { Button } from "../ui/button"; -import { AuthModal } from "../modals/authModal"; +import Link from "next/link"; const AuthButtons = () => { - const [open, setOpen] = useState(false); - const [loading] = useState(false); + // const [open, setOpen] = useState(false); + // const [loading] = useState(false); const session=useSession() return (
{session.status == "unauthenticated" && ( - // + - // + )} {session.status == "authenticated" && ( <> @@ -28,11 +27,11 @@ const AuthButtons = () => { )} - setOpen(false)} loading={loading} - /> + /> */}
); }; diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..b6daa65 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +