From 8fb3e9a37f21c524dd6d74026c7bce48bf5ab017 Mon Sep 17 00:00:00 2001 From: arraysid Date: Wed, 28 May 2025 01:41:20 +0700 Subject: [PATCH] feat: implement reset password functionality with validation and UI components --- .../_actions/forgot-password-action.ts | 2 - .../_actions/reset-password-actions.ts | 28 +++++++ .../_components/reset-password-form.tsx | 73 +++++++++++++++++++ .../_hooks/use-post-reset-password.ts | 24 ++++++ .../_hooks/use-reset-password-form.ts | 22 ++++++ .../_validation/reset-password-validation.ts | 31 ++++++++ .../src/app/(auth)/reset-password/page.tsx | 30 ++++++++ 7 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 apps/landing/src/app/(auth)/reset-password/_actions/reset-password-actions.ts create mode 100644 apps/landing/src/app/(auth)/reset-password/_components/reset-password-form.tsx create mode 100644 apps/landing/src/app/(auth)/reset-password/_hooks/use-post-reset-password.ts create mode 100644 apps/landing/src/app/(auth)/reset-password/_hooks/use-reset-password-form.ts create mode 100644 apps/landing/src/app/(auth)/reset-password/_validation/reset-password-validation.ts create mode 100644 apps/landing/src/app/(auth)/reset-password/page.tsx diff --git a/apps/landing/src/app/(auth)/forgot-password/_actions/forgot-password-action.ts b/apps/landing/src/app/(auth)/forgot-password/_actions/forgot-password-action.ts index 07958f8..2149c4e 100644 --- a/apps/landing/src/app/(auth)/forgot-password/_actions/forgot-password-action.ts +++ b/apps/landing/src/app/(auth)/forgot-password/_actions/forgot-password-action.ts @@ -17,8 +17,6 @@ export async function ForgotPasswordAction( }, }); - console.log(data, error); - if (error) throw new Error(error.message); return data; diff --git a/apps/landing/src/app/(auth)/reset-password/_actions/reset-password-actions.ts b/apps/landing/src/app/(auth)/reset-password/_actions/reset-password-actions.ts new file mode 100644 index 0000000..b4a8a1a --- /dev/null +++ b/apps/landing/src/app/(auth)/reset-password/_actions/reset-password-actions.ts @@ -0,0 +1,28 @@ +'use server'; + +import { fetcher } from '@/lib/fetcher'; +import { + resetPasswordValidationSchema, + ResetPasswordValidationSchema, +} from '../_validation/reset-password-validation'; + +export async function resetPasswordAction( + request: ResetPasswordValidationSchema +) { + const validRequest = resetPasswordValidationSchema.parse(request); + + if (validRequest.confirm_password !== validRequest.confirm_password) { + throw new Error('Password missmatch'); + } + + const { data, error } = await fetcher.POST('/v1/auth/new-password', { + body: { + password: validRequest.password, + token: validRequest.token, + }, + }); + + if (error) throw new Error(error.message); + + return data; +} diff --git a/apps/landing/src/app/(auth)/reset-password/_components/reset-password-form.tsx b/apps/landing/src/app/(auth)/reset-password/_components/reset-password-form.tsx new file mode 100644 index 0000000..c4289b3 --- /dev/null +++ b/apps/landing/src/app/(auth)/reset-password/_components/reset-password-form.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { + Button, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, +} from '@components'; +import { LuLoaderCircle } from 'react-icons/lu'; +import { usePostResetPassword } from '../_hooks/use-post-reset-password'; +import { useResetPasswordForm } from '../_hooks/use-reset-password-form'; +import { ResetPasswordValidationSchema } from '../_validation/reset-password-validation'; + +export function ResetPasswordForm() { + const form = useResetPasswordForm(); + const { mutate, error, isPending } = usePostResetPassword(form); + + const onSubmit = (values: ResetPasswordValidationSchema) => { + mutate(values); + }; + + return ( +
+ {error && ( +
+ {(error as Error).message} +
+ )} + + + ( + + New Password + + + + + + )} + /> + + ( + + Confirm New Password + + + + + + )} + /> + + + + + ); +} diff --git a/apps/landing/src/app/(auth)/reset-password/_hooks/use-post-reset-password.ts b/apps/landing/src/app/(auth)/reset-password/_hooks/use-post-reset-password.ts new file mode 100644 index 0000000..80f89ec --- /dev/null +++ b/apps/landing/src/app/(auth)/reset-password/_hooks/use-post-reset-password.ts @@ -0,0 +1,24 @@ +import { useMutation } from '@tanstack/react-query'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; +import { resetPasswordAction } from '../_actions/reset-password-actions'; +import { useResetPasswordForm } from './use-reset-password-form'; + +export function usePostResetPassword( + form: ReturnType +) { + const router = useRouter(); + + return useMutation({ + mutationFn: resetPasswordAction, + onSuccess: ({ message }) => { + form.reset(); + router.replace('/signin'); + toast.success(message); + }, + onError: ({ message }) => { + form.reset(); + toast.error(message); + }, + }); +} diff --git a/apps/landing/src/app/(auth)/reset-password/_hooks/use-reset-password-form.ts b/apps/landing/src/app/(auth)/reset-password/_hooks/use-reset-password-form.ts new file mode 100644 index 0000000..4cdbb1c --- /dev/null +++ b/apps/landing/src/app/(auth)/reset-password/_hooks/use-reset-password-form.ts @@ -0,0 +1,22 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useSearchParams } from 'next/navigation'; +import { useForm } from 'react-hook-form'; + +import { + resetPasswordValidationSchema, + ResetPasswordValidationSchema, +} from '../_validation/reset-password-validation'; + +export function useResetPasswordForm() { + const searchParams = useSearchParams(); + const tokenFromQuery = searchParams.get('token') ?? ''; + + return useForm({ + resolver: zodResolver(resetPasswordValidationSchema), + defaultValues: { + password: '', + confirm_password: '', + token: tokenFromQuery, + }, + }); +} diff --git a/apps/landing/src/app/(auth)/reset-password/_validation/reset-password-validation.ts b/apps/landing/src/app/(auth)/reset-password/_validation/reset-password-validation.ts new file mode 100644 index 0000000..1261ca8 --- /dev/null +++ b/apps/landing/src/app/(auth)/reset-password/_validation/reset-password-validation.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +export const resetPasswordValidationSchema = z + .object({ + password: z + .string({ + required_error: 'Password tidak boleh kosong', + invalid_type_error: 'Password harus berupa string', + }) + .min(8, 'Password harus minimal 8 karakter') + .max(50, 'Password tidak boleh lebih dari 50 karakter') + .regex( + /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*\W).+$/, + 'Password harus mengandung setidaknya satu huruf kapital, satu huruf kecil, satu angka, dan satu karakter spesial' + ), + confirm_password: z + .string({ + required_error: 'Konfirmasi password tidak boleh kosong', + }) + .min(8, 'Konfirmasi password harus minimal 8 karakter') + .max(50, 'Konfirmasi password tidak boleh lebih dari 50 karakter'), + token: z.string(), + }) + .refine((data) => data.password === data.confirm_password, { + message: 'Password dan Konfirmasi Password harus sama', + path: ['confirm_password'], + }); + +export type ResetPasswordValidationSchema = z.infer< + typeof resetPasswordValidationSchema +>; diff --git a/apps/landing/src/app/(auth)/reset-password/page.tsx b/apps/landing/src/app/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..6378290 --- /dev/null +++ b/apps/landing/src/app/(auth)/reset-password/page.tsx @@ -0,0 +1,30 @@ +import { LogoSimple } from '@/app/_components/logo'; +import { redirect } from 'next/navigation'; +import { use } from 'react'; +import { ResetPasswordForm } from './_components/reset-password-form'; + +export const dynamic = 'force-dynamic'; + +export default function Page({ + searchParams, +}: { + searchParams: Promise<{ token?: string }>; +}) { + const { token } = use(searchParams); + + if (!token) redirect('/signin'); + + return ( + <> +
+ + +

+ Ubah passwordmu +

+ + +
+ + ); +}