feat: implement verification page
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { fetcher } from '@/lib/fetcher';
|
||||||
|
import { fetchPostverifyTurnstile } from '../_http/fetch-post-verify-turnstile';
|
||||||
|
import {
|
||||||
|
resendOTPValidationSchema,
|
||||||
|
type ResendOTPValidationType,
|
||||||
|
} from '../_validation/resend-otp-validation';
|
||||||
|
|
||||||
|
export async function resendOTPAction(request: ResendOTPValidationType) {
|
||||||
|
const validRequest = resendOTPValidationSchema.parse(request);
|
||||||
|
|
||||||
|
const isCapchaValidationValid = await fetchPostverifyTurnstile(
|
||||||
|
validRequest.token
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isCapchaValidationValid) throw new Error('Failed to verify captcha');
|
||||||
|
|
||||||
|
const { data } = await fetcher.POST('/v1/auth/send-otp', {
|
||||||
|
body: {
|
||||||
|
email: validRequest.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
return data?.message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { fetchPostVerifyEmail } from '../_http/fetch-post-verify-email';
|
||||||
|
import {
|
||||||
|
type VerifyEmailValidationType,
|
||||||
|
verifyEmailValidationSchema,
|
||||||
|
} from '../_validation/verify-email-validation';
|
||||||
|
|
||||||
|
export async function verifyEmailAction(request: VerifyEmailValidationType) {
|
||||||
|
const validRequest = verifyEmailValidationSchema.parse(request);
|
||||||
|
|
||||||
|
const data = await fetchPostVerifyEmail(validRequest);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@components';
|
||||||
|
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { useFormResendOTP } from '../_hooks/use-form-resend-otp';
|
||||||
|
import { usePostResendOTP } from '../_hooks/use-post-resend-otp';
|
||||||
|
import { ResendOTPValidationType } from '../_validation/resend-otp-validation';
|
||||||
|
|
||||||
|
export function ResendOTPForm() {
|
||||||
|
const ref = useRef<TurnstileInstance | null>(null);
|
||||||
|
|
||||||
|
const form = useFormResendOTP();
|
||||||
|
|
||||||
|
const { mutate, isPending, error } = usePostResendOTP(form);
|
||||||
|
|
||||||
|
const onSubmit = (values: ResendOTPValidationType) => {
|
||||||
|
mutate(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
|
||||||
|
{(error as Error).message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="emailmu@mail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Turnstile
|
||||||
|
ref={ref}
|
||||||
|
siteKey={String(process.env.NEXT_PUBLIC_TURNSTILE_SITEKEY)}
|
||||||
|
onSuccess={(token) => form.setValue('token', token)}
|
||||||
|
options={{
|
||||||
|
theme: 'light',
|
||||||
|
size: 'flexible',
|
||||||
|
language: 'id',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
disabled={isPending || !form.watch('token')}
|
||||||
|
>
|
||||||
|
Kirim Ulang Kode OTP
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@utils';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ResendOTPForm } from './resend-otp-form';
|
||||||
|
import { VerifyEmailForm } from './verify-email-form';
|
||||||
|
|
||||||
|
export function VerificationTabs() {
|
||||||
|
const [activeTab, setActiveTab] = useState<'form' | 'resend'>('form');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex space-x-2 border-b">
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'py-2 px-4 w-full',
|
||||||
|
activeTab === 'form'
|
||||||
|
? 'border-b-2 border-primary-500 font-semibold'
|
||||||
|
: 'text-gray-500'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveTab('form')}
|
||||||
|
>
|
||||||
|
Verifikasi Email
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'py-2 px-4 w-full',
|
||||||
|
activeTab === 'resend'
|
||||||
|
? 'border-b-2 border-primary-500 font-semibold'
|
||||||
|
: 'text-gray-500'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveTab('resend')}
|
||||||
|
>
|
||||||
|
Kirim Ulang OTP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'form' ? <VerifyEmailForm /> : <ResendOTPForm />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
Input,
|
||||||
|
} from '@components';
|
||||||
|
import { LuLoader } from 'react-icons/lu';
|
||||||
|
import { useFormVerifyEmail } from '../_hooks/use-form-verify-email';
|
||||||
|
import { usePostVerifyEmail } from '../_hooks/use-post-verify-email';
|
||||||
|
import { type VerifyEmailValidationType } from '../_validation/verify-email-validation';
|
||||||
|
|
||||||
|
export function VerifyEmailForm() {
|
||||||
|
const form = useFormVerifyEmail();
|
||||||
|
const { mutate, isPending, error } = usePostVerifyEmail(form);
|
||||||
|
|
||||||
|
const onSubmit = (values: VerifyEmailValidationType) => {
|
||||||
|
mutate(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm">
|
||||||
|
{(error as Error).message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="emailmu@mail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="otp"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>OTP</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="123456" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="w-full hover:bg-[#5fbaef] bg-[#22a5f1] font-bold"
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<LuLoader className="h-5 w-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Verifikasi'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
resendOTPValidationSchema,
|
||||||
|
type ResendOTPValidationType,
|
||||||
|
} from '../_validation/resend-otp-validation';
|
||||||
|
|
||||||
|
export function useFormResendOTP() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const email = searchParams.get('ref');
|
||||||
|
|
||||||
|
return useForm<ResendOTPValidationType>({
|
||||||
|
resolver: zodResolver(resendOTPValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: email ?? '',
|
||||||
|
token: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
verifyEmailValidationSchema,
|
||||||
|
type VerifyEmailValidationType,
|
||||||
|
} from '../_validation/verify-email-validation';
|
||||||
|
|
||||||
|
export function useFormVerifyEmail() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const email = searchParams.get('ref');
|
||||||
|
|
||||||
|
return useForm<VerifyEmailValidationType>({
|
||||||
|
resolver: zodResolver(verifyEmailValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: email ?? '',
|
||||||
|
otp: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { resendOTPAction } from '../_actions/resend-otp-action';
|
||||||
|
import { useFormResendOTP } from './use-form-resend-otp';
|
||||||
|
|
||||||
|
export function usePostResendOTP(form: ReturnType<typeof useFormResendOTP>) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: resendOTPAction,
|
||||||
|
onSuccess: () => router.replace('/verification?success=true'),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { verifyEmailAction } from '../_actions/verify-email-action';
|
||||||
|
import { useFormVerifyEmail } from './use-form-verify-email';
|
||||||
|
|
||||||
|
export function usePostVerifyEmail(
|
||||||
|
form: ReturnType<typeof useFormVerifyEmail>
|
||||||
|
) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: verifyEmailAction,
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push('/');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
form.resetField('otp');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { fetcher } from '@/lib/fetcher';
|
||||||
|
import { VerifyEmailValidationType } from '../_validation/verify-email-validation';
|
||||||
|
|
||||||
|
export async function fetchPostVerifyEmail({
|
||||||
|
email,
|
||||||
|
otp,
|
||||||
|
}: VerifyEmailValidationType) {
|
||||||
|
const formattedOTP = parseInt(otp);
|
||||||
|
|
||||||
|
const { data, error, response } = await fetcher.POST(
|
||||||
|
'/v1/auth/verify-email',
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
otp: formattedOTP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Something went wrong, please try again later');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export async function fetchPostverifyTurnstile(
|
||||||
|
token: string,
|
||||||
|
remoteIp?: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
secret: String(process.env.TURNSTILE_SECRET_KEY),
|
||||||
|
response: token,
|
||||||
|
});
|
||||||
|
if (remoteIp) {
|
||||||
|
params.append('remoteip', remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: params.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Turnstile verify HTTP error', res.status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as TurnstileVerifyResponse;
|
||||||
|
if (!data.success) {
|
||||||
|
console.warn('Turnstile failure', data['error-codes']);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TurnstileVerifyResponse {
|
||||||
|
success: boolean;
|
||||||
|
challenge_ts: string;
|
||||||
|
hostname: string;
|
||||||
|
'error-codes'?: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const resendOTPValidationSchema = z.object({
|
||||||
|
email: z.string().email({ message: 'Format email tidak valid' }),
|
||||||
|
token: z.string().min(1, { message: 'OTP harus terdiri dari 1 digit' }),
|
||||||
|
});
|
||||||
|
export type ResendOTPValidationType = z.infer<typeof resendOTPValidationSchema>;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const verifyEmailValidationSchema = z.object({
|
||||||
|
email: z.string().email({ message: 'Format email tidak valid' }),
|
||||||
|
otp: z
|
||||||
|
.string()
|
||||||
|
.min(6, { message: 'OTP harus terdiri dari 6 digit' })
|
||||||
|
.max(6, { message: 'OTP harus terdiri dari 6 digit' }),
|
||||||
|
});
|
||||||
|
export type VerifyEmailValidationType = z.infer<
|
||||||
|
typeof verifyEmailValidationSchema
|
||||||
|
>;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { LogoSimple } from '../../_components/logo';
|
||||||
|
import { VerificationTabs } from './_components/verification-tabs';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<LogoSimple />
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-center text-sm sm:text-base">
|
||||||
|
Verifikasi akun mu sekarang
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VerificationTabs />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user