diff --git a/components/auth/reset-pass-steps/index.ts b/components/auth/reset-pass-steps/index.ts new file mode 100644 index 0000000..96cd42a --- /dev/null +++ b/components/auth/reset-pass-steps/index.ts @@ -0,0 +1,3 @@ +export * from './user-email'; +export * from './verify-otp'; +export * from './register-user'; diff --git a/components/auth/reset-pass-steps/register-user.tsx b/components/auth/reset-pass-steps/register-user.tsx new file mode 100644 index 0000000..d4b0336 --- /dev/null +++ b/components/auth/reset-pass-steps/register-user.tsx @@ -0,0 +1,60 @@ +import { AuthInput } from '@/components/ui/auth-input'; +import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; +import { useStepper } from '@/components/ui/stepper'; +import { handlePasswordConfirmation } from '@/lib/api/auth/reset-password'; +import { useEmailStore } from '@/lib/stores/email-store'; +import { useActionState, useEffect } from 'react'; +import { toast } from 'sonner'; + +const RegisterUser = () => { + const [state, formAction, isPending] = useActionState( + handlePasswordConfirmation, + null + ); + const stepper = useStepper(); + const { email } = useEmailStore(); + + useEffect(() => { + if (state?.success) { + toast.success(state.message); + stepper.nextStep(); + } + }, [state, stepper]); + + return stepper.hasCompletedAllSteps ? ( +
+

Registration Complete

+

+ You can now log in with your credentials :) +

+
+ ) : ( +
+ + + {state?.errors?.general && ( +

{state.errors.general}

+ )} + + + ); +}; + +export { RegisterUser }; diff --git a/components/auth/reset-pass-steps/user-email.tsx b/components/auth/reset-pass-steps/user-email.tsx new file mode 100644 index 0000000..b13e02a --- /dev/null +++ b/components/auth/reset-pass-steps/user-email.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { AuthInput } from '@/components/ui/auth-input'; +import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; +import { useStepper } from '@/components/ui/stepper'; +import { handleEmailStep } from '@/lib/api/auth/reset-password'; +import { useEmailStore } from '@/lib/stores/email-store'; +import { useActionState, useEffect } from 'react'; +import { toast } from 'sonner'; + +const UserEmail = () => { + const stepper = useStepper(); + const [state, formAction, isPending] = useActionState(handleEmailStep, null); + const { setEmail } = useEmailStore(); + + useEffect(() => { + if (state?.success) { + toast.success(state.message); + stepper.nextStep(); + } + }, [state, stepper]); + + return ( +
+ setEmail(e.target.value)} + /> + {state?.errors?.general && ( +

{state.errors.general}

+ )} + + + ); +}; + +export { UserEmail }; diff --git a/components/auth/reset-pass-steps/verify-otp.tsx b/components/auth/reset-pass-steps/verify-otp.tsx new file mode 100644 index 0000000..42864b9 --- /dev/null +++ b/components/auth/reset-pass-steps/verify-otp.tsx @@ -0,0 +1,49 @@ +import React, { useActionState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from '@/components/ui/input-otp'; +import { handleOtpVerification } from '@/lib/api/auth/reset-password'; +import { useStepper } from '@/components/ui/stepper'; +import { Spinner } from '@/components/ui/spinner'; +import { toast } from 'sonner'; + +const VerifyOtp = () => { + const [state, formAction, isPending] = useActionState( + handleOtpVerification, + null + ); + const stepper = useStepper(); + + useEffect(() => { + if (state?.success) { + toast.success(state.message); + stepper.nextStep(); + } + }, [state, stepper]); + + return ( +
+ + + {[...Array(6)].map((_, i) => ( + + ))} + + + {state?.errors?.otp && ( +

{state.errors.otp}

+ )} + {state?.errors?.general && ( +

{state.errors.general}

+ )} + +
+ ); +}; + +export { VerifyOtp }; diff --git a/components/auth/reset-tab.tsx b/components/auth/reset-tab.tsx index 551c320..e0f3cd2 100644 --- a/components/auth/reset-tab.tsx +++ b/components/auth/reset-tab.tsx @@ -1,147 +1,24 @@ 'use client'; -import { useActionState } from 'react'; -import { Button } from '@/components/ui/button'; -import { AuthInput } from '@/components/ui/auth-input'; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot, -} from '@/components/ui/input-otp'; -import { - handleResetPasswordStep, - steps, - initialState, -} from '@/lib/api/auth/reset-password'; +import React from 'react'; +import { Stepper, StepperProgress, Step } from '@/components/ui/stepper'; +import { UserEmail, VerifyOtp, RegisterUser } from './reset-pass-steps'; -export default function ResetTab() { - const [state, formAction] = useActionState( - handleResetPasswordStep, - initialState - ); - const activeStep = state.errors - ? state.step - : Math.min(state.step, steps.length); - - const resetSteps = () => window.location.reload(); +const RESET_PASS_STEPS = [ + { step: 0, Component: UserEmail }, + { step: 1, Component: VerifyOtp }, + { step: 2, Component: RegisterUser }, +]; +export default function ResetTab() { return ( -
-
-
- {steps.map((step, index) => ( -
-
- {index + 1} -
- - {index < steps.length - 1 && ( -
- )} -
- ))} -
-
- - {/* Fixed height container for the form content */} -
- {activeStep === 0 && ( -
- - {state.errors?.general && ( -

- {state.errors.general[0]} -

- )} - - - )} - - {activeStep === 1 && ( -
- - - {[...Array(6)].map((_, i) => ( - - ))} - - - {state.errors?.otp && ( -

- {state.errors.otp[0]} -

- )} - {state.errors?.general && ( -

- {state.errors.general[0]} -

- )} - -
- )} - - {activeStep === 2 && ( -
- - - {state.errors?.general && ( -

- {state.errors.general[0]} -

- )} - - - )} - - {activeStep === steps.length && ( -
-

Password reset successfully!

- -
- )} -
-
+ + + {RESET_PASS_STEPS.map(({ step, Component }) => ( + + + + ))} + ); } diff --git a/lib/api/auth/reset-password/reset-password-action.ts b/lib/api/auth/reset-password/reset-password-action.ts index e9fab07..f8eced5 100644 --- a/lib/api/auth/reset-password/reset-password-action.ts +++ b/lib/api/auth/reset-password/reset-password-action.ts @@ -1,61 +1,169 @@ -import { z } from 'zod'; -import { EmailSchema, OtpSchema, NewPasswordSchema } from '@/lib/schemas/auth'; -export const steps = ['email', 'otp', 'newPassword'] as const; -export type StepKey = (typeof steps)[number]; +'use server'; -export type FormState = { - step: number; - errors?: Record; -}; +import { + EmailSchema, + NewPasswordSchema, + OtpSchema, + RegisterUserSchema, +} from '@/lib/schemas/auth'; +import { z } from 'zod'; +import { createUnauthenticatedAxios } from '@/lib/api/axios'; +import { AxiosError } from 'axios'; -export const initialState: FormState = { step: 0 }; +interface ActionResponse { + success: boolean; + errors?: { + email?: string; + otp?: string; + fullName?: string; + username?: string; + password?: string; + confirmPassword?: string; + general?: string; + }; + inputs?: { + email?: string; + otp?: string; + fullName?: string; + username?: string; + password?: string; + confirmPassword?: string; + }; + message: string; +} -const EXPECTED_OTP = '123456'; +const axiosInstance = createUnauthenticatedAxios(); +axiosInstance.defaults.headers['Content-Type'] = 'multipart/form-data'; -// TODO: Implement the logic just like sign-up step -export async function handleResetPasswordStep( - prevState: FormState, +// Server Actions for each step // +export async function handleEmailStep( + prevState: unknown, formData: FormData -): Promise { - const step = prevState.step; - const formObj = Object.fromEntries(formData.entries()); - +): Promise { try { - if (step === 0) { - EmailSchema.parse(formObj); - console.log('Sending OTP to:', formObj.email); - return { step: step + 1 }; + EmailSchema.parse(convertFormDataToRecord(formData)); + console.log('Sending OTP to:', formData.get('email')); + // await axiosInstance.post('/auth/send-otp', { + // email: formData.get('email'), + // }); + return createSuccessResponse('OTP sent successfully'); + } catch (err) { + if (err instanceof z.ZodError) { + return handleZodError(err, formData); + } else if (err instanceof AxiosError && err.response) { + const { data } = err.response; + console.log(data); + return { + success: false, + message: 'Failed to send OTP', + errors: { + general: data.message || 'An error occurred while sending OTP', + }, + }; } + return createErrorResponse('An unexpected error occurred'); + } +} - if (step === 1) { - OtpSchema.parse(formObj); - const otp = formObj.otp; - if (otp !== EXPECTED_OTP) { - return { - step, - errors: { otp: ['Incorrect OTP entered'] }, - }; - } - return { step: step + 1 }; +export async function handleOtpVerification( + prevState: unknown, + formData: FormData +): Promise { + try { + OtpSchema.parse(convertFormDataToRecord(formData)); + const otp = formData.get('otp'); + // await axiosInstance.post('/auth/verify-otp', { + // otp, + // }); + return createSuccessResponse('OTP verified successfully'); + } catch (err) { + if (err instanceof z.ZodError) { + return handleZodError(err, formData); + } else if (err instanceof AxiosError && err.response) { + const { data } = err.response; + return { + success: false, + message: 'Failed to verify OTP', + errors: { + general: + data.message || 'Failed to verify OTP, please try again later.', + }, + }; } + return createErrorResponse('An unexpected error occurred'); + } +} - if (step === 2) { - NewPasswordSchema.parse(formObj); - console.log('Resetting password'); - return { step: step + 1 }; - } - } catch (err: any) { +export async function handlePasswordConfirmation( + prevState: unknown, + formData: FormData +): Promise { + try { + NewPasswordSchema.parse(convertFormDataToRecord(formData)); + await axiosInstance.post('/auth/register', { + password: formData.get('newPassword'), + 'ssh-key': '', + }); + return createSuccessResponse('User registered successfully'); + } catch (err) { if (err instanceof z.ZodError) { - const errors: Record = {}; - for (const issue of err.errors) { - const key = issue.path[0] as string; - if (!errors[key]) errors[key] = []; - errors[key].push(issue.message); - } - return { step, errors }; + return handleZodError(err, formData); + } else if (err instanceof AxiosError && err.response) { + const { data } = err.response; + console.log(data); + return { + success: false, + message: 'Failed to create user', + errors: { + general: data.message, + }, + }; + } + return createErrorResponse('An unexpected error occurred'); + } +} + +// Utils // +// TODO: Make a common utils for this +function convertFormDataToRecord(formData: FormData): Record { + const record: Record = {}; + for (const [key, value] of formData.entries()) { + if (typeof value === 'string') { + record[key] = value; } - return { step, errors: { general: ['Unexpected error'] } }; } + return record; +} + +function handleZodError(err: z.ZodError, formData: FormData): ActionResponse { + const errors: Record = {}; + for (const issue of err.errors) { + const key = issue.path[0] as string; + errors[key] = issue.message; + } + console.log(errors); + return { + success: false, + errors, + inputs: convertFormDataToRecord(formData), + message: 'Validation failed', + }; +} + +function createErrorResponse( + message: string, + errors?: Record +): ActionResponse { + return { + success: false, + errors: errors || { general: 'Unexpected error' }, + message, + }; +} - return { step }; +function createSuccessResponse(message: string): ActionResponse { + return { + success: true, + message, + }; } diff --git a/lib/api/auth/sign-up/sign-up-action.ts b/lib/api/auth/sign-up/sign-up-action.ts index 5aaab86..f39db46 100644 --- a/lib/api/auth/sign-up/sign-up-action.ts +++ b/lib/api/auth/sign-up/sign-up-action.ts @@ -38,9 +38,9 @@ export async function handleEmailStep( try { EmailSchema.parse(convertFormDataToRecord(formData)); console.log('Sending OTP to:', formData.get('email')); - await axiosInstance.post('/auth/send-otp', { - email: formData.get('email'), - }); + // await axiosInstance.post('/auth/send-otp', { + // email: formData.get('email'), + // }); return createSuccessResponse('OTP sent successfully'); } catch (err) { if (err instanceof z.ZodError) { @@ -67,9 +67,9 @@ export async function handleOtpVerification( try { OtpSchema.parse(convertFormDataToRecord(formData)); const otp = formData.get('otp'); - await axiosInstance.post('/auth/verify-otp', { - otp, - }); + // await axiosInstance.post('/auth/verify-otp', { + // otp, + // }); return createSuccessResponse('OTP verified successfully'); } catch (err) { if (err instanceof z.ZodError) {