Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions apps/meteor/app/e2e/server/methods/resetOwnE2EKey.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MeteorError } from '@rocket.chat/core-services';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';

Expand All @@ -16,13 +17,15 @@ Meteor.methods<ServerMethods>({
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))) {
return false;
throw new MeteorError('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 @@ -2296,6 +2296,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 @@ -4336,6 +4337,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