MM-66925 - improve user email and password modals (#34739)

* MM-66925 - improve user email and password  modals

* adjust error modal styling

* adjust e2e tests
This commit is contained in:
Pablo Vélez 2025-12-19 15:52:59 +01:00 committed by GitHub
parent a17bb19844
commit 1a21d34aab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 567 additions and 947 deletions

View file

@ -134,9 +134,9 @@ describe('Guest Account - Guest User Invitation Flow', () => {
cy.findByText('Update email').should('be.visible').click();
// * Update email outside whitelisted domain and verify error message
cy.findByTestId('resetEmailModal').should('be.visible').within(() => {
cy.findByTestId('resetEmailForm').should('be.visible').get('input').type(email);
cy.findByTestId('resetEmailButton').click();
cy.get('#resetEmailModal').should('be.visible').within(() => {
cy.get('input[type="email"]').type(email);
cy.get('button.btn-primary.confirm').click();
cy.get('.error').should('be.visible').and('have.text', 'The email you provided does not belong to an accepted domain for guest accounts. Please contact your administrator or sign up with a different email.');
cy.get('.close').click();
});

View file

@ -85,9 +85,9 @@ describe('Guest Account - Verify Manage Guest Users', () => {
// * Update email of Guest User
const email = `temp-${getRandomId()}@mattermost.com`;
cy.findByTestId('resetEmailModal').should('be.visible').within(() => {
cy.findByTestId('resetEmailForm').should('be.visible').get('input').type(email);
cy.findByTestId('resetEmailButton').click();
cy.get('#resetEmailModal').should('be.visible').within(() => {
cy.get('input[type="email"]').type(email);
cy.get('button.btn-primary.confirm').click();
});
// * Verify if Guest's email was updated

View file

@ -78,7 +78,7 @@ describe('System Console > User Management > Users', () => {
// # Type new password and submit.
cy.get('input[type=password]').type('new' + testUser.password);
cy.get('button[type=submit]').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
// # Log out.
cy.apiLogout();
@ -137,10 +137,10 @@ describe('System Console > User Management > Users', () => {
cy.get('input[type=password]').eq(1).type('new' + otherAdmin.password);
// # Click the 'Reset' button.
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
// * Verify the appropriate error is returned.
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
// * Verify the appropriate error is returned (current password error shows in modal header area).
cy.get('.genericModalError .error').should('be.visible').
and('contain', 'The "Current Password" you entered is incorrect. Please check that Caps Lock is off and try again.');
});
@ -160,11 +160,10 @@ describe('System Console > User Management > Users', () => {
cy.get('input[type=password]').eq(1).type('new');
// # Click the 'Reset' button.
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
// * Verify the appropriate error is returned.
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
and('contain', 'Your password must be 5-72 characters long.');
// * Verify the appropriate error is returned (new password error shows under the input).
cy.get('.Input___error').should('be.visible').and('contain', 'characters long');
});
it('MM-T936 Users - System admin changes own password - Blank fields', () => {
@ -179,21 +178,20 @@ describe('System Console > User Management > Users', () => {
cy.findByText('Reset password').click();
// # Click the 'Reset' button.
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
// * Verify the appropriate error is returned.
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
// * Verify the appropriate error is returned (current password missing).
cy.get('.genericModalError .error').should('be.visible').
and('contain', 'Please enter your current password.');
// # Type current password, leave new password blank.
cy.get('input[type=password]').eq(0).type(otherAdmin.password);
// # Click the 'Reset' button.
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
// * Verify the appropriate error is returned.
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').
and('contain', 'Your password must be 5-72 characters long.');
// * Verify the appropriate error is returned (new password error shows under the input).
cy.get('.Input___error').should('be.visible').and('contain', 'characters long');
});
it('MM-T937 Users - System admin changes own password - Successfully changed', () => {
@ -212,7 +210,7 @@ describe('System Console > User Management > Users', () => {
cy.get('input[type=password]').eq(1).type('new' + otherAdmin.password);
// # Click the 'Reset' button.
cy.get('button[type=submit] span').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
// # Log out.
cy.apiLogout();

View file

@ -195,7 +195,7 @@ describe('User Management', () => {
// # Set new password.
cy.get('input[type=password]').type('new' + testUser.password);
cy.get('button[type=submit]').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
cy.get('button.btn-primary.confirm').should('contain', 'Reset').click().wait(TIMEOUTS.HALF_SEC);
// * Verify Update email option is visible.
cy.get('#systemUsersTable-cell-0_actionsColumn').click().wait(TIMEOUTS.HALF_SEC);
@ -262,22 +262,22 @@ describe('User Management', () => {
cy.findByText('Update email').click().wait(TIMEOUTS.HALF_SEC);
// # Verify the modal opened.
cy.findByTestId('resetEmailModal').should('exist');
cy.get('#resetEmailModal').should('exist');
// # Type the new e-mail address.
if (newEmail.length > 0) {
cy.get('input[type=email]').eq(0).clear().type(newEmail);
}
// # Click the "Reset" button.
cy.findByTestId('resetEmailButton').click();
// # Click the "Update" button.
cy.get('button.btn-primary.confirm').click();
// * Check for the error messages, if any.
if (errorMsg.length > 0) {
cy.get('form.form-horizontal').find('.has-error p.error').should('be.visible').and('contain', errorMsg);
cy.get('.Input___error').should('be.visible').and('contain', errorMsg);
// # Close the modal.
cy.findByLabelText('Close').click();
cy.get('button.close').click();
}
}

View file

@ -96,4 +96,8 @@ export default class SystemConsolePage {
async clickResetButton() {
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Reset")').click();
}
async clickUpdateEmailButton() {
await this.saveChangesModal.container.locator('button.btn-primary:has-text("Update")').click();
}
}

View file

@ -172,10 +172,10 @@ test('MM-T5520-5 should change the users email', async ({pw}) => {
const updateEmail = await systemConsolePage.systemUsersActionMenus[0].getMenuItem('Update email');
await updateEmail.click();
// # Enter a random password and click Save
// # Enter new email and click Update
const emailInput = systemConsolePage.page.locator('input[type="email"]');
await emailInput.fill(newEmail);
await systemConsolePage.clickResetButton();
await systemConsolePage.clickUpdateEmailButton();
// * Verify that the modal closed
await emailInput.waitFor({state: 'detached'});

View file

@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Shared styles for admin console modals that use the Input component
// Used by: ResetPasswordModal, ResetEmailModal
.ResetPasswordModal,
.ResetEmailModal {
&__body {
display: flex;
flex-direction: column;
gap: 20px;
}
// Override Input component styles to work properly in admin console modal
// The Input component uses a fieldset for the border, so we need to remove
// the default form-control border styling that GenericModal applies
.Input_container {
.Input_fieldset {
background-color: var(--center-channel-bg);
}
.Input_legend {
background-color: var(--center-channel-bg);
}
.Input_wrapper {
background-color: transparent;
}
// Remove the GenericModal form-control border styling since Input uses fieldset for border
// Also remove any background color that might be inherited
.Input.form-control,
.form-control,
input.form-control,
input {
height: auto;
border: none !important;
background: none !important;
background-color: transparent !important;
box-shadow: none !important;
&:focus {
border: none !important;
background: none !important;
background-color: transparent !important;
box-shadow: none !important;
}
&:hover {
background: none !important;
background-color: transparent !important;
}
}
}
}

View file

@ -1,252 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when not the current user 1`] = `
<Modal
animation={true}
aria-labelledby="resetEmailModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
data-testid="resetEmailModal"
dialogClassName="a11y__modal"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="resetEmailModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="Update Email"
id="admin.reset_email.titleReset"
/>
</ModalTitle>
</ModalHeader>
<form
className="form-horizontal"
role="form"
>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="form-group"
>
<div
className="col-sm-10"
>
<div
className="input-group input-group--limit mb-5"
data-testid="resetEmailForm"
>
<span
className="input-group-addon email__group-addon"
data-toggle="tooltip"
title="New Email"
>
<MemoizedFormattedMessage
defaultMessage="New Email"
id="admin.reset_email.newEmail"
/>
</span>
<input
autoFocus={true}
className="form-control"
maxLength={128}
type="email"
/>
</div>
</div>
</div>
</ModalBody>
<ModalFooter
bsClass="modal-footer"
componentClass="div"
>
<button
className="btn btn-tertiary"
onClick={[Function]}
type="button"
>
<MemoizedFormattedMessage
defaultMessage="Cancel"
id="admin.reset_email.cancel"
/>
</button>
<button
className="btn btn-primary"
data-testid="resetEmailButton"
onClick={[Function]}
type="submit"
>
<MemoizedFormattedMessage
defaultMessage="Reset"
id="admin.reset_email.reset"
/>
</button>
</ModalFooter>
</form>
</Modal>
`;
exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when the current user 1`] = `
<Modal
animation={true}
aria-labelledby="resetEmailModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
data-testid="resetEmailModal"
dialogClassName="a11y__modal"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="resetEmailModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="Update Email"
id="admin.reset_email.titleReset"
/>
</ModalTitle>
</ModalHeader>
<form
className="form-horizontal"
role="form"
>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="form-group"
>
<div
className="col-sm-10"
>
<div
className="input-group input-group--limit mb-5"
data-testid="resetEmailForm"
>
<span
className="input-group-addon email__group-addon"
data-toggle="tooltip"
title="New Email"
>
<MemoizedFormattedMessage
defaultMessage="New Email"
id="admin.reset_email.newEmail"
/>
</span>
<input
autoFocus={true}
className="form-control"
maxLength={128}
type="email"
/>
</div>
<div
className="input-group input-group--limit mb-5"
data-testid="resetEmailForm"
>
<span
className="input-group-addon email__group-addon"
data-toggle="tooltip"
title="Current Password"
>
<MemoizedFormattedMessage
defaultMessage="Current Password"
id="admin.reset_email.currentPassword"
/>
</span>
<input
className="form-control"
type="password"
/>
</div>
</div>
</div>
</ModalBody>
<ModalFooter
bsClass="modal-footer"
componentClass="div"
>
<button
className="btn btn-tertiary"
onClick={[Function]}
type="button"
>
<MemoizedFormattedMessage
defaultMessage="Cancel"
id="admin.reset_email.cancel"
/>
</button>
<button
className="btn btn-primary"
data-testid="resetEmailButton"
onClick={[Function]}
type="submit"
>
<MemoizedFormattedMessage
defaultMessage="Reset"
id="admin.reset_email.reset"
/>
</button>
</ModalFooter>
</form>
</Modal>
`;
exports[`components/admin_console/reset_email_modal/reset_email_modal.tsx should match snapshot when there is no user 1`] = `<div />`;

View file

@ -1,113 +1,136 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {UserProfile} from '@mattermost/types/users';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ResetEmailModal from './reset_email_modal';
describe('components/admin_console/reset_email_modal/reset_email_modal.tsx', () => {
const user: UserProfile = TestHelper.getUserMock({
id: 'user_id_1',
email: 'arvin.darmawan@gmail.com',
first_name: 'Arvin',
last_name: 'Darmawan',
});
const baseProps = {
actions: {patchUser: jest.fn(() => Promise.resolve({}))},
user,
currentUserId: 'random_user_id',
onHide: jest.fn(),
onSuccess: jest.fn(),
onExited: jest.fn(),
};
test('should match snapshot when not the current user', () => {
const wrapper = shallow(<ResetEmailModal {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
beforeEach(() => {
jest.clearAllMocks();
});
test('should match snapshot when there is no user', () => {
test('should render modal with user name in title', () => {
renderWithContext(<ResetEmailModal {...baseProps}/>);
expect(screen.getByText(/Update email for Arvin Darmawan/i)).toBeInTheDocument();
expect(screen.getByRole('textbox')).toBeInTheDocument();
});
test('should render null when there is no user', () => {
const props = {...baseProps, user: undefined};
const wrapper = shallow(<ResetEmailModal {...props}/>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ResetEmailModal {...props}/>);
expect(container).toBeEmptyDOMElement();
});
test('should match snapshot when the current user', () => {
test('should show password field when updating own email', () => {
const props = {...baseProps, currentUserId: user.id};
const wrapper = shallow(<ResetEmailModal {...props}/>);
expect(wrapper).toMatchSnapshot();
renderWithContext(<ResetEmailModal {...props}/>);
// Should have both email and password inputs
const inputs = screen.getAllByRole('textbox');
expect(inputs.length).toBeGreaterThanOrEqual(1);
// Password input won't have role="textbox", check by placeholder
expect(screen.getByPlaceholderText(/Current password/i)).toBeInTheDocument();
});
test('should not update email since the email is empty', () => {
const wrapper = mountWithIntl(<ResetEmailModal {...baseProps}/>);
test('should not update email since the email is empty', async () => {
renderWithContext(<ResetEmailModal {...baseProps}/>);
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = '';
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
// Click submit without entering email
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
expect(baseProps.actions.patchUser.mock.calls.length).toBe(0);
expect(wrapper.state('error')).toStrictEqual(
<FormattedMessage
id='user.settings.general.validEmail'
defaultMessage='Please enter a valid email address.'
/>,
);
expect(baseProps.actions.patchUser).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByText(/Please enter a valid email address/i)).toBeInTheDocument();
});
});
test('should not update email since the email is invalid', () => {
const wrapper = mountWithIntl(<ResetEmailModal {...baseProps}/>);
test('should not update email since the email is invalid', async () => {
renderWithContext(<ResetEmailModal {...baseProps}/>);
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'invalid-email';
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
await userEvent.type(emailInput, 'invalid-email');
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
expect(baseProps.actions.patchUser.mock.calls.length).toBe(0);
expect(wrapper.state('error')).toStrictEqual(
<FormattedMessage
id='user.settings.general.validEmail'
defaultMessage='Please enter a valid email address.'
/>,
);
expect(baseProps.actions.patchUser).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByText(/Please enter a valid email address/i)).toBeInTheDocument();
});
});
test('should require password when updating email of the current user', () => {
test('should require password when updating email of the current user', async () => {
const props = {...baseProps, currentUserId: user.id};
const wrapper = mountWithIntl(<ResetEmailModal {...props}/>);
renderWithContext(<ResetEmailModal {...props}/>);
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'currentUser@test.com';
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
await userEvent.type(emailInput, 'currentUser@test.com');
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
expect(baseProps.actions.patchUser.mock.calls.length).toBe(0);
expect(wrapper.state('error')).toStrictEqual(
<FormattedMessage
id='admin.reset_email.missing_current_password'
defaultMessage='Please enter your current password.'
/>,
);
expect(baseProps.actions.patchUser).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByText(/Please enter your current password/i)).toBeInTheDocument();
});
});
test('should update email since the email is valid of the another user', () => {
const wrapper = mountWithIntl(<ResetEmailModal {...baseProps}/>);
test('should update email since the email is valid for another user', async () => {
const patchUser = jest.fn(() => Promise.resolve({}));
const props = {...baseProps, actions: {patchUser}};
renderWithContext(<ResetEmailModal {...props}/>);
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'user@test.com';
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
await userEvent.type(emailInput, 'user@test.com');
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
expect(baseProps.actions.patchUser.mock.calls.length).toBe(1);
expect(wrapper.state('error')).toBeNull();
await waitFor(() => {
expect(patchUser).toHaveBeenCalledTimes(1);
expect(patchUser).toHaveBeenCalledWith(expect.objectContaining({
email: 'user@test.com',
}));
});
});
test('should update email since the email is valid of the current user', () => {
const props = {...baseProps, currentUserId: user.id};
const wrapper = mountWithIntl(<ResetEmailModal {...props}/>);
test('should update email since the email is valid for the current user', async () => {
const patchUser = jest.fn(() => Promise.resolve({}));
const props = {...baseProps, currentUserId: user.id, actions: {patchUser}};
renderWithContext(<ResetEmailModal {...props}/>);
(wrapper.find('input[type=\'email\']').first().instance() as unknown as HTMLInputElement).value = 'currentUser@test.com';
(wrapper.find('input[type=\'password\']').first().instance() as unknown as HTMLInputElement).value = 'password';
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
const emailInput = screen.getByPlaceholderText(/Enter new email address/i);
await userEvent.type(emailInput, 'currentUser@test.com');
expect(baseProps.actions.patchUser.mock.calls.length).toBe(1);
expect(wrapper.state('error')).toBeNull();
const passwordInput = screen.getByPlaceholderText(/Current password/i);
await userEvent.type(passwordInput, 'password123');
await userEvent.click(screen.getByRole('button', {name: /Update/i}));
await waitFor(() => {
expect(patchUser).toHaveBeenCalledTimes(1);
expect(patchUser).toHaveBeenCalledWith(expect.objectContaining({
email: 'currentuser@test.com', // lowercase
password: 'password123',
}));
});
});
});

View file

@ -1,21 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import React, {useCallback, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {GenericModal} from '@mattermost/components';
import type {UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {isEmail} from 'mattermost-redux/utils/helpers';
type State = {
show: boolean;
error: JSX.Element|string|null;
isEmailError: boolean;
isCurrentPasswordError: boolean;
}
import Input from 'components/widgets/inputs/input/input';
import {getFullName} from 'utils/utils';
import '../admin_modal_with_input.scss';
type Props = {
user?: UserProfile;
@ -27,220 +26,159 @@ type Props = {
};
}
export default class ResetEmailModal extends React.PureComponent<Props, State> {
private emailRef: React.RefObject<HTMLInputElement>;
private currentPasswordRef: React.RefObject<HTMLInputElement>;
export default function ResetEmailModal({
user,
currentUserId,
onSuccess,
onExited,
actions,
}: Props) {
const {formatMessage} = useIntl();
public constructor(props: Props) {
super(props);
const [show, setShow] = useState(true);
const [email, setEmail] = useState('');
const [currentPassword, setCurrentPassword] = useState('');
const [error, setError] = useState<React.ReactNode>(null);
const [emailError, setEmailError] = useState<React.ReactNode>(null);
const [passwordError, setPasswordError] = useState<React.ReactNode>(null);
this.state = {
show: true,
error: null,
isEmailError: false,
isCurrentPasswordError: false,
};
const emailRef = useRef<HTMLInputElement>(null);
const currentPasswordRef = useRef<HTMLInputElement>(null);
this.emailRef = React.createRef();
this.currentPasswordRef = React.createRef();
}
const isUpdatingOwnEmail = user?.id === currentUserId;
private isEmailValid = (): boolean => {
if (!this.emailRef.current || !this.emailRef.current.value || !isEmail(this.emailRef.current.value)) {
const errMsg = (
<FormattedMessage
id='user.settings.general.validEmail'
defaultMessage='Please enter a valid email address.'
/>
);
this.setState({error: errMsg, isEmailError: true});
return false;
}
const handleCancel = useCallback(() => {
setShow(false);
}, []);
this.setState({error: null, isEmailError: false});
return true;
};
private isCurrentPasswordValid = (): boolean => {
if (!this.currentPasswordRef.current || !this.currentPasswordRef.current.value) {
const errMsg = (
<FormattedMessage
id='admin.reset_email.missing_current_password'
defaultMessage='Please enter your current password.'
/>
);
this.setState({error: errMsg, isCurrentPasswordError: true});
return false;
}
this.setState({error: null, isCurrentPasswordError: false});
return true;
};
private doSubmit = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (!this.props.user) {
return;
}
if (!this.isEmailValid()) {
return;
}
const user = {
...this.props.user,
email: (this.emailRef.current as HTMLInputElement).value.trim().toLowerCase(),
};
if (this.props.user?.id === this.props.currentUserId) {
if (!this.isCurrentPasswordValid()) {
return;
}
user.password = (this.currentPasswordRef.current as HTMLInputElement).value;
}
const result = await this.props.actions.patchUser(user);
if ('error' in result) {
this.setState({
error: result.error.message,
isEmailError: result.error.server_error_id === 'app.user.save.email_exists.app_error',
isCurrentPasswordError: result.error.server_error_id === 'api.user.check_user_password.invalid.app_error',
});
return;
}
this.props.onSuccess(user.email);
this.setState({show: false});
};
private doCancel = (): void => {
this.setState({
show: false,
error: null,
});
};
public render(): JSX.Element {
const user = this.props.user;
const handleConfirm = useCallback(async () => {
if (!user) {
return <div/>;
return;
}
const groupClass = 'input-group input-group--limit mb-5';
// Clear previous errors
setError(null);
setEmailError(null);
setPasswordError(null);
const title = (
<FormattedMessage
id='admin.reset_email.titleReset'
defaultMessage='Update Email'
/>
);
// Validate email
if (!email || !isEmail(email)) {
setEmailError(formatMessage({
id: 'user.settings.general.validEmail',
defaultMessage: 'Please enter a valid email address.',
}));
return;
}
return (
<Modal
dialogClassName='a11y__modal'
show={this.state.show}
onHide={this.doCancel}
onExited={this.props.onExited}
role='none'
aria-labelledby='resetEmailModalLabel'
data-testid='resetEmailModal'
>
<Modal.Header closeButton={true}>
<Modal.Title
componentClass='h1'
id='resetEmailModalLabel'
>
{title}
</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div className='form-group'>
<div className='col-sm-10'>
<div
className={`${groupClass}${this.state.isEmailError ? ' has-error' : ''}`}
data-testid='resetEmailForm'
>
<span
data-toggle='tooltip'
title='New Email'
className='input-group-addon email__group-addon'
>
<FormattedMessage
id='admin.reset_email.newEmail'
defaultMessage='New Email'
/>
</span>
<input
type='email'
ref={this.emailRef}
className='form-control'
maxLength={128}
autoFocus={true}
/>
</div>
// Validate current password if updating own email
if (isUpdatingOwnEmail && !currentPassword) {
setPasswordError(formatMessage({
id: 'admin.reset_email.missing_current_password',
defaultMessage: 'Please enter your current password.',
}));
return;
}
{this.props.user?.id === this.props.currentUserId && (
<div
className={`${groupClass}${this.state.isCurrentPasswordError ? ' has-error' : ''}`}
data-testid='resetEmailForm'
>
<span
data-toggle='tooltip'
title='Current Password'
className='input-group-addon email__group-addon'
>
<FormattedMessage
id='admin.reset_email.currentPassword'
defaultMessage='Current Password'
/>
</span>
<input
type='password'
ref={this.currentPasswordRef}
className='form-control'
/>
</div>
)}
const updatedUser: UserProfile = {
...user,
email: email.trim().toLowerCase(),
};
{this.state.error && (
<div className='has-error'>
<p className='input__help error'>
{this.state.error}
</p>
</div>
)}
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-tertiary'
onClick={this.doCancel}
>
<FormattedMessage
id='admin.reset_email.cancel'
defaultMessage='Cancel'
/>
</button>
<button
onClick={this.doSubmit}
type='submit'
className='btn btn-primary'
data-testid='resetEmailButton'
>
<FormattedMessage
id='admin.reset_email.reset'
defaultMessage='Reset'
/>
</button>
</Modal.Footer>
</form>
</Modal>
);
if (isUpdatingOwnEmail) {
updatedUser.password = currentPassword;
}
const result = await actions.patchUser(updatedUser);
if ('error' in result) {
const isEmailError = result.error.server_error_id === 'app.user.save.email_exists.app_error';
const isPasswordError = result.error.server_error_id === 'api.user.check_user_password.invalid.app_error';
if (isEmailError) {
setEmailError(result.error.message);
} else if (isPasswordError) {
setPasswordError(result.error.message);
} else {
setError(result.error.message);
}
return;
}
onSuccess(updatedUser.email);
setShow(false);
}, [user, email, currentPassword, isUpdatingOwnEmail, actions, onSuccess, formatMessage]);
if (!user) {
return null;
}
const displayName = getFullName(user) || user.username;
const title = formatMessage({
id: 'admin.reset_email.titleResetFor',
defaultMessage: 'Update email for {name}',
}, {name: displayName});
return (
<GenericModal
id='resetEmailModal'
className='ResetEmailModal'
modalHeaderText={title}
show={show}
onExited={onExited}
onHide={handleCancel}
handleCancel={handleCancel}
handleConfirm={handleConfirm}
handleEnterKeyPress={handleConfirm}
confirmButtonText={formatMessage({
id: 'admin.reset_email.update',
defaultMessage: 'Update',
})}
compassDesign={true}
autoCloseOnConfirmButton={false}
errorText={error ? <span className='error'>{error}</span> : undefined}
dataTestId='resetEmailModal'
>
<div className='ResetEmailModal__body'>
<Input
ref={emailRef as React.Ref<HTMLInputElement>}
type='email'
name='newEmail'
autoComplete='off'
label={formatMessage({
id: 'admin.reset_email.newEmail',
defaultMessage: 'New email',
})}
placeholder={formatMessage({
id: 'admin.reset_email.enterNewEmail',
defaultMessage: 'Enter new email address',
})}
value={email}
onChange={(e) => setEmail(e.target.value)}
autoFocus={true}
maxLength={128}
customMessage={emailError ? {type: 'error', value: emailError} : undefined}
/>
{isUpdatingOwnEmail && (
<Input
ref={currentPasswordRef as React.Ref<HTMLInputElement>}
type='password'
name='currentPassword'
autoComplete='current-password'
label={formatMessage({
id: 'admin.reset_email.currentPassword',
defaultMessage: 'Current password',
})}
placeholder={formatMessage({
id: 'admin.reset_email.enterCurrentPassword',
defaultMessage: 'Enter current password',
})}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
customMessage={passwordError ? {type: 'error', value: passwordError} : undefined}
/>
)}
</div>
</GenericModal>
);
}

View file

@ -1,137 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/admin_console/reset_password_modal/reset_password_modal.tsx should match snapshot 1`] = `
<Modal
animation={true}
aria-labelledby="resetPasswordModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="none"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="resetPasswordModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="Switch Account to Email/Password"
id="admin.reset_password.titleSwitch"
/>
</ModalTitle>
</ModalHeader>
<form
className="form-horizontal"
role="form"
>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="form-group"
>
<div
className="col-sm-10 password__group-addon-space"
>
<div
className="input-group input-group--limit"
>
<span
className="input-group-addon password__group-addon"
data-toggle="tooltip"
title="Current Password"
>
<MemoizedFormattedMessage
defaultMessage="Current Password"
id="admin.reset_password.curentPassword"
/>
</span>
<input
autoFocus={true}
className="form-control"
type="password"
/>
</div>
</div>
<div
className="col-sm-10"
>
<div
className="input-group input-group--limit"
>
<span
className="input-group-addon password__group-addon"
data-toggle="tooltip"
title="New Password"
>
<MemoizedFormattedMessage
defaultMessage="New Password"
id="admin.reset_password.newPassword"
/>
</span>
<input
autoFocus={false}
className="form-control"
type="password"
/>
</div>
</div>
</div>
</ModalBody>
<ModalFooter
bsClass="modal-footer"
componentClass="div"
>
<button
className="btn btn-tertiary"
onClick={[Function]}
type="button"
>
<MemoizedFormattedMessage
defaultMessage="Cancel"
id="admin.reset_password.cancel"
/>
</button>
<button
className="btn btn-primary"
onClick={[Function]}
type="submit"
>
<MemoizedFormattedMessage
defaultMessage="Reset"
id="admin.reset_password.reset"
/>
</button>
</ModalFooter>
</form>
</Modal>
`;
exports[`components/admin_console/reset_password_modal/reset_password_modal.tsx should match snapshot when there is no user 1`] = `<div />`;

View file

@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {UserNotifyProps, UserProfile} from '@mattermost/types/users';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ResetPasswordModal from './reset_password_modal';
@ -27,16 +25,20 @@ describe('components/admin_console/reset_password_modal/reset_password_modal.tsx
push: 'default',
push_status: 'ooo',
};
const user: UserProfile = TestHelper.getUserMock({
auth_service: 'test',
id: 'user_id_1',
auth_service: '',
notify_props: notifyProps,
first_name: 'Test',
last_name: 'User',
username: 'testuser',
});
const baseProps = {
actions: {updateUserPassword: jest.fn(() => Promise.resolve({data: ''}))},
currentUserId: user.id,
user,
onHide: jest.fn(),
onExited: jest.fn(),
passwordConfig: {
minimumLength: 10,
@ -47,65 +49,119 @@ describe('components/admin_console/reset_password_modal/reset_password_modal.tsx
},
};
test('should match snapshot', () => {
const wrapper = shallow(
<ResetPasswordModal {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
beforeEach(() => {
jest.clearAllMocks();
});
test('should match snapshot when there is no user', () => {
test('should render modal with user name in title', () => {
renderWithContext(<ResetPasswordModal {...baseProps}/>);
expect(screen.getByText(/Reset password for Test User/i)).toBeInTheDocument();
});
test('should render null when there is no user', () => {
const props = {...baseProps, user: undefined};
const wrapper = shallow(
<ResetPasswordModal {...props}/>,
);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ResetPasswordModal {...props}/>);
expect(container).toBeEmptyDOMElement();
});
test('should call updateUserPassword', () => {
test('should show switch account title when user has auth_service', () => {
const authUser = TestHelper.getUserMock({
...user,
auth_service: 'ldap',
});
const props = {...baseProps, user: authUser};
renderWithContext(<ResetPasswordModal {...props}/>);
expect(screen.getByText(/Switch account to Email\/Password/i)).toBeInTheDocument();
});
test('should show current password field when resetting own password', () => {
renderWithContext(<ResetPasswordModal {...baseProps}/>);
expect(screen.getByPlaceholderText(/Current password/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/New password/i)).toBeInTheDocument();
});
test('should not show current password field when resetting another user password', () => {
const props = {...baseProps, currentUserId: 'different_user_id'};
renderWithContext(<ResetPasswordModal {...props}/>);
expect(screen.queryByPlaceholderText(/Current password/i)).not.toBeInTheDocument();
expect(screen.getByPlaceholderText(/New password/i)).toBeInTheDocument();
});
test('should call updateUserPassword with both passwords when resetting own password', async () => {
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
const oldPassword = 'oldPassword123!';
const newPassword = 'newPassword123!';
const props = {...baseProps, actions: {updateUserPassword}};
const wrapper = mountWithIntl(<ResetPasswordModal {...props}/>);
renderWithContext(<ResetPasswordModal {...props}/>);
(wrapper.find('input[type=\'password\']').first().instance() as unknown as HTMLInputElement).value = oldPassword;
(wrapper.find('input[type=\'password\']').last().instance() as unknown as HTMLInputElement).value = newPassword;
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
const currentPasswordInput = screen.getByPlaceholderText(/Current password/i);
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
expect(updateUserPassword.mock.calls.length).toBe(1);
expect(wrapper.state('serverErrorCurrentPass')).toBeNull();
expect(wrapper.state('serverErrorNewPass')).toBeNull();
await userEvent.type(currentPasswordInput, 'oldPassword123!');
await userEvent.type(newPasswordInput, 'newPassword123!');
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
await waitFor(() => {
expect(updateUserPassword).toHaveBeenCalledTimes(1);
expect(updateUserPassword).toHaveBeenCalledWith(
user.id,
'oldPassword123!',
'newPassword123!',
);
});
});
test('should not call updateUserPassword when the old password is not provided', () => {
test('should not call updateUserPassword when the current password is not provided', async () => {
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
const newPassword = 'newPassword123!';
const props = {...baseProps, actions: {updateUserPassword}};
const wrapper = mountWithIntl(<ResetPasswordModal {...props}/>);
renderWithContext(<ResetPasswordModal {...props}/>);
(wrapper.find('input[type=\'password\']').last().instance() as unknown as HTMLInputElement).value = newPassword;
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
await userEvent.type(newPasswordInput, 'newPassword123!');
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
expect(updateUserPassword.mock.calls.length).toBe(0);
expect(wrapper.state('serverErrorCurrentPass')).toStrictEqual(
<FormattedMessage
defaultMessage='Please enter your current password.'
id='admin.reset_password.missing_current'
/>);
expect(wrapper.state('serverErrorNewPass')).toBeNull();
expect(updateUserPassword).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByText(/Please enter your current password/i)).toBeInTheDocument();
});
});
test('should call updateUserPassword', () => {
test('should call updateUserPassword without current password when resetting another user', async () => {
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
const password = 'Password123!';
const props = {...baseProps, currentUserId: 'different_user_id', actions: {updateUserPassword}};
renderWithContext(<ResetPasswordModal {...props}/>);
const props = {...baseProps, currentUserId: '2', actions: {updateUserPassword}};
const wrapper = mountWithIntl(<ResetPasswordModal {...props}/>);
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
await userEvent.type(newPasswordInput, 'Password123!');
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
(wrapper.find('input[type=\'password\']').first().instance() as unknown as HTMLInputElement).value = password;
wrapper.find('button[type=\'submit\']').first().simulate('click', {preventDefault: jest.fn()});
await waitFor(() => {
expect(updateUserPassword).toHaveBeenCalledTimes(1);
expect(updateUserPassword).toHaveBeenCalledWith(
user.id,
'',
'Password123!',
);
});
});
expect(updateUserPassword.mock.calls.length).toBe(1);
test('should show error when password does not meet requirements', async () => {
const updateUserPassword = jest.fn(() => Promise.resolve({data: ''}));
const props = {...baseProps, currentUserId: 'different_user_id', actions: {updateUserPassword}};
renderWithContext(<ResetPasswordModal {...props}/>);
const newPasswordInput = screen.getByPlaceholderText(/New password/i);
await userEvent.type(newPasswordInput, 'weak');
await userEvent.click(screen.getByRole('button', {name: /Reset/i}));
expect(updateUserPassword).not.toHaveBeenCalled();
// Password validation error should appear
await waitFor(() => {
expect(screen.getByText(/Must be 10-72 characters long/i)).toBeInTheDocument();
});
});
});

View file

@ -1,15 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import React, {useCallback, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {GenericModal} from '@mattermost/components';
import type {UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
import Input from 'components/widgets/inputs/input/input';
import {isValidPassword} from 'utils/password';
import {getFullName} from 'utils/utils';
import '../admin_modal_with_input.scss';
interface PasswordConfig {
minimumLength: number;
@ -19,12 +24,6 @@ interface PasswordConfig {
requireUppercase: boolean;
}
type State = {
show: boolean;
serverErrorNewPass: React.ReactNode;
serverErrorCurrentPass: React.ReactNode;
}
export type Props = {
user?: UserProfile;
currentUserId: string;
@ -36,215 +35,146 @@ export type Props = {
};
}
export default class ResetPasswordModal extends React.PureComponent<Props, State> {
private currentPasswordRef: React.RefObject<HTMLInputElement>;
private passwordRef: React.RefObject<HTMLInputElement>;
export default function ResetPasswordModal({
user,
currentUserId,
onSuccess,
onExited,
passwordConfig,
actions,
}: Props) {
const {formatMessage} = useIntl();
public constructor(props: Props) {
super(props);
const [show, setShow] = useState(true);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [errorNewPass, setErrorNewPass] = useState<React.ReactNode>(null);
const [errorCurrentPass, setErrorCurrentPass] = useState<React.ReactNode>(null);
this.state = {
show: true,
serverErrorNewPass: null,
serverErrorCurrentPass: null,
};
const currentPasswordRef = useRef<HTMLInputElement>(null);
const newPasswordRef = useRef<HTMLInputElement>(null);
this.currentPasswordRef = React.createRef();
this.passwordRef = React.createRef();
}
const isResettingOwnPassword = user?.id === currentUserId;
public componentWillUnmount(): void {
this.setState({
serverErrorNewPass: null,
serverErrorCurrentPass: null,
});
}
const handleCancel = useCallback(() => {
setShow(false);
}, []);
private doSubmit = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
if (!this.props.user) {
const handleConfirm = useCallback(async () => {
if (!user) {
return;
}
let currentPassword = '';
if (this.currentPasswordRef.current) {
currentPassword = (this.currentPasswordRef.current as HTMLInputElement).value;
if (currentPassword === '') {
const errorMsg = (
<FormattedMessage
id='admin.reset_password.missing_current'
defaultMessage='Please enter your current password.'
/>
);
this.setState({serverErrorCurrentPass: errorMsg});
return;
}
// Clear previous errors
setErrorNewPass(null);
setErrorCurrentPass(null);
// Validate current password if resetting own password
if (isResettingOwnPassword && currentPassword === '') {
setErrorCurrentPass(formatMessage({
id: 'admin.reset_password.missing_current',
defaultMessage: 'Please enter your current password.',
}));
return;
}
const password = (this.passwordRef.current as HTMLInputElement).value;
const {valid, error} = isValidPassword(password, this.props.passwordConfig);
// Validate new password
const {valid, error} = isValidPassword(newPassword, passwordConfig);
if (!valid && error) {
this.setState({serverErrorNewPass: error});
setErrorNewPass(error);
return;
}
this.setState({serverErrorNewPass: null});
const result = await this.props.actions.updateUserPassword(this.props.user.id, currentPassword, password);
if ('error' in result) {
this.setState({serverErrorCurrentPass: result.error.message});
return;
}
this.props.onSuccess?.();
this.setState({show: false});
};
private doCancel = (): void => {
this.setState({
show: false,
serverErrorNewPass: null,
serverErrorCurrentPass: null,
});
};
public render(): JSX.Element {
const user = this.props.user;
if (user == null) {
return <div/>;
}
let urlClass = 'input-group input-group--limit';
let serverErrorNewPass = null;
if (this.state.serverErrorNewPass) {
urlClass += ' has-error';
serverErrorNewPass = <div className='has-error'><p className='input__help error'>{this.state.serverErrorNewPass}</p></div>;
}
let title;
if (user.auth_service) {
title = (
<FormattedMessage
id='admin.reset_password.titleSwitch'
defaultMessage='Switch Account to Email/Password'
/>
);
} else {
title = (
<FormattedMessage
id='admin.reset_password.titleReset'
defaultMessage='Reset Password'
/>
);
}
let currentPassword = null;
let serverErrorCurrentPass = null;
let newPasswordFocus = true;
if (this.props.currentUserId === user.id) {
newPasswordFocus = false;
let urlClassCurrentPass = 'input-group input-group--limit';
if (this.state.serverErrorCurrentPass) {
urlClassCurrentPass += ' has-error';
serverErrorCurrentPass = <div className='has-error'><p className='input__help error'>{this.state.serverErrorCurrentPass}</p></div>;
}
currentPassword = (
<div className='col-sm-10 password__group-addon-space'>
<div className={urlClassCurrentPass}>
<span
data-toggle='tooltip'
title='Current Password'
className='input-group-addon password__group-addon'
>
<FormattedMessage
id='admin.reset_password.curentPassword'
defaultMessage='Current Password'
/>
</span>
<input
type='password'
ref={this.currentPasswordRef}
className='form-control'
autoFocus={true}
/>
</div>
</div>
);
}
return (
<Modal
dialogClassName='a11y__modal'
show={this.state.show}
onHide={this.doCancel}
onExited={this.props.onExited}
role='none'
aria-labelledby='resetPasswordModalLabel'
>
<Modal.Header closeButton={true}>
<Modal.Title
componentClass='h1'
id='resetPasswordModalLabel'
>
{title}
</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div className='form-group'>
{currentPassword}
<div className='col-sm-10'>
<div className={urlClass}>
<span
data-toggle='tooltip'
title='New Password'
className='input-group-addon password__group-addon'
>
<FormattedMessage
id='admin.reset_password.newPassword'
defaultMessage='New Password'
/>
</span>
<input
type='password'
ref={this.passwordRef}
className='form-control'
autoFocus={newPasswordFocus}
/>
</div>
{serverErrorNewPass}
{serverErrorCurrentPass}
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-tertiary'
onClick={this.doCancel}
>
<FormattedMessage
id='admin.reset_password.cancel'
defaultMessage='Cancel'
/>
</button>
<button
onClick={this.doSubmit}
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='admin.reset_password.reset'
defaultMessage='Reset'
/>
</button>
</Modal.Footer>
</form>
</Modal>
const result = await actions.updateUserPassword(
user.id,
isResettingOwnPassword ? currentPassword : '',
newPassword,
);
if ('error' in result) {
setErrorCurrentPass(result.error.message);
return;
}
onSuccess?.();
setShow(false);
}, [user, isResettingOwnPassword, currentPassword, newPassword, passwordConfig, actions, onSuccess, formatMessage]);
if (!user) {
return null;
}
const displayName = getFullName(user) || user.username;
const isAuthUser = Boolean(user.auth_service);
const title = isAuthUser ?
formatMessage({
id: 'admin.reset_password.titleSwitchFor',
defaultMessage: 'Switch account to Email/Password for {name}',
}, {name: displayName}) :
formatMessage({
id: 'admin.reset_password.titleResetFor',
defaultMessage: 'Reset password for {name}',
}, {name: displayName});
return (
<GenericModal
id='resetPasswordModal'
className='ResetPasswordModal'
modalHeaderText={title}
show={show}
onExited={onExited}
onHide={handleCancel}
handleCancel={handleCancel}
handleConfirm={handleConfirm}
handleEnterKeyPress={handleConfirm}
confirmButtonText={formatMessage({
id: 'admin.reset_password.reset',
defaultMessage: 'Reset',
})}
compassDesign={true}
autoCloseOnConfirmButton={false}
errorText={errorCurrentPass ? <span className='error'>{errorCurrentPass}</span> : undefined}
>
<div className='ResetPasswordModal__body'>
{isResettingOwnPassword && (
<Input
ref={currentPasswordRef as React.Ref<HTMLInputElement>}
type='password'
name='currentPassword'
autoComplete='current-password'
label={formatMessage({
id: 'admin.reset_password.currentPassword',
defaultMessage: 'Current password',
})}
placeholder={formatMessage({
id: 'admin.reset_password.enterCurrentPassword',
defaultMessage: 'Enter current password',
})}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
autoFocus={true}
/>
)}
<Input
ref={newPasswordRef as React.Ref<HTMLInputElement>}
type='password'
name='newPassword'
autoComplete='new-password'
label={formatMessage({
id: 'admin.reset_password.newPassword',
defaultMessage: 'New password',
})}
placeholder={formatMessage({
id: 'admin.reset_password.enterNewPassword',
defaultMessage: 'Enter new password',
})}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
autoFocus={!isResettingOwnPassword}
customMessage={errorNewPass ? {type: 'error', value: errorNewPass} : undefined}
/>
</div>
</GenericModal>
);
}

View file

@ -2454,19 +2454,21 @@
"admin.requestButton.loading": " Loading...",
"admin.requestButton.requestFailure": "Test Failure: {error}",
"admin.requestButton.requestSuccess": "Test Successful",
"admin.reset_email.cancel": "Cancel",
"admin.reset_email.currentPassword": "Current Password",
"admin.reset_email.enterCurrentPassword": "Enter current password",
"admin.reset_email.enterNewEmail": "Enter new email address",
"admin.reset_email.missing_current_password": "Please enter your current password.",
"admin.reset_email.newEmail": "New Email",
"admin.reset_email.reset": "Reset",
"admin.reset_email.titleReset": "Update Email",
"admin.reset_password.cancel": "Cancel",
"admin.reset_password.curentPassword": "Current Password",
"admin.reset_email.titleResetFor": "Update email for {name}",
"admin.reset_email.update": "Update",
"admin.reset_password.currentPassword": "Current password",
"admin.reset_password.enterCurrentPassword": "Enter current password",
"admin.reset_password.enterNewPassword": "Enter new password",
"admin.reset_password.missing_current": "Please enter your current password.",
"admin.reset_password.newPassword": "New Password",
"admin.reset_password.reset": "Reset",
"admin.reset_password.titleReset": "Reset Password",
"admin.reset_password.titleSwitch": "Switch Account to Email/Password",
"admin.reset_password.titleResetFor": "Reset password for {name}",
"admin.reset_password.titleSwitchFor": "Switch account to Email/Password for {name}",
"admin.revoke_token_button.delete": "Delete",
"admin.s3.connectionS3Test": "Test Connection",
"admin.s3.s3Fail": "Connection unsuccessful: {error}",

View file

@ -126,7 +126,8 @@
display: flex;
box-sizing: border-box;
flex: 1;
padding: 14px 32px;
margin: 0 32px;
padding: 14px 16px;
border: 1px solid rgba(var(--dnd-indicator-rgb), 0.16);
margin-bottom: 24px;
background: rgba(var(--dnd-indicator-rgb), 0.08);