Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: reset 2fa button [DHIS2-18831] (#1530) #1533

Merged
merged 1 commit into from
Mar 12, 2025
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
41 changes: 22 additions & 19 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -854,30 +854,15 @@ 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}}"

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"

Expand Down Expand Up @@ -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"

Expand Down
12 changes: 6 additions & 6 deletions src/pages/UserList/ContextMenu/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -48,7 +48,7 @@ const ContextMenu = ({ user, anchorRef, refetchUsers, onClose }) => {
const [CurrentModal, setCurrentModal] = useCurrentModal()
const {
access,
twoFactorEnabled,

userCredentials: { disabled },
} = user
const canReplicate =
Expand All @@ -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 (
Expand Down Expand Up @@ -132,13 +133,13 @@ const ContextMenu = ({ user, anchorRef, refetchUsers, onClose }) => {
dense
/>
)}
{access.update && twoFactorEnabled && (
{canReset2FA && (
<MenuItem
label={i18n.t(
'Disable Two Factor Authentication'
'Reset two factor authentication'
)}
icon={<IconLockOpen16 color={colors.grey600} />}
onClick={() => setCurrentModal(Disable2FaModal)}
onClick={() => setCurrentModal(ResetTwoFAModal)}
dense
/>
)}
Expand Down Expand Up @@ -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,
Expand Down
99 changes: 99 additions & 0 deletions src/pages/UserList/ContextMenu/ContextMenu.test.js
Original file line number Diff line number Diff line change
@@ -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(() => <p>Replicate modal</p>)
)

const DEFAULT_USER = {
access: {
delete: false,
read: true,
update: true,
},
disabled: false,
id: 'userABC1234',
email: '[email protected]',
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(<ContextMenu {...DEFAULT_PROPS} />)
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(<ContextMenu {...DEFAULT_PROPS} user={modifiedUser} />)
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(<ContextMenu {...DEFAULT_PROPS} user={modifiedUser} />)
expect(
screen.queryByText('Reset two factor authentication')
).not.toBeInTheDocument()
})

it('shows explanation about resetting 2FA when Reset 2FA option is clicked', async () => {
render(<ContextMenu {...DEFAULT_PROPS} />)

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(<ContextMenu {...DEFAULT_PROPS} />)
// 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')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,32 @@ 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({
resource: `users/${user.id}/twoFA/disabled`,
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: '-:-',
Expand All @@ -47,45 +47,39 @@ const Disable2FaModal = ({ user, refetchUsers, onClose }) => {
}

return (
<Modal>
<ModalTitle>
{i18n.t(
'Disable two factor authentication for user {{- name}}',
{
name: user.displayName,
}
)}
</ModalTitle>
<Modal small>
<ModalTitle>{i18n.t('Reset two factor authentication')}</ModalTitle>
<ModalContent>
{i18n.t(
`Are you sure you want to disable two factor authentication for {{- name}}?`,
{
name: user.displayName,
}
)}
<div>
{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 }
)}
</div>
<div className={styles.secondary2FAModalText}>
{i18n.t(
`Are you sure you want to reset two factor authentication for {{- name}}?`,
{ name: user.displayName }
)}
</div>
</ModalContent>
<ModalActions>
<ButtonStrip end>
<Button secondary onClick={onClose} disabled={loading}>
{i18n.t('No, cancel')}
</Button>
<Button
primary
loading={loading}
onClick={handleDisable2Fa}
>
{i18n.t('Yes, disable two factor authentication')}
<Button primary loading={loading} onClick={handleReset2FA}>
{i18n.t('Yes, reset')}
</Button>
</ButtonStrip>
</ModalActions>
</Modal>
)
}

Disable2FaModal.propTypes = {
refetchUsers: PropTypes.func.isRequired,
ResetTwoFAModal.propTypes = {
user: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
}

export default Disable2FaModal
export default ResetTwoFAModal
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.secondary2FAModalText {
margin-block: 8px;
}
Loading