diff --git a/.changeset/beige-experts-applaud.md b/.changeset/beige-experts-applaud.md new file mode 100644 index 0000000000000..12c2ce5ba1862 --- /dev/null +++ b/.changeset/beige-experts-applaud.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/password-policies": minor +"@rocket.chat/ui-client": minor +"@rocket.chat/ui-contexts": minor +--- + +Adds complexity requirements to end-to-end encryption passphrase diff --git a/apps/meteor/client/views/account/security/ChangePassphrase.tsx b/apps/meteor/client/views/account/security/ChangePassphrase.tsx new file mode 100644 index 0000000000000..752d34ee6f47e --- /dev/null +++ b/apps/meteor/client/views/account/security/ChangePassphrase.tsx @@ -0,0 +1,211 @@ +import { Box, Field, FieldError, FieldGroup, FieldHint, FieldLabel, FieldRow, PasswordInput, Button } from '@rocket.chat/fuselage'; +import { PasswordVerifierList } from '@rocket.chat/ui-client'; +import { useToastMessageDispatch, usePasswordPolicy } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import DOMPurify from 'dompurify'; +import { useEffect, useId } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; + +import { e2e } from '../../../lib/e2ee/rocketchat.e2e'; +import { useE2EEState } from '../../room/hooks/useE2EEState'; + +const PASSPHRASE_POLICY = Object.freeze({ + enabled: true, + minLength: 30, + mustContainAtLeastOneLowercase: true, + mustContainAtLeastOneUppercase: true, + mustContainAtLeastOneNumber: true, + mustContainAtLeastOneSpecialCharacter: true, + forbidRepeatingCharacters: false, +}); + +const useKeysExist = () => { + const state = useE2EEState(); + return state === 'READY' || state === 'SAVE_PASSWORD'; +}; + +const useValidatePassphrase = (passphrase: string) => { + const validate = usePasswordPolicy(PASSPHRASE_POLICY); + return validate(passphrase); +}; + +const useChangeE2EPasswordMutation = () => { + return useMutation({ + mutationFn: async (newPassword: string) => { + await e2e.changePassword(newPassword); + }, + }); +}; + +const defaultValues = { + passphrase: '', + confirmationPassphrase: '', +}; + +export const ChangePassphrase = (): JSX.Element => { + const { t } = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const uniqueId = useId(); + const passphraseId = `passphrase-${uniqueId}`; + const passphraseHintId = `${passphraseId}-hint`; + const passphraseErrorId = `${passphraseId}-error`; + const confirmPassphraseId = `confirm-passphrase-${uniqueId}`; + const confirmPassphraseErrorId = `${confirmPassphraseId}-error`; + const passphraseVerifierId = `verifier-${uniqueId}`; + const e2ePasswordExplanationId = `explanation-${uniqueId}`; + + const { + watch, + formState: { errors, isValid }, + handleSubmit, + reset, + resetField, + control, + trigger, + } = useForm({ + defaultValues, + mode: 'all', + }); + + const { passphrase, confirmationPassphrase } = watch(); + const { validations, valid } = useValidatePassphrase(passphrase); + useEffect(() => { + if (!valid) { + resetField('confirmationPassphrase'); + return; + } + if (confirmationPassphrase) { + const validateConfirmation = async () => { + await trigger('confirmationPassphrase'); + }; + void validateConfirmation(); + } + }, [valid, confirmationPassphrase, resetField, trigger]); + const keysExist = useKeysExist(); + + const updatePassword = useChangeE2EPasswordMutation(); + + const handleSave = async ({ passphrase }: { passphrase: string }) => { + try { + await updatePassword.mutateAsync(passphrase); + dispatchToastMessage({ type: 'success', message: t('Encryption_key_saved_successfully') }); + reset(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }; + + return ( + <> + + + + {t('Change_E2EE_password')} + + + + {t('New_E2EE_password')} + + (valid ? true : t('Password_must_meet_the_complexity_requirements')), + }} + render={({ field }) => ( + + )} + /> + + {errors.passphrase && ( + + {errors.passphrase.message} + + )} + {keysExist ? ( + + ) : ( + + + To set a new password, first + { + e.preventDefault(); + await e2e.decodePrivateKeyFlow(); + }} + > + enter your current E2EE password. + + + + )} + + {valid && ( + + {t('Confirm_new_E2EE_password')} + + (passphrase !== value ? t('Passwords_do_not_match') : true), + }} + render={({ field }) => ( + + )} + /> + + {errors.confirmationPassphrase && ( + + {errors.confirmationPassphrase.message} + + )} + + )} + + + + + ); +}; diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index 6285ac8476fac..217430b3cf80d 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -1,174 +1,14 @@ -import { Box, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button, Divider } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import DOMPurify from 'dompurify'; -import { Accounts } from 'meteor/accounts-base'; -import type { ComponentProps, ReactElement } from 'react'; -import { useId, useEffect } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { Trans, useTranslation } from 'react-i18next'; +import { Box, Divider } from '@rocket.chat/fuselage'; -import { e2e } from '../../../lib/e2ee/rocketchat.e2e'; -import { useResetE2EPasswordMutation } from '../../hooks/useResetE2EPasswordMutation'; - -const EndToEnd = (props: ComponentProps): ReactElement => { - const { t } = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - - const publicKey = Accounts.storageLocation.getItem('public_key'); - const privateKey = Accounts.storageLocation.getItem('private_key'); - - const resetE2EPassword = useResetE2EPasswordMutation(); - - const { - handleSubmit, - watch, - resetField, - formState: { errors, isValid }, - control, - } = useForm({ - defaultValues: { - password: '', - passwordConfirm: '', - }, - }); - - const { password } = watch(); - - /** - * TODO: We need to figure out a way to make this reactive, - * so the form will allow change password as soon the user enter the current E2EE password - */ - const keysExist = Boolean(publicKey && privateKey); - - const hasTypedPassword = Boolean(password?.trim().length); - - const saveNewPassword = async (data: { password: string; passwordConfirm: string }) => { - try { - await e2e.changePassword(data.password); - resetField('password'); - dispatchToastMessage({ type: 'success', message: t('Encryption_key_saved_successfully') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }; - - useEffect(() => { - if (password?.trim() === '') { - resetField('passwordConfirm'); - } - }, [password, resetField]); - - const passwordId = useId(); - const e2ePasswordExplanationId = useId(); - const passwordConfirmId = useId(); +import { ChangePassphrase } from './ChangePassphrase'; +import { ResetPassphrase } from './ResetPassphrase'; +const EndToEnd = (): JSX.Element => { return ( - - - - - {t('Change_E2EE_password')} - - - - {t('New_E2EE_password')} - - ( - - )} - /> - - {!keysExist && ( - - - To set a new password, first - { - e.preventDefault(); - await e2e.decodePrivateKeyFlow(); - }} - > - enter your current E2EE password. - - - - )} - {errors?.password && ( - - {errors.password.message} - - )} - - {hasTypedPassword && ( - - {t('Confirm_new_E2EE_password')} - - (password !== value ? t('Passwords_do_not_match') : true), - }} - render={({ field }) => ( - - )} - /> - - {errors.passwordConfirm && ( - - {errors.passwordConfirm.message} - - )} - - )} - - - + + - - - {t('Reset_E2EE_password')} - - - {t('Reset_E2EE_password_description')} - - - + ); }; diff --git a/apps/meteor/client/views/account/security/ResetPassphrase.tsx b/apps/meteor/client/views/account/security/ResetPassphrase.tsx new file mode 100644 index 0000000000000..83077292c8732 --- /dev/null +++ b/apps/meteor/client/views/account/security/ResetPassphrase.tsx @@ -0,0 +1,22 @@ +import { Box, Button } from '@rocket.chat/fuselage'; +import { useTranslation } from 'react-i18next'; + +import { useResetE2EPasswordMutation } from '../../hooks/useResetE2EPasswordMutation'; + +export const ResetPassphrase = (): JSX.Element => { + const { t } = useTranslation(); + const resetE2EPassword = useResetE2EPasswordMutation(); + return ( + <> + + {t('Reset_E2EE_password')} + + + {t('Reset_E2EE_password_description')} + + + + ); +}; diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts index d018e4315ba35..ae2d2870086e7 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts @@ -121,7 +121,14 @@ test.describe('E2EE Passphrase Management - Initial Setup', () => { const e2EEKeyDecodeFailureBanner = new E2EEKeyDecodeFailureBanner(page); const sidenav = new HomeSidenav(page); - const newPassword = faker.string.uuid(); + const newPassword = faker.internet.password({ + length: 30, + prefix: + faker.string.alpha({ casing: 'lower' }) + + faker.string.alpha({ casing: 'upper' }) + + faker.string.numeric() + + faker.string.symbol(), + }); await setupE2EEPassword(page); diff --git a/packages/password-policies/src/PasswordPolicy.ts b/packages/password-policies/src/PasswordPolicy.ts index 8212df18002c2..d46c3740ebd34 100644 --- a/packages/password-policies/src/PasswordPolicy.ts +++ b/packages/password-policies/src/PasswordPolicy.ts @@ -1,16 +1,43 @@ import { PasswordPolicyError } from './PasswordPolicyError'; -type PasswordPolicyType = { - enabled: boolean; - policy: [name: string, options?: Record][]; +type PasswordPolicyMap = { + minLength: number; + maxLength: number; + forbidRepeatingCharacters: boolean; + forbidRepeatingCharactersCount: number; + mustContainAtLeastOneLowercase: boolean; + mustContainAtLeastOneUppercase: boolean; + mustContainAtLeastOneNumber: boolean; + mustContainAtLeastOneSpecialCharacter: boolean; }; -type ValidationMessageType = { - name: string; - isValid: boolean; - limit?: number; +type PasswordPolicyKey = keyof PasswordPolicyMap; +type PasswordPolicyName = `get-password-policy-${K}`; + +type PasswordPolicyParametersEntry = { + [K in PasswordPolicyKey]: PasswordPolicyMap[K] extends number + ? [PasswordPolicyName, Record] + : [PasswordPolicyName]; +}[PasswordPolicyKey]; + +type PasswordPolicyType = { + enabled: boolean; + policy: Entry[]; }; +export type PasswordPolicyOptions = Partial< + PasswordPolicyMap & { + enabled: boolean; + throwError: boolean; + } +>; + +export type PasswordPolicyValidation = { + [K in PasswordPolicyKey]: PasswordPolicyMap[K] extends number + ? { name: PasswordPolicyName; limit: number } + : { name: PasswordPolicyName }; +}[PasswordPolicyKey] & { isValid: boolean }; + export class PasswordPolicy { private regex: { forbiddingRepeatingCharacters: RegExp; @@ -51,7 +78,7 @@ export class PasswordPolicy { mustContainAtLeastOneNumber = false, mustContainAtLeastOneSpecialCharacter = false, throwError = true, - }) { + }: PasswordPolicyOptions) { this.enabled = enabled; this.minLength = minLength; this.maxLength = maxLength; @@ -87,12 +114,8 @@ export class PasswordPolicy { return false; } - sendValidationMessage(password: string): { - name: string; - isValid: boolean; - limit?: number; - }[] { - const validationReturn: ValidationMessageType[] = []; + sendValidationMessage(password: string): PasswordPolicyValidation[] { + const validationReturn: PasswordPolicyValidation[] = []; if (!this.enabled) { return []; @@ -223,7 +246,7 @@ export class PasswordPolicy { return true; } - getPasswordPolicy(): PasswordPolicyType { + getPasswordPolicy(): PasswordPolicyType<[name: string, params?: Record]> { const data: PasswordPolicyType = { enabled: false, policy: [], diff --git a/packages/password-policies/src/index.ts b/packages/password-policies/src/index.ts index fe91af266a939..8b629aad61333 100644 --- a/packages/password-policies/src/index.ts +++ b/packages/password-policies/src/index.ts @@ -1 +1 @@ -export { PasswordPolicy } from './PasswordPolicy'; +export { PasswordPolicy, type PasswordPolicyOptions, type PasswordPolicyValidation } from './PasswordPolicy'; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx index 829d6a8b61b78..844629f21a439 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx @@ -1,53 +1,12 @@ -import { Box } from '@rocket.chat/fuselage'; import { useVerifyPassword } from '@rocket.chat/ui-contexts'; -import { useId } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PasswordVerifierItem } from './PasswordVerifierItem'; +import { PasswordVerifierList, type PasswordVerifierListProps } from './PasswordVerifierList'; -type PasswordVerifierProps = { - password: string | undefined; - id?: string; - vertical?: boolean; +export type PasswordVerifierProps = Pick & { + password: string; }; -type PasswordVerificationProps = { - name: string; - isValid: boolean; - limit?: number; -}[]; - export const PasswordVerifier = ({ password, id, vertical }: PasswordVerifierProps) => { - const { t } = useTranslation(); - const uniqueId = useId(); - - const passwordVerifications: PasswordVerificationProps = useVerifyPassword(password || ''); - - if (!passwordVerifications?.length) { - return ; - } - - return ( - <> - - - - {t('Password_must_have')} - - - {passwordVerifications.map(({ isValid, limit, name }) => ( - - ))} - - - - ); + const { validations } = useVerifyPassword(password || ''); + return ; }; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx index c622fc74e6c86..8798fd436a49c 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx @@ -1,43 +1,49 @@ -import { Box, Icon } from '@rocket.chat/fuselage'; -import { AllHTMLAttributes, ComponentProps } from 'react'; +import { Box, Icon, type IconProps } from '@rocket.chat/fuselage'; +import type { PasswordPolicyValidation } from '@rocket.chat/ui-contexts'; +import { useId } from 'react'; +import { useTranslation, type UseTranslationResponse } from 'react-i18next'; -const variants: { - [key: string]: { - icon: ComponentProps['name']; - color: string; - }; -} = { - success: { - icon: 'success-circle', - color: 'status-font-on-success', - }, - error: { - icon: 'error-circle', - color: 'status-font-on-danger', - }, +type PasswordVerifierItemProps = PasswordPolicyValidation & { + vertical: boolean; }; -export const PasswordVerifierItem = ({ - text, - isValid, - vertical, - ...props -}: { text: string; isValid: boolean; vertical: boolean } & Omit, 'is'>) => { - const { icon, color } = variants[isValid ? 'success' : 'error']; +const getIconProps = ( + isValid: boolean, + t: UseTranslationResponse<'translation', undefined>['t'], +): Pick, 'name' | 'aria-label' | 'color'> => + isValid + ? { + 'name': 'success-circle', + 'aria-label': t('Success'), + 'color': 'status-font-on-success', + } + : { + 'name': 'error-circle', + 'aria-label': t('Error'), + 'color': 'status-font-on-danger', + }; + +export const PasswordVerifierItem = ({ isValid, ...props }: PasswordVerifierItemProps) => { + const { t } = useTranslation(); + const icon = getIconProps(isValid, t); + const id = useId(); + const iconId = `${id}-icon`; + const textId = `${id}-text`; + const requirementText = t(`${props.name}-label` as const, 'limit' in props ? { limit: props.limit } : undefined); return ( - - {text} + + {requirementText} ); }; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierList.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierList.tsx new file mode 100644 index 0000000000000..d77430dded56d --- /dev/null +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierList.tsx @@ -0,0 +1,39 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { PasswordPolicyValidation } from '@rocket.chat/ui-contexts'; +import { useId } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { PasswordVerifierItem } from './PasswordVerifierItem'; + +export type PasswordVerifierListProps = { + id?: string; + validations: PasswordPolicyValidation[]; + vertical?: boolean; +}; + +export const PasswordVerifierList = ({ id, validations, vertical = true }: PasswordVerifierListProps) => { + const { t } = useTranslation(); + const uniqueId = useId(); + + if (!validations?.length) { + return ; + } + + return ( + <> + + + + {t('Password_must_have')} + + + {validations.map((validation) => ( + + ))} + + + + ); +}; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx index dcc3fb040d9d0..ba91f360efbf5 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx @@ -40,7 +40,7 @@ it('should render policy list if its enabled and not empty', async () => { }); expect(screen.queryByRole('list')).toBeVisible(); - expect(screen.queryByRole('listitem', { name: 'get-password-policy-minLength-label' })).toBeVisible(); + expect(screen.queryByRole('listitem', { name: 'Success get-password-policy-minLength-label' })).toBeVisible(); }); it('should render all the policies when all policies are enabled', async () => { @@ -77,7 +77,10 @@ it("should render policy as invalid if password doesn't match the requirements", expect(screen.queryByTestId('password-verifier-skeleton')).toBeNull(); }); - expect(screen.getByRole('listitem', { name: 'get-password-policy-minLength-label' })).toHaveAttribute('aria-invalid', 'true'); + const item = screen.getByRole('listitem', { name: 'Error get-password-policy-minLength-label' }); + + expect(item.children[0]).toHaveAccessibleName('Error'); + expect(item.children[1]).toHaveTextContent('get-password-policy-minLength-label'); }); it('should render policy as valid if password matches the requirements', async () => { @@ -91,5 +94,9 @@ it('should render policy as valid if password matches the requirements', async ( await waitFor(() => { expect(screen.queryByTestId('password-verifier-skeleton')).toBeNull(); }); - expect(screen.getByRole('listitem', { name: 'get-password-policy-minLength-label' })).toHaveAttribute('aria-invalid', 'false'); + + const item = screen.getByRole('listitem', { name: 'Success get-password-policy-minLength-label' }); + + expect(item.children[0]).toHaveAccessibleName('Success'); + expect(item.children[1]).toHaveTextContent('get-password-policy-minLength-label'); }); diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 702dc0550b8b1..bc236af89a7c1 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -3,6 +3,7 @@ export * from './EmojiPicker'; export * from './ExternalLink'; export * from './DotLeader'; export * from './CustomFieldsForm'; +export { PasswordVerifierList } from './PasswordVerifier/PasswordVerifierList'; export * from './PasswordVerifier/PasswordVerifier'; export { default as TextSeparator } from './TextSeparator'; export * from './TooltipComponent'; diff --git a/packages/ui-client/src/hooks/useValidatePassword.ts b/packages/ui-client/src/hooks/useValidatePassword.ts index 9098d13009e1e..f56b9859879fb 100644 --- a/packages/ui-client/src/hooks/useValidatePassword.ts +++ b/packages/ui-client/src/hooks/useValidatePassword.ts @@ -1,14 +1,6 @@ import { useVerifyPassword } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -type passwordVerificationsType = { - name: string; - isValid: boolean; - limit?: number; -}[]; export const useValidatePassword = (password: string): boolean => { - const passwordVerifications: passwordVerificationsType = useVerifyPassword(password); - - return useMemo(() => passwordVerifications.every(({ isValid }) => isValid), [passwordVerifications]); + const passwordVerifications = useVerifyPassword(password); + return passwordVerifications.valid; }; diff --git a/packages/ui-contexts/src/hooks/usePasswordPolicy.ts b/packages/ui-contexts/src/hooks/usePasswordPolicy.ts new file mode 100644 index 0000000000000..a6c24ef02a25d --- /dev/null +++ b/packages/ui-contexts/src/hooks/usePasswordPolicy.ts @@ -0,0 +1,28 @@ +import { PasswordPolicy, type PasswordPolicyOptions, type PasswordPolicyValidation } from '@rocket.chat/password-policies'; +import { useMemo, useCallback } from 'react'; + +export type { PasswordPolicyValidation }; + +export type UsePasswordPolicyResult = { + validations: PasswordPolicyValidation[]; + valid: boolean; +}; + +export type UsePasswordPolicyReturn = (password: string) => UsePasswordPolicyResult; + +export type UsePasswordPolicy = (options: PasswordPolicyOptions) => UsePasswordPolicyReturn; + +export const usePasswordPolicy: UsePasswordPolicy = (options) => { + const policy = useMemo(() => new PasswordPolicy(options), [options]); + + return useCallback( + (password: string) => { + const validations = policy.sendValidationMessage(password); + return { + validations, + valid: validations.every(({ isValid }) => isValid), + }; + }, + [policy], + ); +}; diff --git a/packages/ui-contexts/src/hooks/useVerifyPassword.ts b/packages/ui-contexts/src/hooks/useVerifyPassword.ts index e010c1f678869..f854f227e7dac 100644 --- a/packages/ui-contexts/src/hooks/useVerifyPassword.ts +++ b/packages/ui-contexts/src/hooks/useVerifyPassword.ts @@ -1,11 +1,9 @@ -import { PasswordPolicy } from '@rocket.chat/password-policies'; import { useMemo } from 'react'; +import { usePasswordPolicy, type UsePasswordPolicyReturn } from './usePasswordPolicy'; import { useSetting } from './useSetting'; -type PasswordVerifications = { isValid: boolean; limit?: number; name: string }[]; - -export const useVerifyPassword = (password: string): PasswordVerifications => { +export const useVerifyPassword: UsePasswordPolicyReturn = (password) => { const enabled = useSetting('Accounts_Password_Policy_Enabled', false); const minLength = useSetting('Accounts_Password_Policy_MinLength', 7); const maxLength = useSetting('Accounts_Password_Policy_MaxLength', -1); @@ -16,32 +14,18 @@ export const useVerifyPassword = (password: string): PasswordVerifications => { const mustContainAtLeastOneNumber = useSetting('Accounts_Password_Policy_AtLeastOneNumber', true); const mustContainAtLeastOneSpecialCharacter = useSetting('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true); - const validator = useMemo( - () => - new PasswordPolicy({ - enabled, - minLength, - maxLength, - forbidRepeatingCharacters, - forbidRepeatingCharactersCount, - mustContainAtLeastOneLowercase, - mustContainAtLeastOneUppercase, - mustContainAtLeastOneNumber, - mustContainAtLeastOneSpecialCharacter, - throwError: true, - }), - [ - enabled, - minLength, - maxLength, - forbidRepeatingCharacters, - forbidRepeatingCharactersCount, - mustContainAtLeastOneLowercase, - mustContainAtLeastOneUppercase, - mustContainAtLeastOneNumber, - mustContainAtLeastOneSpecialCharacter, - ], - ); + const validate = usePasswordPolicy({ + enabled, + minLength, + maxLength, + forbidRepeatingCharacters, + forbidRepeatingCharactersCount, + mustContainAtLeastOneLowercase, + mustContainAtLeastOneUppercase, + mustContainAtLeastOneNumber, + mustContainAtLeastOneSpecialCharacter, + throwError: false, + }); - return useMemo(() => validator.sendValidationMessage(password || ''), [password, validator]); + return useMemo(() => validate(password || ''), [password, validate]); }; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index b30813e345a41..0c8071bdad788 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -85,6 +85,7 @@ export { useUserRoom } from './hooks/useUserRoom'; export { useUserSubscription } from './hooks/useUserSubscription'; export { useUserSubscriptionByName } from './hooks/useUserSubscriptionByName'; export { useUserSubscriptions } from './hooks/useUserSubscriptions'; +export { usePasswordPolicy, type PasswordPolicyValidation } from './hooks/usePasswordPolicy'; export { useVerifyPassword } from './hooks/useVerifyPassword'; export { useSelectedDevices } from './hooks/useSelectedDevices'; export { useDeviceConstraints } from './hooks/useDeviceConstraints';