Skip to content

Commit

Permalink
Merge pull request #54 from blockydevs/HAF-12-forgot-password-reset-p…
Browse files Browse the repository at this point in the history
…assword

Haf 12 forgot password reset password
  • Loading branch information
MicDebBlocky committed Apr 22, 2024
2 parents d959a5c + bea419d commit 1bdd9ca
Show file tree
Hide file tree
Showing 14 changed files with 392 additions and 95 deletions.
6 changes: 6 additions & 0 deletions packages/apps/human-app/frontend/src/api/api-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ export const apiPaths = {
signUp: {
path: '/auth/signup',
},
sendResetLink: {
path: '/auth/reset-link',
},
resetPassword: {
path: '/auth/reset-password',
},
},
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from 'zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { apiClient } from '@/api/api-client';
import { apiPaths } from '@/api/api-paths';
import { routerPaths } from '@/router/router-paths';

export const sendResetLinkDtoSchema = z.object({
email: z.string().email(),
});

export type SendResetLinkDto = z.infer<typeof sendResetLinkDtoSchema>;

const SendResetLinkSuccessResponseSchema = z.unknown();

function sendResetLinkMutationFn(data: SendResetLinkDto) {
return apiClient(apiPaths.worker.sendResetLink.path, {
successSchema: SendResetLinkSuccessResponseSchema,
options: { method: 'POST', body: JSON.stringify(data) },
});
}

