feat: implement reset password functionality with validation and UI components
This commit is contained in:
@@ -17,8 +17,6 @@ export async function ForgotPasswordAction(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(data, error);
|
|
||||||
|
|
||||||
if (error) throw new Error(error.message);
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Form {...form}>
|
||||||
|
{error && (
|
||||||
|
<div className="p-2 text-xs bg-red-50 border border-red-200 text-red-800 rounded-sm w-full">
|
||||||
|
{(error as Error).message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>New Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirm_password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Confirm New Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={isPending}>
|
||||||
|
{isPending ? (
|
||||||
|
<LuLoaderCircle className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Ubah password'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<typeof useResetPasswordForm>
|
||||||
|
) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: resetPasswordAction,
|
||||||
|
onSuccess: ({ message }) => {
|
||||||
|
form.reset();
|
||||||
|
router.replace('/signin');
|
||||||
|
toast.success(message);
|
||||||
|
},
|
||||||
|
onError: ({ message }) => {
|
||||||
|
form.reset();
|
||||||
|
toast.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<ResetPasswordValidationSchema>({
|
||||||
|
resolver: zodResolver(resetPasswordValidationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
token: tokenFromQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
>;
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<LogoSimple />
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-center text-sm sm:text-base">
|
||||||
|
Ubah passwordmu
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ResetPasswordForm />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user