Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add password recovery #591

Merged
merged 7 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ REDIS_PASS=A3vniod98Zbuvn9u5

#REDIS_TLS=

MAIL_HOST=smtp.example.com
[email protected]
MAIL_PASSWORD=your-email-password


# ================================================
# Tip: use mailtrap.io for local development
Expand Down
12 changes: 12 additions & 0 deletions apps/webapp/src/app/b2c/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import { useEffect, useState } from "react";
import Cookies from 'js-cookie';
import useProfileStore from "@/state/profileStore";
import useUser from "@/hooks/get/useUser";
import { Button } from "@/components/ui/button";

export default function Page() {
const [userInitialized,setUserInitialized] = useState(true)
const {mutate} = useUser()
const router = useRouter()
const {profile} = useProfileStore();
const [activeTab, setActiveTab] = useState('login');

useEffect(() => {
if(profile)
Expand Down Expand Up @@ -70,6 +72,16 @@ export default function Page() {
<CreateUserForm/>
</TabsContent>
</Tabs>
{activeTab === 'login' && (
<Button variant="link" onClick={() => setActiveTab('forgot-password')}>
Forgot Password?
</Button>
)}
{activeTab === 'forgot-password' && (
<Button variant="link" onClick={() => setActiveTab('login')}>
Back to Login
</Button>
)}
</div>
<div className='hidden lg:block relative flex-1'>
<img className='absolute inset-0 h-full w-full object-cover border-l' src="/bgbg.jpeg" alt='Login Page Image' />
Expand Down
24 changes: 24 additions & 0 deletions apps/webapp/src/app/b2c/login/reset-password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import React from 'react';
import { useSearchParams } from 'next/navigation';
import ResetPasswordForm from '@/components/Auth/CustomLoginComponent/ResetPasswordForm';

const ResetPasswordPage = () => {
const searchParams = useSearchParams();
const token = searchParams.get('token');

if (!token) {
return <div>Invalid or missing reset token. Please try the password reset process again.</div>;
}

return (
<div className='min-h-screen flex items-center justify-center bg-gray-100'>
<div className='max-w-md w-full'>
<ResetPasswordForm token={token} />
</div>
</div>
);
};

export default ResetPasswordPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider importing only necessary parts from libraries.

Importing entire libraries can increase the bundle size. Consider importing only the necessary parts.

- import * as z from 'zod';
+ import { string, object } from 'zod';
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { string, object } from 'zod';

import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import useInitiatePasswordRecovery from '@/hooks/create/useInitiatePasswordRecovery';

const formSchema = z.object({
email: z.string().email({ message: 'Enter valid Email' }),
});
Comment on lines +12 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve form validation schema.

Consider adding more validation rules to the form schema to enhance user input validation.

- const formSchema = z.object({
-   email: z.string().email({ message: 'Enter valid Email' }),
- });
+ const formSchema = object({
+   email: string().min(1, { message: 'Email is required' }).email({ message: 'Enter valid Email' }),
+ });

Committable suggestion was skipped due to low confidence.


const ForgotPasswordForm = () => {
const { func } = useInitiatePasswordRecovery();

const sform = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
},
});

const onSubmit = (values: z.infer<typeof formSchema>) => {
toast.promise(
func({ email: values.email }),
{
loading: 'Sending recovery email...',
success: 'Recovery email sent. Please check your inbox.',
error: 'Failed to send recovery email. Please try again.',
}
);
};
Comment on lines +26 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add error handling for form submission.

Currently, there is no error handling for form submission errors. Consider adding a catch block to handle potential errors.

- const onSubmit = (values: z.infer<typeof formSchema>) => {
-   toast.promise(
-     func({ email: values.email }),
-     {
-       loading: 'Sending recovery email...',
-       success: 'Recovery email sent. Please check your inbox.',
-       error: 'Failed to send recovery email. Please try again.',
-     }
-   );
- };
+ const onSubmit = async (values: z.infer<typeof formSchema>) => {
+   try {
+     await toast.promise(
+       func({ email: values.email }),
+       {
+         loading: 'Sending recovery email...',
+         success: 'Recovery email sent. Please check your inbox.',
+         error: 'Failed to send recovery email. Please try again.',
+       }
+     );
+   } catch (error) {
+     console.error('Error sending recovery email:', error);
+   }
+ };
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onSubmit = (values: z.infer<typeof formSchema>) => {
toast.promise(
func({ email: values.email }),
{
loading: 'Sending recovery email...',
success: 'Recovery email sent. Please check your inbox.',
error: 'Failed to send recovery email. Please try again.',
}
);
};
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await toast.promise(
func({ email: values.email }),
{
loading: 'Sending recovery email...',
success: 'Recovery email sent. Please check your inbox.',
error: 'Failed to send recovery email. Please try again.',
}
);
} catch (error) {
console.error('Error sending recovery email:', error);
}
};


