From 596b44e7f35aed5ee60df3db62b0d5853633c9e4 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Mon, 24 Feb 2025 10:44:25 +0100 Subject: [PATCH] fix: reset 2fa button [DHIS2-18831] (#1530) --- i18n/en.pot | 41 ++++---- src/pages/UserList/ContextMenu/ContextMenu.js | 12 +-- .../UserList/ContextMenu/ContextMenu.test.js | 99 +++++++++++++++++++ ...{Disable2FaModal.js => ResetTwoFAModal.js} | 52 +++++----- .../Modals/ResetTwoFAModal.module.css | 3 + 5 files changed, 153 insertions(+), 54 deletions(-) create mode 100644 src/pages/UserList/ContextMenu/ContextMenu.test.js rename src/pages/UserList/ContextMenu/Modals/{Disable2FaModal.js => ResetTwoFAModal.js} (56%) create mode 100644 src/pages/UserList/ContextMenu/Modals/ResetTwoFAModal.module.css diff --git a/i18n/en.pot b/i18n/en.pot index 446d226d..be67d92f 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-03-27T09:03:25.496Z\n" -"PO-Revision-Date: 2024-03-27T09:03:25.496Z\n" +"POT-Creation-Date: 2025-02-21T10:16:54.865Z\n" +"PO-Revision-Date: 2025-02-21T10:16:54.865Z\n" msgid "Yes" msgstr "Yes" @@ -854,8 +854,8 @@ msgstr "Disable" msgid "Enable" msgstr "Enable" -msgid "Disable Two Factor Authentication" -msgstr "Disable Two Factor Authentication" +msgid "Reset two factor authentication" +msgstr "Reset two factor authentication" msgid "There was an error deleting the user: {{- error}}" msgstr "There was an error deleting the user: {{- error}}" @@ -863,21 +863,6 @@ msgstr "There was an error deleting the user: {{- error}}" msgid "Delete user {{- name}}" msgstr "Delete user {{- name}}" -msgid "Disabled two factor authentication for \"{{- name}}\" successfuly" -msgstr "Disabled two factor authentication for \"{{- name}}\" successfuly" - -msgid "There was an error disabling two factor authentication: {{- error}}" -msgstr "There was an error disabling two factor authentication: {{- error}}" - -msgid "Disable two factor authentication for user {{- name}}" -msgstr "Disable two factor authentication for user {{- name}}" - -msgid "Are you sure you want to disable two factor authentication for {{- name}}?" -msgstr "Are you sure you want to disable two factor authentication for {{- name}}?" - -msgid "Yes, disable two factor authentication" -msgstr "Yes, disable two factor authentication" - msgid "User \"{{- name}}\" disabled successfuly" msgstr "User \"{{- name}}\" disabled successfuly" @@ -938,6 +923,24 @@ msgstr "Are you sure you want to reset the password for {{- name}}?" msgid "Yes, reset" msgstr "Yes, reset" +msgid "Two factor authentication for \"{{- name}}\" has reset successfully" +msgstr "Two factor authentication for \"{{- name}}\" has reset successfully" + +msgid "There was an error resetting the two factor authentication: {{- error}}" +msgstr "There was an error resetting the two factor authentication: {{- error}}" + +msgid "" +"If {{- name}} has two factor authentication enabled, resetting the two " +"factor authentication will make it possible for them to log in without " +"providing a two factor authentication code." +msgstr "" +"If {{- name}} has two factor authentication enabled, resetting the two " +"factor authentication will make it possible for them to log in without " +"providing a two factor authentication code." + +msgid "Are you sure you want to reset two factor authentication for {{- name}}?" +msgstr "Are you sure you want to reset two factor authentication for {{- name}}?" + msgid "1 month" msgstr "1 month" diff --git a/src/pages/UserList/ContextMenu/ContextMenu.js b/src/pages/UserList/ContextMenu/ContextMenu.js index 6021695c..732a8391 100644 --- a/src/pages/UserList/ContextMenu/ContextMenu.js +++ b/src/pages/UserList/ContextMenu/ContextMenu.js @@ -21,11 +21,11 @@ import { useCurrentUser } from '../../../hooks/useCurrentUser.js' import { useReferrerInfo } from '../../../providers/useReferrer.js' import navigateTo from '../../../utils/navigateTo.js' import DeleteModal from './Modals/DeleteModal.js' -import Disable2FaModal from './Modals/Disable2FaModal.js' import DisableModal from './Modals/DisableModal.js' import EnableModal from './Modals/EnableModal.js' import ReplicateModal from './Modals/ReplicateModal.js' import ResetPasswordModal from './Modals/ResetPasswordModal.js' +import ResetTwoFAModal from './Modals/ResetTwoFAModal.js' const useCurrentModal = () => { const [CurrentModal, setCurrentModal] = useState() @@ -48,7 +48,7 @@ const ContextMenu = ({ user, anchorRef, refetchUsers, onClose }) => { const [CurrentModal, setCurrentModal] = useCurrentModal() const { access, - twoFactorEnabled, + userCredentials: { disabled }, } = user const canReplicate = @@ -68,6 +68,7 @@ const ContextMenu = ({ user, anchorRef, refetchUsers, onClose }) => { ) const canDisable = currentUser.id !== user.id && access.update && !disabled const canDelete = currentUser.id !== user.id && access.delete + const canReset2FA = currentUser.id !== user.id && access.update const { setReferrer } = useReferrerInfo() return ( @@ -132,13 +133,13 @@ const ContextMenu = ({ user, anchorRef, refetchUsers, onClose }) => { dense /> )} - {access.update && twoFactorEnabled && ( + {canReset2FA && ( } - onClick={() => setCurrentModal(Disable2FaModal)} + onClick={() => setCurrentModal(ResetTwoFAModal)} dense /> )} @@ -178,7 +179,6 @@ ContextMenu.propTypes = { update: PropTypes.bool.isRequired, }).isRequired, id: PropTypes.string.isRequired, - twoFactorEnabled: PropTypes.bool.isRequired, userCredentials: PropTypes.shape({ disabled: PropTypes.bool.isRequired, }).isRequired, diff --git a/src/pages/UserList/ContextMenu/ContextMenu.test.js b/src/pages/UserList/ContextMenu/ContextMenu.test.js new file mode 100644 index 00000000..6ab9537d --- /dev/null +++ b/src/pages/UserList/ContextMenu/ContextMenu.test.js @@ -0,0 +1,99 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import React from 'react' +import ContextMenu from './ContextMenu.js' + +jest.mock('@dhis2/app-runtime', () => ({ + ...jest.requireActual('@dhis2/app-runtime'), + useConfig: jest.fn(() => ({ systemInfo: { emailConfigured: true } })), +})) + +jest.mock('../../../hooks/useCurrentUser.js', () => ({ + useCurrentUser: jest.fn(() => ({ id: 'anotherID01', authorities: [] })), +})) + +jest.mock('./Modals/ReplicateModal.js', () => + jest.fn(() =>

Replicate modal

) +) + +const DEFAULT_USER = { + access: { + delete: false, + read: true, + update: true, + }, + disabled: false, + id: 'userABC1234', + email: 'dhis2user@dhis2.org', + displayName: 'Nils Holgersson', + userCredentials: { + disabled: false, + }, +} + +const DEFAULT_PROPS = { + anchorRef: {}, + refetchUsers: jest.fn(), + onClose: jest.fn(), + user: DEFAULT_USER, +} + +describe('Context Menu', () => { + it('should show two factor reset if admin has update access to user object', () => { + render() + expect( + screen.getByText('Reset two factor authentication') + ).toBeInTheDocument() + }) + + it('should not show two factor reset if admin does not have update access to user object', () => { + const modifiedUser = { + ...DEFAULT_USER, + access: { ...DEFAULT_USER.access, update: false }, + } + render() + expect( + screen.queryByText('Reset two factor authentication') + ).not.toBeInTheDocument() + }) + + it('should not show two factor reset if admin is modifying themself', () => { + const modifiedUser = { ...DEFAULT_USER, id: 'anotherID01' } + render() + expect( + screen.queryByText('Reset two factor authentication') + ).not.toBeInTheDocument() + }) + + it('shows explanation about resetting 2FA when Reset 2FA option is clicked', async () => { + render() + + const resetTwoFAOption = screen.getByText( + 'Reset two factor authentication' + ) + await waitFor(() => { + userEvent.click(resetTwoFAOption) + }) + const explanationText = screen.getByText( + 'If Nils Holgersson has two factor authentication enabled, resetting the two factor authentication will make it possible for them to log in without providing a two factor authentication code.' + ) + expect(explanationText).toBeInTheDocument() + }) + + // mocking with the Provider is not working well with v40 code, so skipping this test + it.skip('calls disable mutation when admin clicks through modal to reset 2FA', async () => { + // render() + // const resetTwoFAOption = screen.getByText( + // 'Reset two factor authentication' + // ) + // await waitFor(() => { + // userEvent.click(resetTwoFAOption) + // }) + // const resetTwoFAConfirm = screen.getByText('Yes, reset') + // await waitFor(() => { + // userEvent.click(resetTwoFAConfirm) + // }) + // expect(mockReset2FA).toHaveBeenCalledTimes(1) + // expect(mockReset2FA).toHaveBeenCalledWith('create') + }) +}) diff --git a/src/pages/UserList/ContextMenu/Modals/Disable2FaModal.js b/src/pages/UserList/ContextMenu/Modals/ResetTwoFAModal.js similarity index 56% rename from src/pages/UserList/ContextMenu/Modals/Disable2FaModal.js rename to src/pages/UserList/ContextMenu/Modals/ResetTwoFAModal.js index 3fa60f9e..8c0cd876 100644 --- a/src/pages/UserList/ContextMenu/Modals/Disable2FaModal.js +++ b/src/pages/UserList/ContextMenu/Modals/ResetTwoFAModal.js @@ -11,13 +11,14 @@ import { import PropTypes from 'prop-types' import React, { useState } from 'react' import { useFetchAlert } from '../../../../hooks/useFetchAlert.js' +import styles from './ResetTwoFAModal.module.css' -const Disable2FaModal = ({ user, refetchUsers, onClose }) => { +const ResetTwoFAModal = ({ user, onClose }) => { const engine = useDataEngine() const [loading, setLoading] = useState(false) const { showSuccess, showError } = useFetchAlert() - const handleDisable2Fa = async () => { + const handleReset2FA = async () => { setLoading(true) try { await engine.mutate({ @@ -25,18 +26,17 @@ const Disable2FaModal = ({ user, refetchUsers, onClose }) => { type: 'create', }) const message = i18n.t( - 'Disabled two factor authentication for "{{- name}}" successfuly', + 'Two factor authentication for "{{- name}}" has reset successfully', { name: user.displayName, } ) showSuccess(message) - refetchUsers() onClose() } catch (error) { setLoading(false) const message = i18n.t( - 'There was an error disabling two factor authentication: {{- error}}', + 'There was an error resetting the two factor authentication: {{- error}}', { error: error.message, nsSeparator: '-:-', @@ -47,34 +47,29 @@ const Disable2FaModal = ({ user, refetchUsers, onClose }) => { } return ( - - - {i18n.t( - 'Disable two factor authentication for user {{- name}}', - { - name: user.displayName, - } - )} - + + {i18n.t('Reset two factor authentication')} - {i18n.t( - `Are you sure you want to disable two factor authentication for {{- name}}?`, - { - name: user.displayName, - } - )} +
+ {i18n.t( + `If {{- name}} has two factor authentication enabled, resetting the two factor authentication will make it possible for them to log in without providing a two factor authentication code.`, + { name: user.displayName } + )} +
+
+ {i18n.t( + `Are you sure you want to reset two factor authentication for {{- name}}?`, + { name: user.displayName } + )} +
- @@ -82,10 +77,9 @@ const Disable2FaModal = ({ user, refetchUsers, onClose }) => { ) } -Disable2FaModal.propTypes = { - refetchUsers: PropTypes.func.isRequired, +ResetTwoFAModal.propTypes = { user: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, } -export default Disable2FaModal +export default ResetTwoFAModal diff --git a/src/pages/UserList/ContextMenu/Modals/ResetTwoFAModal.module.css b/src/pages/UserList/ContextMenu/Modals/ResetTwoFAModal.module.css new file mode 100644 index 00000000..8aa792b3 --- /dev/null +++ b/src/pages/UserList/ContextMenu/Modals/ResetTwoFAModal.module.css @@ -0,0 +1,3 @@ +.secondary2FAModalText { + margin-block: 8px; +}