Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/purple-sheep-bathe.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Meteor.methods<ServerMethods>({
}

if (!(await resetUserE2EEncriptionKey(userId, false))) {
return false;
throw new Meteor.Error('failed-reset-e2e-password', 'Failed to reset E2E password', {
method: 'resetOwnE2EKey',
});
}
return true;
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ const AccountSecurityPage = (): ReactElement => {
{allowPasswordChange && (
<FormProvider {...methods}>
<Accordion>
<AccordionItem title={t('Password')} expanded={!require2faSetup}>
<AccordionItem title={t('Password')} defaultExpanded={!require2faSetup}>
<ChangePassword id={passwordFormId} />
</AccordionItem>
</Accordion>
</FormProvider>
)}
<Accordion>
{(twoFactorTOTP || showEmailTwoFactor) && twoFactorEnabled && (
<AccordionItem expanded={require2faSetup} title={t('Two Factor Authentication')}>
<AccordionItem defaultExpanded={require2faSetup} title={t('Two Factor Authentication')}>
{require2faSetup && (
<Callout type='warning' title={t('Enable_two-factor_authentication')} mbe='24px'>
{t('Enable_two-factor_authentication_callout_description')}
Expand Down
22 changes: 5 additions & 17 deletions apps/meteor/client/views/account/security/EndToEnd.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Box>): 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,
Expand Down Expand Up @@ -48,18 +48,6 @@ const EndToEnd = (props: ComponentProps<typeof Box>): 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');
Expand Down Expand Up @@ -161,7 +149,7 @@ const EndToEnd = (props: ComponentProps<typeof Box>): ReactElement => {
{t('Reset_E2E_Key')}
</Box>
<Box is='p' fontScale='p1' mbe={12} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_Reset_Key_Explanation')) }} />
<Button onClick={handleResetE2eKey} data-qa-type='e2e-encryption-reset-key-button'>
<Button onClick={() => resetE2EPassword.mutate()} data-qa-type='e2e-encryption-reset-key-button'>
{t('Reset_E2E_Key')}
</Button>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand All @@ -30,6 +35,22 @@ const EnterE2EPasswordModal = ({ onConfirm, onClose, onCancel }: EnterE2EPasswor
setFocus('password');
}, [setFocus]);

if (confirmResetPassword) {
return (
<GenericModal
variant='warning'
title={t('Reset_E2EE_password')}
icon='warning'
confirmText={t('Reset_E2EE_password')}
onClose={onClose}
onCancel={onClose}
onConfirm={() => resetE2EPassword.mutate()}
>
<Box is='p'>{t('Reset_E2EE_password_description')}</Box>
</GenericModal>
);
}

return (
<GenericModal
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit(({ password }) => onConfirm(password))} {...props} />}
Expand Down Expand Up @@ -66,6 +87,18 @@ const EnterE2EPasswordModal = ({ onConfirm, onClose, onCancel }: EnterE2EPasswor
{errors.password.message}
</FieldError>
)}
<FieldRow alignSelf='end'>
<FieldLink
href='#'
target={undefined}
onClick={(e) => {
e.preventDefault();
setConfirmResetPassword(true);
}}
>
{t('Forgot_E2EE_Password')}
</FieldLink>
</FieldRow>
</Field>
</FieldGroup>
</GenericModal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ exports[`renders Default without crashing 1`] = `
</span>
</label>
</span>
<span
class="rcx-box rcx-box--full rcx-field__row rcx-css-idbaye"
>
<a
class="rcx-box rcx-box--full rcx-field__link"
href="#"
>
Forgot_E2EE_Password
</a>
</span>
</div>
</fieldset>
</div>
Expand Down
24 changes: 24 additions & 0 deletions apps/meteor/client/views/hooks/useResetE2EPasswordMutation.ts
Original file line number Diff line number Diff line change
@@ -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,
});
};
17 changes: 17 additions & 0 deletions apps/meteor/tests/e2e/e2e-encryption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
E2EEKeyDecodeFailureBanner,
EnterE2EEPasswordBanner,
EnterE2EEPasswordModal,
ResetE2EEPasswordModal,
SaveE2EEPasswordBanner,
SaveE2EEPasswordModal,
} from './page-objects/fragments/e2ee';
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/tests/e2e/page-objects/home-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Loading