From e9f037a974374bb8b64bd50bd3d7334c9ade401f Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 1 Sep 2025 15:53:57 -0300 Subject: [PATCH 1/5] feat: create link to open modal --- .../app/e2e/server/methods/resetOwnE2EKey.ts | 4 +- .../account/security/AccountSecurityPage.tsx | 4 +- .../views/account/security/EndToEnd.tsx | 22 +++-------- .../EnterE2EPasswordModal.tsx | 37 ++++++++++++++++++- .../hooks/useResetE2EPasswordMutation.ts | 24 ++++++++++++ packages/i18n/src/locales/en.i18n.json | 3 ++ 6 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 apps/meteor/client/views/hooks/useResetE2EPasswordMutation.ts diff --git a/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts b/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts index b1d40e48bb5e7..eccabc20b356e 100644 --- a/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts +++ b/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts @@ -22,7 +22,9 @@ Meteor.methods({ } if (!(await resetUserE2EEncriptionKey(userId, false))) { - return false; + throw new Meteor.Error('failed-reset-e2e-password', 'Failed to reset E2E password', { + method: 'resetOwnE2EKey', + }); } return true; }), diff --git a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx index 3033161d1aca5..53f4237ff19b2 100644 --- a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx +++ b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx @@ -49,7 +49,7 @@ const AccountSecurityPage = (): ReactElement => { {allowPasswordChange && ( - + @@ -57,7 +57,7 @@ const AccountSecurityPage = (): ReactElement => { )} {(twoFactorTOTP || showEmailTwoFactor) && twoFactorEnabled && ( - + {require2faSetup && ( {t('Enable_two-factor_authentication_callout_description')} diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index e36ea8076bec6..d8b8a46543563 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -1,22 +1,22 @@ import { Box, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button, Divider } from '@rocket.chat/fuselage'; -import { useToastMessageDispatch, useMethod, useTranslation, useLogout } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import DOMPurify from 'dompurify'; import { Accounts } from 'meteor/accounts-base'; import type { ComponentProps, ReactElement } from 'react'; -import { useId, useCallback, useEffect } from 'react'; +import { useId, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { e2e } from '../../../lib/e2ee/rocketchat.e2e'; +import { useResetE2EPasswordMutation } from '../../hooks/useResetE2EPasswordMutation'; const EndToEnd = (props: ComponentProps): ReactElement => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const logout = useLogout(); const publicKey = Accounts.storageLocation.getItem('public_key'); const privateKey = Accounts.storageLocation.getItem('private_key'); - const resetE2eKey = useMethod('e2e.resetOwnE2EKey'); + const resetE2EPassword = useResetE2EPasswordMutation(); const { handleSubmit, @@ -48,18 +48,6 @@ const EndToEnd = (props: ComponentProps): ReactElement => { } }; - const handleResetE2eKey = useCallback(async () => { - try { - const result = await resetE2eKey(); - if (result) { - dispatchToastMessage({ type: 'success', message: t('User_e2e_key_was_reset') }); - logout(); - } - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }, [dispatchToastMessage, resetE2eKey, logout, t]); - useEffect(() => { if (password?.trim() === '') { resetField('passwordConfirm'); @@ -161,7 +149,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { {t('Reset_E2E_Key')} - diff --git a/apps/meteor/client/views/e2e/EnterE2EPasswordModal/EnterE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/EnterE2EPasswordModal/EnterE2EPasswordModal.tsx index 68199d9545824..534827a6b9e80 100644 --- a/apps/meteor/client/views/e2e/EnterE2EPasswordModal/EnterE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/EnterE2EPasswordModal/EnterE2EPasswordModal.tsx @@ -1,10 +1,12 @@ -import { Box, PasswordInput, Field, FieldGroup, FieldRow, FieldError } from '@rocket.chat/fuselage'; +import { Box, PasswordInput, Field, FieldGroup, FieldRow, FieldError, FieldLink } from '@rocket.chat/fuselage'; import { GenericModal } from '@rocket.chat/ui-client'; import DOMPurify from 'dompurify'; -import { useEffect, useId } from 'react'; +import { useEffect, useId, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { useResetE2EPasswordMutation } from '../../hooks/useResetE2EPasswordMutation'; + type EnterE2EPasswordModalProps = { onConfirm: (password: string) => void; onClose: () => void; @@ -13,6 +15,9 @@ type EnterE2EPasswordModalProps = { const EnterE2EPasswordModal = ({ onConfirm, onClose, onCancel }: EnterE2EPasswordModalProps) => { const { t } = useTranslation(); + const [confirmResetPassword, setConfirmResetPassword] = useState(false); + const resetE2EPassword = useResetE2EPasswordMutation({ options: { onSettled: () => onClose() } }); + const { handleSubmit, control, @@ -30,6 +35,22 @@ const EnterE2EPasswordModal = ({ onConfirm, onClose, onCancel }: EnterE2EPasswor setFocus('password'); }, [setFocus]); + if (confirmResetPassword) { + return ( + resetE2EPassword.mutate()} + > + {t('Reset_E2EE_password_description')} + + ); + } + return ( onConfirm(password))} {...props} />} @@ -66,6 +87,18 @@ const EnterE2EPasswordModal = ({ onConfirm, onClose, onCancel }: EnterE2EPasswor {errors.password.message} )} + + { + e.preventDefault(); + setConfirmResetPassword(true); + }} + > + {t('Forgot_E2EE_Password')} + + diff --git a/apps/meteor/client/views/hooks/useResetE2EPasswordMutation.ts b/apps/meteor/client/views/hooks/useResetE2EPasswordMutation.ts new file mode 100644 index 0000000000000..652c4e8b02143 --- /dev/null +++ b/apps/meteor/client/views/hooks/useResetE2EPasswordMutation.ts @@ -0,0 +1,24 @@ +import { useLogout, useMethod, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { MutationOptions } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +export const useResetE2EPasswordMutation = ({ options }: { options?: MutationOptions } = {}) => { + const { t } = useTranslation(); + + const logout = useLogout(); + const resetE2eKey = useMethod('e2e.resetOwnE2EKey'); + const dispatchToastMessage = useToastMessageDispatch(); + + return useMutation({ + mutationFn: async () => resetE2eKey(), + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('User_e2e_key_was_reset') }); + logout(); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + ...options, + }); +}; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8c2f9ca1bf33b..58913e90905b4 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2292,6 +2292,7 @@ "Forgot_Password_Email_Subject": "[Site_Name] - Password Recovery", "Forgot_password": "Forgot your password?", "Forgot_password_section": "Forgot password", + "Forgot_E2EE_Password": "Forgot E2EE password?", "Format": "Format", "Forward": "Forward", "Forward_chat": "Forward chat", @@ -4329,6 +4330,8 @@ "Reset": "Reset", "Reset_Connection": "Reset Connection", "Reset_E2E_Key": "Reset E2EE key", + "Reset_E2EE_password": "Reset E2EE password", + "Reset_E2EE_password_description": "Resetting will log you out and generate a new E2EE password upon logging back in. You‘ll regain access to encrypted rooms with online members, but not to those without any members online.", "Reset_TOTP": "Reset TOTP", "Reset_password": "Reset password", "Reset_priorities": "Reset priorities", From 0199e3e91c3541886f28f44fc47e03a9a56d00c4 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 1 Sep 2025 16:36:53 -0300 Subject: [PATCH 2/5] test: update snapshot test --- .../__snapshots__/EnterE2EPasswordModal.spec.tsx.snap | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/meteor/client/views/e2e/EnterE2EPasswordModal/__snapshots__/EnterE2EPasswordModal.spec.tsx.snap b/apps/meteor/client/views/e2e/EnterE2EPasswordModal/__snapshots__/EnterE2EPasswordModal.spec.tsx.snap index 78af4fcea26c4..2158a8003700e 100644 --- a/apps/meteor/client/views/e2e/EnterE2EPasswordModal/__snapshots__/EnterE2EPasswordModal.spec.tsx.snap +++ b/apps/meteor/client/views/e2e/EnterE2EPasswordModal/__snapshots__/EnterE2EPasswordModal.spec.tsx.snap @@ -98,6 +98,16 @@ exports[`renders Default without crashing 1`] = ` + + + Forgot_E2EE_Password + + From 65f877958e3b84ce28c11fcae1db7b65e712604d Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 1 Sep 2025 17:01:27 -0300 Subject: [PATCH 3/5] test: add e2e test --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 17 +++++++++++++ .../tests/e2e/page-objects/fragments/e2ee.ts | 24 +++++++++++++++++++ .../tests/e2e/page-objects/home-channel.ts | 4 ++++ 3 files changed, 45 insertions(+) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index fff4b14801e54..b0d76a12cb530 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -13,6 +13,7 @@ import { E2EEKeyDecodeFailureBanner, EnterE2EEPasswordBanner, EnterE2EEPasswordModal, + ResetE2EEPasswordModal, SaveE2EEPasswordBanner, SaveE2EEPasswordModal, } from './page-objects/fragments/e2ee'; @@ -92,6 +93,22 @@ test.describe('initial setup', () => { await loginPage.loginByUserState(Users.admin); }); + test('should reset e2e password from the modal', async ({ page }) => { + const sidenav = new HomeSidenav(page); + const loginPage = new LoginPage(page); + const enterE2EEPasswordBanner = new EnterE2EEPasswordBanner(page); + const enterE2EEPasswordModal = new EnterE2EEPasswordModal(page); + const resetE2EEPasswordModal = new ResetE2EEPasswordModal(page); + + await sidenav.logout(); + await loginPage.loginByUserState(Users.admin); + await enterE2EEPasswordBanner.click(); + await enterE2EEPasswordModal.forgotPassword(); + await resetE2EEPasswordModal.confirmReset(); + + await loginPage.loginByUserState(Users.admin); + }); + test('expect to manually set a new password', async ({ page }) => { const accountSecurityPage = new AccountSecurityPage(page); const loginPage = new LoginPage(page); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts b/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts index c5ebd1ce1409e..6fdaf3adda099 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts @@ -75,6 +75,10 @@ export class EnterE2EEPasswordModal extends Modal { return this.root.getByPlaceholder('Please enter your E2EE password'); } + private get forgotPasswordLink() { + return this.root.getByRole('link', { name: 'Forgot E2EE password?' }); + } + private get enterE2EEPasswordButton() { return this.root.getByRole('button', { name: 'Enable encryption' }); } @@ -84,6 +88,26 @@ export class EnterE2EEPasswordModal extends Modal { await this.enterE2EEPasswordButton.click(); await this.waitForDismissal(); } + + async forgotPassword() { + await this.forgotPasswordLink.click(); + await this.waitForDismissal(); + } +} + +export class ResetE2EEPasswordModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Reset E2EE password' })); + } + + private get resetE2EEPasswordButton() { + return this.root.getByRole('button', { name: 'Reset E2EE password' }); + } + + async confirmReset() { + await this.resetE2EEPasswordButton.click(); + await this.waitForDismissal(); + } } export class EnableRoomEncryptionModal extends Modal { diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index e89bfd072459a..32b024b08d7d5 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -27,6 +27,10 @@ export class HomeChannel { this.tabs = new HomeFlextab(page); } + goto() { + return this.page.goto('/home'); + } + get toastSuccess(): Locator { return this.page.locator('.rcx-toastbar.rcx-toastbar--success'); } From 755a7adc2d27b75c9f007c78dd75b1b818873914 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 1 Sep 2025 17:05:04 -0300 Subject: [PATCH 4/5] chore: changeset --- .changeset/purple-sheep-bathe.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/purple-sheep-bathe.md diff --git a/.changeset/purple-sheep-bathe.md b/.changeset/purple-sheep-bathe.md new file mode 100644 index 0000000000000..4ebcedc841b75 --- /dev/null +++ b/.changeset/purple-sheep-bathe.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduces the ability to reset the e2e encrypted password from the enter e2e encrypted password modal From c36eeb36a8759805a1d0541a117b137f8bc462bf Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 2 Sep 2025 15:33:24 -0300 Subject: [PATCH 5/5] fix: review --- apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts b/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts index eccabc20b356e..13e59e0286554 100644 --- a/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts +++ b/apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts @@ -1,3 +1,4 @@ +import { MeteorError } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; @@ -16,13 +17,13 @@ Meteor.methods({ const userId = Meteor.userId(); if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { + throw new MeteorError('error-invalid-user', 'Invalid user', { method: 'resetOwnE2EKey', }); } if (!(await resetUserE2EEncriptionKey(userId, false))) { - throw new Meteor.Error('failed-reset-e2e-password', 'Failed to reset E2E password', { + throw new MeteorError('failed-reset-e2e-password', 'Failed to reset E2E password', { method: 'resetOwnE2EKey', }); }