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_Policy_Aria_Description')}
-
-
-
- {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_Policy_Aria_Description')}
+
+
+
+ {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';