export function useSendResetLinkMutation() {
const queryClient = useQueryClient();
const navigate = useNavigate();

return useMutation({
mutationFn: sendResetLinkMutationFn,
onSuccess: async (_, { email }) => {
navigate(routerPaths.worker.sendResetLinkSuccess, { state: { email } });
await queryClient.invalidateQueries();
},
onError: async () => {
await queryClient.invalidateQueries();
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const signInSuccessResponseSchema = z.unknown();
function signInMutationFn(data: SignInDto) {
return apiClient(apiPaths.worker.signIn.path, {
successSchema: signInSuccessResponseSchema,
options: { body: JSON.stringify(data) },
options: { method: 'POST', body: JSON.stringify(data) },
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { t } from 'i18next';
import { z } from 'zod';
import type { PasswordCheck } from '@/components/data-entry/password/password-check-label';
import {
password8Chars,
passwordLowercase,
passwordNumeric,
passwordSpecialCharacter,
passwordUppercase,
} from '@/shared/helpers/regex';

export const passwordChecks: PasswordCheck[] = [
{
requirementsLabel: t('validation.password8Chars'),
schema: z.string().regex(password8Chars),
},
{
requirementsLabel: t('validation.passwordUppercase'),
schema: z.string().regex(passwordUppercase),
},
{
requirementsLabel: t('validation.passwordLowercase'),
schema: z.string().regex(passwordLowercase),
},
{
requirementsLabel: t('validation.passwordNumeric'),
schema: z.string().regex(passwordNumeric),
},
{
requirementsLabel: t('validation.passwordSpecialCharacter'),
schema: z.string().regex(passwordSpecialCharacter),
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Box, Grid, Typography, styled } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { breakpoints } from '@/styles/theme';
import { routerPaths } from '@/router/router-paths';
import { Alert } from '@/components/ui/alert';
import { colorPalette } from '@/styles/color-palette';
import { useBackgroundColorStore } from '@/hooks/use-background-store';

const IconWrapper = styled('div')(() => ({
width: '40px',
Expand All @@ -30,20 +31,30 @@ interface FormCardProps {
childrenMaxWidth?: string;
backArrowPath?: string | -1;
cancelBtnPath?: string | -1;
withLayoutBackground?: boolean;
}

export function FormCard({
export function PageCard({
title,
children,
alert,
cardMaxWidth = '1200px',
childrenMaxWidth = '486px',
backArrowPath = -1,
backArrowPath,
cancelBtnPath = routerPaths.homePage,
withLayoutBackground = true,
}: FormCardProps) {
const { setGrayBackground } = useBackgroundColorStore();
const navigate = useNavigate();
const { t } = useTranslation();

useEffect(() => {
if (withLayoutBackground) {
setGrayBackground();
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- call this effect once
}, []);

const goBack = (path: string | -1) => {
if (typeof path === 'string') {
navigate(path);
Expand All @@ -62,6 +73,7 @@ export function FormCard({
alignItems: 'center',
gap: '2rem',
borderRadius: '20px',
minHeight: '70vh',
maxWidth: cardMaxWidth,
width: '100%',
background: colorPalette.white,
Expand Down Expand Up @@ -115,9 +127,11 @@ export function FormCard({
},
}}
>
<IconWrapper onClick={goBack.bind(null, backArrowPath)}>
<ArrowBackIcon />
</IconWrapper>
{backArrowPath ? (
<IconWrapper onClick={goBack.bind(null, backArrowPath)}>
<ArrowBackIcon />
</IconWrapper>
) : null}
<Button onClick={goBack.bind(null, cancelBtnPath)}>
<Typography variant="buttonMedium">
{t('components.modal.header.closeBtn')}
Expand All @@ -130,19 +144,16 @@ export function FormCard({
md={10}
order={{ xs: 2, md: 2 }}
sx={{
height: '3rem',
minHeight: '3rem',
width: '100%',
[breakpoints.mobile]: {
height: 'auto',
minHeight: 'unset',
},
}}
xs={12}
>
{alert ? (
<Alert color="error" severity="error" sx={{ width: '100%' }}>
{alert}
</Alert>
) : null}
{alert ? <>{alert}</> : null}
</Grid>
<Grid
item
Expand All @@ -155,9 +166,11 @@ export function FormCard({
}}
xs={12}
>
<IconWrapper onClick={goBack.bind(null, backArrowPath)}>
<ArrowBackIcon />
</IconWrapper>
{backArrowPath ? (
<IconWrapper onClick={goBack.bind(null, backArrowPath)}>
<ArrowBackIcon />
</IconWrapper>
) : null}
</Grid>
<Grid item md={10} order={{ xs: 4, md: 4 }} xs={12}>
<Typography variant="h4">{title}</Typography>
Expand Down
44 changes: 44 additions & 0 deletions packages/apps/human-app/frontend/src/hooks/use-location-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access -- that's ok because we validate this members with zod */
import { useEffect, useState } from 'react';
import type { Location } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import type { z } from 'zod';
import { routerPaths } from '@/router/router-paths';

interface UseLocationStateProps<T> {
schema: z.ZodSchema<T>;
locationStorage?: keyof Location;
keyInStorage?: string;
onErrorRedirectPath?: string;
}

export function useLocationState<T>({
keyInStorage,
onErrorRedirectPath = routerPaths.homePage,
locationStorage = 'state',
schema,
}: UseLocationStateProps<T>) {
const location = useLocation();
const navigate = useNavigate();
const [fieldFromState, setFieldFromState] = useState<T>();

useEffect(() => {
try {
const storage = (
keyInStorage
? location[locationStorage]?.[keyInStorage]
: location[locationStorage]
) as unknown;

const validFiled = schema.parse(storage);
setFieldFromState(validFiled);
} catch {
navigate(onErrorRedirectPath, { replace: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- call this effect once
}, []);

return {
field: fieldFromState,
};
}
27 changes: 25 additions & 2 deletions packages/apps/human-app/frontend/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
"passwordSpecialCharacter": "1 special character (@#$%^&+=!*])"
},
"errors": {
"unknown": "Unknown error",
"withInfoCode": "An error has occurred - error code {{code}}"
"errorWithMessage": "Error with message: {{message}}",
"errorWithStatusCode": "Error with status code: {{code}}",
"unknown": "An error occurred, please try again."
},
"components": {
"multiSelect": {
Expand Down Expand Up @@ -87,6 +88,9 @@
"confirmPassword": "Confirm Password",
"email": "your@email.com"
},
"errors": {
"emailTaken": "Email is already taken. Please sign in."
},
"title": "Sign Up",
"submitBtn": "Sign up",
"passwordDetailedValidation": "Password must contain at least:",
Expand All @@ -97,10 +101,29 @@
"password": "Password",
"email": "your@email.com"
},
"errors": {
"invalidCredentials": "Incorrect email or password!"
},
"title": "Sign In",
"submitBtn": "Sign in",
"forgotPassword": "Forgot password?"
},
"sendResetLinkForm": {
"fields": {
"email": "your@email.com"
},
"title": "Reset Password",
"description": "Please enter your email address. We will send you an email to reset your password.",
"submitBtn": "Send Email"
},
"sendResetLinkSuccess": {
"title": "Reset password",
"paragraph1": "We’ve sent an email to <1>{{email}}</1> Please check your inbox and verify your email.",
"paragraph2": "The verification link is valid for 24 hours.",
"paragraph3": "<1>Can’t find our email?</1> Please check your spam folder, or click below and we will send you a new one.",
"btn": "Sign In",
"paragraph4": "<1>Having trouble?</1> Please <2>Contact Support</2>"
},
"profile": {
"profileHeader": "Profile",
"email": "Email",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Grid, Typography } from '@mui/material';
import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { z } from 'zod';
import { PageCard } from '@/components/ui/page-card';
import { Button } from '@/components/ui/button';
import { routerPaths } from '@/router/router-paths';
import { colorPalette } from '@/styles/color-palette';
import { useLocationState } from '@/hooks/use-location-state';

export function SendResetLinkWorkerSuccessPage() {
const { t } = useTranslation();
const { field: email } = useLocationState({
keyInStorage: 'email',
schema: z.string().email(),
});

return (
<PageCard title={t('worker.sendResetLinkForm.title')}>
<Grid container gap="2rem">
<Typography>
<Trans
i18nKey="worker.sendResetLinkSuccess.paragraph1"
values={{ email }}
>
Strong <Typography variant="buttonMedium" />
</Trans>
</Typography>
<Typography color={colorPalette.primary.light} variant="body1">
{t('worker.sendResetLinkSuccess.paragraph2')}
</Typography>
<Typography variant="body1">
<Trans
i18nKey="worker.sendResetLinkSuccess.paragraph3"
values={{ email }}
>
Strong <Typography variant="buttonMedium" />
</Trans>
</Typography>
<Button
component={Link}
fullWidth
to={routerPaths.worker.signIn}
variant="contained"
>
{t('worker.sendResetLinkSuccess.btn')}
</Button>

<Typography variant="body1">
<Trans
i18nKey="worker.sendResetLinkSuccess.paragraph4"
values={{ email }}
>
Strong
<Typography variant="buttonMedium" />
<Link to={routerPaths.homePage} />
</Trans>
</Typography>
</Grid>
</PageCard>
);
}
Loading

0 comments on commit 1bdd9ca

Please sign in to comment.