Skip to content

Commit

Permalink
chore: PasswordVerifier spec (#29995)
Browse files Browse the repository at this point in the history
Co-authored-by: juliajforesti <[email protected]>
  • Loading branch information
ggazzo and juliajforesti authored Aug 30, 2023
1 parent 1b42dfc commit 781ed63
Show file tree
Hide file tree
Showing 17 changed files with 402 additions and 64 deletions.
20 changes: 16 additions & 4 deletions apps/meteor/client/views/account/profile/AccountProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Field, FieldGroup, TextInput, TextAreaInput, Box, Icon, PasswordInput, Button } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { CustomFieldsForm, PasswordVerifier } from '@rocket.chat/ui-client';
import { CustomFieldsForm, PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client';
import {
useAccountsCustomFields,
useToastMessageDispatch,
Expand Down Expand Up @@ -91,6 +91,8 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
}
};

const passwordIsValid = useValidatePassword(password);

// FIXME: replace to endpoint
const updateOwnBasicInfo = useMethod('saveUserProfile');

Expand Down Expand Up @@ -128,6 +130,7 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
const emailId = useUniqueId();
const passwordId = useUniqueId();
const confirmPasswordId = useUniqueId();
const passwordVerifierId = useUniqueId();

return (
<Box {...props} is='form' autoComplete='off' onSubmit={handleSubmit(handleSave)}>
Expand Down Expand Up @@ -292,13 +295,23 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
<Field.Row>
<PasswordInput
id={passwordId}
{...register('password')}
{...register('password', {
validate: () => (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true),
})}
error={errors.password?.message}
flexGrow={1}
addon={<Icon name='key' size='x20' />}
disabled={!allowPasswordChange}
aria-describedby={passwordVerifierId}
aria-invalid={errors.password ? 'true' : 'false'}
/>
</Field.Row>
{errors?.password && (
<Field.Error aria-live='assertive' id={`${passwordId}-error`}>
{errors.password.message}
</Field.Error>
)}
{allowPasswordChange && <PasswordVerifier password={password} id={passwordVerifierId} />}
</Field>
<Field>
<Field.Label htmlFor={confirmPasswordId}>{t('Confirm_password')}</Field.Label>
Expand All @@ -311,7 +324,7 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
error={errors.confirmationPassword?.message}
flexGrow={1}
addon={<Icon name='key' size='x20' />}
disabled={!allowPasswordChange}
disabled={!allowPasswordChange || !passwordIsValid}
aria-required={password !== '' ? 'true' : 'false'}
aria-invalid={errors.confirmationPassword ? 'true' : 'false'}
aria-describedby={`${confirmPasswordId}-error ${confirmPasswordId}-hint`}
Expand All @@ -323,7 +336,6 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
{errors.confirmationPassword.message}
</Field.Error>
)}
{allowPasswordChange && <PasswordVerifier password={password} />}
</Field>
{customFieldsMetadata && <CustomFieldsForm formName='customFields' formControl={control} metadata={customFieldsMetadata} />}
</FieldGroup>
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3935,6 +3935,8 @@
"Password_History_Amount_Description": "Amount of most recently used passwords to prevent users from reusing.",
"Password_must_have": "Password must have:",
"Password_Policy": "Password Policy",
"Password_Policy_Aria_Description": "Below it's listed the password requirement verifications",
"Password_must_meet_the_complexity_requirements": "Password must meet the complexity requirements.",
"Password_to_access": "Password to access",
"Passwords_do_not_match": "Passwords do not match",
"Past_Chats": "Past Chats",
Expand Down
4 changes: 3 additions & 1 deletion packages/ui-client/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export default {
errorOnDeprecated: true,

testEnvironment: 'jsdom',
modulePathIgnorePatterns: ['<rootDir>/dist/'],
testMatch: ['<rootDir>/src/**/**.spec.[jt]s?(x)'],
Expand All @@ -22,4 +21,7 @@ export default {
'\\.css$': 'identity-obj-proxy',
'^react($|/.+)': '<rootDir>/../../node_modules/react$1',
},
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
};
1 change: 1 addition & 0 deletions packages/ui-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@storybook/react": "~6.5.16",
"@storybook/testing-library": "~0.0.13",
"@swc/jest": "^0.2.26",
"@testing-library/jest-dom": "~5.16.5",
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/babel__core": "~7.20.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import type { ComponentMeta, ComponentStory } from '@storybook/react';

import { PasswordVerifier } from './PasswordVerifier';

type Response = {
enabled: boolean;
policy: [
name: string,
value?:
| {
[x: string]: number;
}
| undefined,
][];
};