return (
<Form {...sform}>
<form onSubmit={sform.handleSubmit(onSubmit)}>
<Card>
<CardHeader>
<CardTitle>Forgot Password</CardTitle>
<CardDescription>Enter your email to reset your password.</CardDescription>
</CardHeader>
<CardContent>
<FormField
name="email"
control={sform.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} placeholder='Enter Email' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter>
<Button type='submit' size="sm" className='h-7 gap-1'>Send Recovery Email</Button>
</CardFooter>
</Card>
</form>
</Form>
);
};

export default ForgotPasswordForm;
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ import { toast } from 'sonner'
import useProfileStore from '@/state/profileStore';
import Cookies from 'js-cookie';
import { useQueryClient } from '@tanstack/react-query'
import Link from 'next/link'

const formSchema = z.object({
email: z.string().email({
message:"Enter valid Email"
message:"Enter valid Email"
}),
password : z.string().min(2, {
message: "Enter Password.",
Expand Down Expand Up @@ -132,6 +133,9 @@ const LoginUserForm = () => {
</CardContent>
<CardFooter>
<Button type='submit' size="sm" className='h-7 gap-1'>Login</Button>
<Link href="/forgot-password" className="text-sm text-blue-600 hover:underline">
Forgot Password?
</Link>
</CardFooter>
</Card>
</form>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// src/components/Auth/CustomLoginComponent/ResetPasswordForm.tsx

import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useRouter } from 'next/navigation';
import useResetPassword from '@/hooks/create/useResetPassword';
import { Eye, EyeOff } from 'lucide-react';

const formSchema = z.object({
newPassword: z
.string()
.min(8, { message: "Password must be at least 8 characters long" })
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, {
message: "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character",
}),
confirmPassword: z.string(),
}).refine((data) => data.newPassword === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});

const ResetPasswordForm = ({ token }) => {
const router = useRouter();
const { func } = useResetPassword();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
newPassword: '',
confirmPassword: '',
},
});

const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
toast.promise(
func({ token, newPassword: values.newPassword }),
{
loading: 'Resetting password...',
success: 'Password reset successful. Please log in with your new password.',
error: 'Failed to reset password. Please try again.',
}
);
router.push('/b2c/login');
} catch (error) {
console.error('Password reset error:', error);
}
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardHeader>
<CardTitle>Reset Password</CardTitle>
<CardDescription>Enter your new password to reset your account.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="Enter new password"
{...field}
/>
<Button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm New Password</FormLabel>
<FormControl>
<div className="relative">
<Input
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm new password"
{...field}
/>
<Button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? <EyeOff size={20} /> : <Eye size={20} />}
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter>
<Button type="submit" className="w-full">
Reset Password
</Button>
</CardFooter>
</Card>
</form>
</Form>
);
};

export default ResetPasswordForm;
45 changes: 45 additions & 0 deletions apps/webapp/src/hooks/create/useInitiatePasswordRecovery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import config from '@/lib/config';
import { useMutation } from '@tanstack/react-query';
import Cookies from 'js-cookie';

const useInitiatePasswordRecovery = () => {
const call = async (data: {
email: string
}) => {
const response = await fetch(`${config.API_URL}/auth/forgot-password`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "Unknown error occurred");
}

return response.json();
};
Comment on lines +6 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure proper error handling and response parsing.

The call function correctly handles the fetch request and parses the response. However, consider the following improvements:

  • Add a timeout to the fetch request to handle cases where the server might not respond.
  • Log the error for better debugging.

