Skip to content

Commit

Permalink
global session management throughout the website, User Login function…
Browse files Browse the repository at this point in the history
…ality with secure OTP-based email verification, includes an .env.example (#375)

* initialised next app and added homepage

* setup email, prisma, DB schema, nextAUTH

* fic

* conflict

* user signup frontend and backend

* auth functionality complete

* added .env.example
  • Loading branch information
ShivanshPlays authored Oct 31, 2024
1 parent 4e3433d commit 39ca948
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 96 deletions.
10 changes: 10 additions & 0 deletions scruter-nextjs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DATABASE_URL="yourMongoDBurl/scruter"

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=email app id
SMTP_PASS=email app PW

NEXTAUTH_URL=http://localhost:3000 or prod url
NEXTAUTH_SECRET=password_nextauth for signing JWT tokens
2 changes: 1 addition & 1 deletion scruter-nextjs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ yarn-debug.log*
yarn-error.log*

# env files (can opt-in for commiting if needed)
.env*
.env

# vercel
.vercel
Expand Down
33 changes: 33 additions & 0 deletions scruter-nextjs/actions/user/login-action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use server"
import { generateAndSendOTP } from '@/lib/auth';
import prismadb from '@/lib/prismadb';
import { Prisma, User } from '@prisma/client';

export async function UserVerify({
email
}:{
email:string
}): Promise<{ success: boolean; error?: string ; data?:User}> {

const exitingUser = await prismadb.user.findUnique({
where: {
email: email,
},
});

if (!exitingUser) {
return {
success: false,
error: 'User does not exists',
};
}
const resp=await generateAndSendOTP(email,"user");

if(!resp){
return {success:false, error:"Error occured in sending otp"};
}

return { success: true};


}
133 changes: 55 additions & 78 deletions scruter-nextjs/app/(routes)/auth/components/otp-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,36 @@
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from '@/components/ui/input-otp';

import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";

Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { z } from "zod";

import { zodResolver } from "@hookform/resolvers/zod";
import * as React from 'react';

import { cn } from '@/lib/utils';
import { Icons } from '@/components/ui/icons';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Toaster, toast } from 'react-hot-toast';
import { SellerCreate } from '@/actions/seller/signup-action';
import { zodResolver } from "@hookform/resolvers/zod";

import { signIn } from "next-auth/react";
import { useForm } from 'react-hook-form';
import { ChevronLeftCircleIcon } from 'lucide-react';
import { useRouter } from 'next/navigation'; // use 'next/navigation' for Next.js 13 App Router

interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {
roleType: 'user' | 'seller';
email:string;
setOtpOpen:(otp:boolean)=>void
email: string;
setOtpOpen: (otp: boolean) => void;
}

const FormSchema = z.object({
pin: z.string().min(6, {
message: "Your one-time password must be 6 characters.",
}),
});

pin: z.string().length(6, { message: "Your one-time password must be 6 characters." }),
});

export function OtpForm({
className,
Expand All @@ -55,82 +42,72 @@ export function OtpForm({
...props
}: UserAuthFormProps) {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const router = useRouter(); // Ensure this is declared outside of useEffect

const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
pin: "",
},
defaultValues: { pin: "" },
});

async function onOTPSubmit(data: z.infer<typeof FormSchema>) {
// console.log(data.pin+email);
setIsLoading(true);

// toast.success(data.pin)

const result = await signIn("credentials", {
email,
otp: data.pin,
role: roleType,
redirect: false,
});
console.log(result);

if (!result?.ok) {
toast.error("Invalid email or otp");
toast.error("Invalid email or OTP");
} else {
toast.success(`Welcome!`);

// setTimeout(() => {
// window.location.href = `"/`; // Redirect on success
// }, 2000);
if (roleType === 'user') {
router.push('/'); // Redirect to home for user
}
}
setIsLoading(false);
}


return (
<div className={cn('grid gap-6', className)} {...props}>
<Toaster />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onOTPSubmit)}
className="flex flex-col dark:text-gray-200 z-10 items-start justify-center py-10 md:py-0 pl-14 gap-4 w-2/4"
>
<FormField
control={form.control}
name="pin"
disabled={isLoading}
render={({ field }) => (
<FormItem className="flex gap-2 items-start justify-center flex-col">
<FormLabel className="text-2xl gap-2 flex items-center justify-center text-customTeal dark:text-Green font-bold">
<ChevronLeftCircleIcon onClick={()=>{setOtpOpen(false)}} className="h-5 w-5"/>
One-Time Password
</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Please enter the one-time password sent to your email.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>

<Button className="bg-black" type="submit">
Submit
</Button>
</form>
</Form>
<form
onSubmit={form.handleSubmit(onOTPSubmit)}
className="flex flex-col dark:text-gray-200 z-10 items-start justify-center py-10 md:py-0 pl-14 gap-4 w-2/4"
>
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem className="flex gap-2 items-start justify-center flex-col">
<FormLabel className="text-2xl gap-2 flex items-center justify-center text-customTeal dark:text-Green font-bold">
<ChevronLeftCircleIcon onClick={() => setOtpOpen(false)} className="h-5 w-5" />
One-Time Password
</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className="bg-black" type="submit" disabled={isLoading}>
{isLoading ? "Loading..." : "Submit"}
</Button>
</form>
</Form>
</div>
);
}
94 changes: 94 additions & 0 deletions scruter-nextjs/app/(routes)/auth/components/user/login-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use client';

import * as React from 'react';

import { cn } from '@/lib/utils';
import { Icons } from '@/components/ui/icons';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import toast, { Toaster } from 'react-hot-toast';
import { OtpForm } from '../otp-form';
import { UserVerify } from '@/actions/user/login-action';

interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {
authType: 'signup' | 'login';
}

export function UserLoginForm({
className,
authType,
...props
}: UserAuthFormProps) {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [email, setEmail] = React.useState('');
const [otpOpen, setOtpOpen] = React.useState(false);

async function onSubmit(event: React.SyntheticEvent) {
event.preventDefault();
setIsLoading(true);

if (!email) {
toast.error('missing details');
setIsLoading(false)
return;
}
const res = await UserVerify({
email: email,
});

if (!res.success && res.error) {
toast.error(res.error);
setIsLoading(false)
return;
}

toast.success('user is valid, please enter OTP');

setOtpOpen(true);
setIsLoading(false);
}

return (
<div className={cn('grid gap-6', className)} {...props}>
<Toaster/>
{!otpOpen && (
<form onSubmit={onSubmit}>
<div className="grid gap-2">
<div className="grid gap-1">
<Label className="sr-only" htmlFor="email">
Email
</Label>
<Input
id="email"
placeholder="[email protected]"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading}
value={email}
onChange={e => {
setEmail(e.target.value);
}}
/>
</div>
<Button type="submit" className='bg-black' disabled={isLoading}>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
{authType === 'signup' ? (
<p>Sign Up with Email</p>
) : (
<p>Sign In with Email</p>
)}
</Button>
</div>
</form>
)}
{otpOpen && (
<OtpForm email={email} setOtpOpen={setOtpOpen} roleType="user" />
)}
</div>
);
}
Loading

0 comments on commit 39ca948

Please sign in to comment.