Skip to content

Commit

Permalink
Merge pull request #591 from panoratech/feat/auth-password
Browse files Browse the repository at this point in the history
✨ Add password recovery
  • Loading branch information
naelob authored Aug 2, 2024
2 parents 01e7c26 + 1faf9bc commit 8b30a6a
Show file tree
Hide file tree
Showing 84 changed files with 5,127 additions and 1,139 deletions.
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
MAIL_USER=[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';
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' }),
});

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.',
}
);
};

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 }: {token: string}) => {
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;
2 changes: 1 addition & 1 deletion apps/webapp/src/hooks/create/useCreateBatchLinkedUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface ILinkedUserDto {
}
const useCreateBatchLinkedUser = () => {
const add = async (linkedUserData: ILinkedUserDto) => {
const response = await fetch(`${config.API_URL}/linked-users/internal/batch`, {
const response = await fetch(`${config.API_URL}/linked_users/internal/batch`, {
method: 'POST',
body: JSON.stringify(linkedUserData),
headers: {
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/src/hooks/create/useCreateWebhook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface IWebhookDto {
}
const useCreateWebhook = () => {
const add = async (data: IWebhookDto) => {
const response = await fetch(`${config.API_URL}/webhook/internal`, {
const response = await fetch(`${config.API_URL}/webhooks/internal`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
Expand Down
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();
};
const func = (data: {
email: string
}) => {
return new Promise(async (resolve, reject) => {
try {
const result = await call(data);
resolve(result);

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

export default useInitiatePasswordRecovery;
Loading

0 comments on commit 8b30a6a

Please sign in to comment.