Example:

+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 5000);

const response = await fetch(`${config.API_URL}/auth/forgot-password`, {
  method: 'POST',
  body: JSON.stringify(data),
  headers: {
    'Content-Type': 'application/json',
  },
+ signal: controller.signal,
});

+ clearTimeout(timeoutId);

if (!response.ok) {
  const errorData = await response.json();
+ console.error('Password recovery error:', errorData);
  throw new Error(errorData.message || "Unknown error occurred");
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const call = async (data: {
email: string
}) => {
const response = await fetch(`${config.API_URL}/auth/forgot-password`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "Unknown error occurred");
}
return response.json();
};
const call = async (data: {
email: string
}) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${config.API_URL}/auth/forgot-password`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json();
console.error('Password recovery error:', errorData);
throw new Error(errorData.message || "Unknown error occurred");
}
return response.json();
};

const func = (data: {
email: string
}) => {
return new Promise(async (resolve, reject) => {
try {
const result = await call(data);
resolve(result);

} catch (error) {
reject(error);
}
});
Comment on lines +27 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor to avoid using async Promise executor.

Using an async function as a Promise executor is not recommended. Refactor the func function to avoid this pattern.

- return new Promise(async (resolve, reject) => {
-   try {
-     const result = await call(data);
-     resolve(result);
-   } catch (error) {
-     reject(error);
-   }
- });

+ return call(data);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Promise(async (resolve, reject) => {
try {
const result = await call(data);
resolve(result);
} catch (error) {
reject(error);
}
});
return call(data);
Tools
Biome

[error] 27-35: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

};
return {
mutationFn: useMutation({
mutationFn: call,
}),
func
}
};

export default useInitiatePasswordRecovery;
46 changes: 46 additions & 0 deletions apps/webapp/src/hooks/create/useResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import config from '@/lib/config';
import { useMutation } from '@tanstack/react-query';
import Cookies from 'js-cookie';

interface ResetPasswordData {
token: string;
newPassword: string;
}

const useResetPassword = () => {
const call = async (data: ResetPasswordData) => {
const response = await fetch(`${config.API_URL}/auth/reset-password`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "Unknown error occurred");
}

return response.json();
Comment on lines +11 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure proper error handling and response parsing.

The call function correctly handles the fetch request and parses the response. However, consider the following improvements:

  • Add a timeout to the fetch request to handle cases where the server might not respond.
  • Log the error for better debugging.

Example:

+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 5000);

const response = await fetch(`${config.API_URL}/auth/reset-password`, {
  method: 'POST',
  body: JSON.stringify(data),
  headers: {
    'Content-Type': 'application/json',
  },
+ signal: controller.signal,
});

+ clearTimeout(timeoutId);

if (!response.ok) {
  const errorData = await response.json();
+ console.error('Password reset error:', errorData);
  throw new Error(errorData.message || "Unknown error occurred");
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const call = async (data: ResetPasswordData) => {
const response = await fetch(`${config.API_URL}/auth/reset-password`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || "Unknown error occurred");
}
return response.json();
const call = async (data: ResetPasswordData) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${config.API_URL}/auth/reset-password`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json();
console.error('Password reset error:', errorData);
throw new Error(errorData.message || "Unknown error occurred");
}
return response.json();

};
const func = (data: ResetPasswordData) => {
return new Promise(async (resolve, reject) => {
try {
const result = await call(data);
resolve(result);

} catch (error) {
reject(error);
}
});
Comment on lines +28 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor to avoid using async Promise executor.

Using an async function as a Promise executor is not recommended. Refactor the func function to avoid this pattern.

- return new Promise(async (resolve, reject) => {
-   try {
-     const result = await call(data);
-     resolve(result);
-   } catch (error) {
-     reject(error);
-   }
- });

+ return call(data);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Promise(async (resolve, reject) => {
try {
const result = await call(data);
resolve(result);
} catch (error) {
reject(error);
}
});
return call(data);
Tools
Biome

[error] 28-36: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

};
return {
mutationFn: useMutation({
mutationFn: call,
}),
func
}
};

export default useResetPassword;
Loading
Loading