export default {
title: 'Components/PasswordVerifier',
component: PasswordVerifier,
} as ComponentMeta<typeof PasswordVerifier>;

const response: Response = {
enabled: true,
policy: [
['get-password-policy-minLength', { minLength: 10 }],
['get-password-policy-forbidRepeatingCharactersCount', { maxRepeatingChars: 3 }],
['get-password-policy-mustContainAtLeastOneLowercase'],
['get-password-policy-mustContainAtLeastOneUppercase'],
['get-password-policy-mustContainAtLeastOneNumber'],
['get-password-policy-mustContainAtLeastOneSpecialCharacter'],
],
};

const Wrapper = mockAppRoot()
.withEndpoint('GET', '/v1/pw.getPolicy', () => response)
.build();

export const Default: ComponentStory<typeof PasswordVerifier> = (args) => (
<Wrapper>
<PasswordVerifier {...args} />
</Wrapper>
);

Default.storyName = 'PasswordVerifier';
Default.args = {
password: 'asd',
};
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
import { Box } from '@rocket.chat/fuselage';
import { Box, Skeleton } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useVerifyPassword } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';

import { PasswordVerifierItemCorrect } from './PasswordVerifierCorrect';
import { PasswordVerifierItemInvalid } from './PasswordVerifierInvalid';
import { PasswordVerifierItem } from './PasswordVerifierItem';

type PasswordVerifierProps = {
password: string;
id?: string;
};

export const PasswordVerifier = ({ password }: PasswordVerifierProps) => {
export const PasswordVerifier = ({ password, id }: PasswordVerifierProps) => {
const { t } = useTranslation();
const uniqueId = useUniqueId();

const passwordVerifications = useVerifyPassword(password);
const { data: passwordVerifications, isLoading } = useVerifyPassword(password);

if (!passwordVerifications.length) {
return <></>;
if (isLoading) {
return <Skeleton data-testid='password-verifier-skeleton' w='full' mbe={8} />;
}

if (!passwordVerifications?.length) {
return null;
}

return (
<Box display='flex' flexDirection='column' mbs={8}>
<Box mbe={8} fontScale='c2'>
{t('Password_must_have')}
</Box>
<Box display='flex' flexWrap='wrap'>
{passwordVerifications.map(({ isValid, limit, name }) =>
isValid ? (
<PasswordVerifierItemCorrect key={name} text={t(`${name}-label`, { limit })} />
) : (
<PasswordVerifierItemInvalid key={name} text={t(`${name}-label`, { limit })} />
),
)}
<>
<span id={id} hidden>
{t('Password_Policy_Aria_Description')}
</span>
<Box display='flex' flexDirection='column' mbs={8}>
<Box mbe={8} fontScale='c2' id={uniqueId} aria-hidden>
{t('Password_must_have')}
</Box>
<Box display='flex' flexWrap='wrap' role='list' aria-labelledby={uniqueId}>
{passwordVerifications.map(({ isValid, limit, name }) => (
<PasswordVerifierItem key={name} text={t(`${name}-label`, { limit })} isValid={isValid} aria-invalid={!isValid} />
))}
</Box>
</Box>
</Box>
</>
);
};

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
import { Box, Icon } from '@rocket.chat/fuselage';
import { AllHTMLAttributes, ComponentProps } from 'react';

const variants: {
[key: string]: {
icon: ComponentProps<typeof Icon>['name'];
color: string;
};
} = {
success: {
icon: 'success-circle',
color: 'status-font-on-success',
},
error: {
icon: 'error-circle',
color: 'status-font-on-danger',
},
};

export const PasswordVerifierItem = ({
text,
color,
icon,
}: {
text: string;
color: 'status-font-on-success' | 'status-font-on-danger';
icon: 'success-circle' | 'error-circle';
}) => (
<Box display='flex' flexBasis='50%' alignItems='center' mbe={8} fontScale='c1' color={color}>
<Icon name={icon} color={color} size='x16' mie={4} />
{text}
</Box>
);
isValid,
...props
}: { text: string; isValid: boolean } & Omit<AllHTMLAttributes<HTMLElement>, 'is'>) => {
const { icon, color } = variants[isValid ? 'success' : 'error'];
return (
<Box
display='flex'
flexBasis='50%'
alignItems='center'
mbe={8}
fontScale='c1'
color={color}
role='listitem'
aria-label={text}
{...props}
>
<Icon name={icon} color={color} size='x16' mie={4} />
<span aria-hidden>{text}</span>
</Box>
);
};
Loading

0 comments on commit 781ed63

Please sign in to comment.