diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index f4ac926..155df5d 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -24,6 +24,7 @@ import PageRedirect from "@/pages/page/page-redirect.tsx"; import Layout from "@/components/layouts/global/layout.tsx"; import { ErrorBoundary } from "react-error-boundary"; import InviteSignup from "@/pages/auth/invite-signup.tsx"; +import ForgotPassword from "@/pages/auth/forgot-password.tsx"; export default function App() { const [, setSocket] = useAtom(socketAtom); @@ -61,6 +62,7 @@ export default function App() { } /> } /> + } /> } /> } /> diff --git a/apps/client/src/features/auth/components/auth.module.css b/apps/client/src/features/auth/components/auth.module.css index 958dccf..a7f6962 100644 --- a/apps/client/src/features/auth/components/auth.module.css +++ b/apps/client/src/features/auth/components/auth.module.css @@ -3,3 +3,12 @@ border-radius: 4px; background: light-dark(var(--mantine-color-body), rgba(0, 0, 0, 0.1)); } + +.forgotPasswordBtn { + font-size: var(--input-label-size, var(--mantine-font-size-sm)); + text-decoration: underline; +} + +.formElemWithTopMargin { + margin-top: var(--mantine-spacing-md); +} diff --git a/apps/client/src/features/auth/components/forgot-password-form.tsx b/apps/client/src/features/auth/components/forgot-password-form.tsx new file mode 100644 index 0000000..1f1f572 --- /dev/null +++ b/apps/client/src/features/auth/components/forgot-password-form.tsx @@ -0,0 +1,109 @@ +import * as React from "react"; +import {useState} from "react"; +import * as z from "zod"; +import {useForm, zodResolver} from "@mantine/form"; +import useAuth from "@/features/auth/hooks/use-auth"; +import {IForgotPassword} from "@/features/auth/types/auth.types"; +import {Box, Button, Container, PasswordInput, TextInput, Title,} from "@mantine/core"; +import classes from "./auth.module.css"; +import {useRedirectIfAuthenticated} from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import {notifications} from "@mantine/notifications"; + +const stepOneSchema = z.object({ + email: z + .string() + .min(1, {message: "Email is required"}) + .email({message: "Invalid email address"}), +}); + +const stepTwoSchema = z.object({ + email: z + .string() + .min(1, {message: "Email is required"}) + .email({message: "Invalid email address"}), + token: z.string().min(1, {message: 'Token is required'}), + newPassword: z.string().min(8, {message: 'Password must contain at least 8 character(s)'}), +}); + +export function ForgotPasswordForm() { + const {forgotPassword, isLoading} = useAuth(); + const [isTokenSend, setIsTokenSend] = useState(false) + useRedirectIfAuthenticated(); + + const form = useForm({ + validate: isTokenSend ? zodResolver(stepTwoSchema) : zodResolver(stepOneSchema), + initialValues: { + email: "", + token: null, + newPassword: null, + }, + }); + + async function onSubmit(data: IForgotPassword) { + const success = await forgotPassword(data); + + if (success) { + if (isTokenSend) { + notifications.show({ + message: 'Password updated', + color: "green", + }); + } + + if (!isTokenSend) { + setIsTokenSend(true); + } + } + } + + return ( + + + + Forgot password + + +
+ + + { + isTokenSend + && ( + <> + + + + + ) + } + + + +
+
+ ); +} diff --git a/apps/client/src/features/auth/components/login-form.tsx b/apps/client/src/features/auth/components/login-form.tsx index 9433b1a..d7bb6b9 100644 --- a/apps/client/src/features/auth/components/login-form.tsx +++ b/apps/client/src/features/auth/components/login-form.tsx @@ -10,9 +10,13 @@ import { Button, PasswordInput, Box, + UnstyledButton, } from "@mantine/core"; import classes from "./auth.module.css"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import clsx from "clsx"; +import {useNavigate} from "react-router-dom"; +import APP_ROUTE from "@/lib/app-route.ts"; const formSchema = z.object({ email: z @@ -24,6 +28,7 @@ const formSchema = z.object({ export function LoginForm() { const { signIn, isLoading } = useAuth(); + const navigate = useNavigate(); useRedirectIfAuthenticated(); const form = useForm({ @@ -62,6 +67,14 @@ export function LoginForm() { mt="md" {...form.getInputProps("password")} /> + + navigate(APP_ROUTE.AUTH.FORGOT_PASSWORD)} + className = {clsx(classes.forgotPasswordBtn, classes.formElemWithTopMargin)}> +
+ Forgot Password +
+
diff --git a/apps/client/src/features/auth/hooks/use-auth.ts b/apps/client/src/features/auth/hooks/use-auth.ts index ede22a0..0210db8 100644 --- a/apps/client/src/features/auth/hooks/use-auth.ts +++ b/apps/client/src/features/auth/hooks/use-auth.ts @@ -1,10 +1,10 @@ import { useState } from "react"; -import { login, setupWorkspace } from "@/features/auth/services/auth-service"; +import {forgotPassword, login, setupWorkspace} from "@/features/auth/services/auth-service"; import { useNavigate } from "react-router-dom"; import { useAtom } from "jotai"; import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom"; import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; -import { ILogin, ISetupWorkspace } from "@/features/auth/types/auth.types"; +import {IForgotPassword, ILogin, ISetupWorkspace} from "@/features/auth/types/auth.types"; import { notifications } from "@mantine/notifications"; import { IAcceptInvite } from "@/features/workspace/types/workspace.types.ts"; import { acceptInvitation } from "@/features/workspace/services/workspace-service.ts"; @@ -105,12 +105,33 @@ export default function useAuth() { navigate(APP_ROUTE.AUTH.LOGIN); }; + const handleForgotPassword = async (data: IForgotPassword) => { + setIsLoading(true); + + try { + const res = await forgotPassword(data); + setIsLoading(false); + + return true; + } catch (err) { + console.log(err); + setIsLoading(false); + notifications.show({ + message: err.response?.data.message, + color: "red", + }); + + return false; + } + }; + return { signIn: handleSignIn, invitationSignup: handleInvitationSignUp, setupWorkspace: handleSetupWorkspace, isAuthenticated: handleIsAuthenticated, logout: handleLogout, + forgotPassword: handleForgotPassword, hasTokens, isLoading, }; diff --git a/apps/client/src/features/auth/services/auth-service.ts b/apps/client/src/features/auth/services/auth-service.ts index b34b47f..8374ba8 100644 --- a/apps/client/src/features/auth/services/auth-service.ts +++ b/apps/client/src/features/auth/services/auth-service.ts @@ -1,6 +1,7 @@ import api from "@/lib/api-client"; import { IChangePassword, + IForgotPassword, ILogin, IRegister, ISetupWorkspace, @@ -31,3 +32,9 @@ export async function setupWorkspace( const req = await api.post("/auth/setup", data); return req.data; } + +export async function forgotPassword(data: IForgotPassword): Promise { + const req = await api.post("/auth/forgot-password", data); + return req.data; +} + diff --git a/apps/client/src/features/auth/types/auth.types.ts b/apps/client/src/features/auth/types/auth.types.ts index 0aef005..eec4de7 100644 --- a/apps/client/src/features/auth/types/auth.types.ts +++ b/apps/client/src/features/auth/types/auth.types.ts @@ -3,6 +3,12 @@ export interface ILogin { password: string; } +export interface IForgotPassword { + email: string; + token: string; + newPassword: string; +} + export interface IRegister { name?: string; email: string; diff --git a/apps/client/src/lib/app-route.ts b/apps/client/src/lib/app-route.ts index 5821683..c955462 100644 --- a/apps/client/src/lib/app-route.ts +++ b/apps/client/src/lib/app-route.ts @@ -4,6 +4,7 @@ const APP_ROUTE = { LOGIN: "/login", SIGNUP: "/signup", SETUP: "/setup/register", + FORGOT_PASSWORD: "/forgotPassword", }, SETTINGS: { ACCOUNT: { diff --git a/apps/client/src/pages/auth/forgot-password.tsx b/apps/client/src/pages/auth/forgot-password.tsx new file mode 100644 index 0000000..a7df477 --- /dev/null +++ b/apps/client/src/pages/auth/forgot-password.tsx @@ -0,0 +1,13 @@ +import { Helmet } from "react-helmet-async"; +import {ForgotPasswordForm} from "@/features/auth/components/forgot-password-form.tsx"; + +export default function ForgotPassword() { + return ( + <> + + Forgot Password + + + + ); +}