--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+.reset-page {
+ padding-top: 10vh;
+}
+
+.reset-page h1 {
+ line-height: 1.5;
+ font-size: 24px;
+ font-weight: 300;
+ text-align: center;
+}
+
+.reset-form {
+ width: 300px;
+ margin-left: auto;
+ margin-right: auto;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import ResetPasswordForm from '../../components/common/ResetPassword';
+import { whenLoggedIn } from '../../components/hoc/whenLoggedIn';
+import { Router, withRouter } from '../../components/hoc/withRouter';
+import GlobalMessagesContainer from './GlobalMessagesContainer';
+import './ResetPassword.css';
+
+export interface ResetPasswordProps {
+ currentUser: T.LoggedInUser;
+ router: Router;
+}
+
+export function ResetPassword(props: ResetPasswordProps) {
+ const { router, currentUser } = props;
+ const redirect = () => {
+ router.replace('/');
+ };
+
+ return (
+ <div className="reset-page">
+ <h1 className="text-center spacer-bottom">{translate('my_account.reset_password')}</h1>
+ <h2 className="text-center huge-spacer-bottom">
+ {translate('my_account.reset_password.explain')}
+ </h2>
+ <GlobalMessagesContainer />
+ <div className="reset-form">
+ <ResetPasswordForm user={currentUser} onPasswordChange={redirect} />
+ </div>
+ </div>
+ );
+}
+
+export default whenLoggedIn(withRouter(ResetPassword));
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockLoggedInUser, mockRouter } from '../../../helpers/testMocks';
+import { ResetPassword, ResetPasswordProps } from '../ResetPassword';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<ResetPasswordProps> = {}) {
+ return shallow(
+ <ResetPassword currentUser={mockLoggedInUser()} router={mockRouter()} {...props} />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="reset-page"
+>
+ <h1
+ className="text-center spacer-bottom"
+ >
+ my_account.reset_password
+ </h1>
+ <h2
+ className="text-center huge-spacer-bottom"
+ >
+ my_account.reset_password.explain
+ </h2>
+ <Connect(GlobalMessages) />
+ <div
+ className="reset-form"
+ >
+ <ResetPassword
+ onPasswordChange={[Function]}
+ user={
+ Object {
+ "groups": Array [],
+ "isLoggedIn": true,
+ "login": "luke",
+ "name": "Skywalker",
+ "scmAccounts": Array [],
+ }
+ }
+ />
+ </div>
+</div>
+`;
{renderAdminRoutes()}
</Route>
+ <Route
+ // We don't want this route to have any menu.
+ // That is why we can not have it under the accountRoutes
+ path="account/reset_password"
+ component={lazyLoadComponent(() => import('../components/ResetPassword'))}
+ />
<Route
path="not_found"
component={lazyLoadComponent(() => import('../components/NotFound'))}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import { Alert } from 'sonar-ui-common/components/ui/Alert';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { changePassword } from '../../../api/users';
-
-interface Props {
- user: T.LoggedInUser;
-}
-
-interface State {
- errors?: string[];
- success: boolean;
-}
-
-export default class Password extends React.Component<Props, State> {
- oldPassword!: HTMLInputElement;
- password!: HTMLInputElement;
- passwordConfirmation!: HTMLInputElement;
- state: State = {
- success: false
- };
-
- handleSuccessfulChange = () => {
- this.oldPassword.value = '';
- this.password.value = '';
- this.passwordConfirmation.value = '';
- this.setState({ success: true, errors: undefined });
- };
-
- setErrors = (errors: string[]) => {
- this.setState({ success: false, errors });
- };
-
- handleChangePassword = (event: React.FormEvent) => {
- event.preventDefault();
-
- const { user } = this.props;
- const previousPassword = this.oldPassword.value;
- const password = this.password.value;
- const passwordConfirmation = this.passwordConfirmation.value;
-
- if (password !== passwordConfirmation) {
- this.password.focus();
- this.setErrors([translate('user.password_doesnt_match_confirmation')]);
- } else {
- changePassword({ login: user.login, password, previousPassword }).then(
- this.handleSuccessfulChange,
- () => {}
- );
- }
- };
-
- render() {
- const { success, errors } = this.state;
-
- return (
- <section className="boxed-group">
- <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>
-
- <form className="boxed-group-inner" onSubmit={this.handleChangePassword}>
- {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>}
-
- {errors &&
- errors.map((e, i) => (
- <Alert key={i} variant="error">
- {e}
- </Alert>
- ))}
-
- <div className="form-field">
- <label htmlFor="old_password">
- {translate('my_profile.password.old')}
- <em className="mandatory">*</em>
- </label>
- <input
- autoComplete="off"
- id="old_password"
- name="old_password"
- ref={elem => (this.oldPassword = elem!)}
- required={true}
- type="password"
- />
- </div>
- <div className="form-field">
- <label htmlFor="password">
- {translate('my_profile.password.new')}
- <em className="mandatory">*</em>
- </label>
- <input
- autoComplete="off"
- id="password"
- name="password"
- ref={elem => (this.password = elem!)}
- required={true}
- type="password"
- />
- </div>
- <div className="form-field">
- <label htmlFor="password_confirmation">
- {translate('my_profile.password.confirm')}
- <em className="mandatory">*</em>
- </label>
- <input
- autoComplete="off"
- id="password_confirmation"
- name="password_confirmation"
- ref={elem => (this.passwordConfirmation = elem!)}
- required={true}
- type="password"
- />
- </div>
- <div className="form-field">
- <SubmitButton id="change-password">
- {translate('my_profile.password.submit')}
- </SubmitButton>
- </div>
- </form>
- </section>
- );
- }
-}
import { Helmet } from 'react-helmet-async';
import { connect } from 'react-redux';
import { translate } from 'sonar-ui-common/helpers/l10n';
+import ResetPassword from '../../../components/common/ResetPassword';
import { getCurrentUser, Store } from '../../../store/rootReducer';
-import Password from './Password';
import Tokens from './Tokens';
export interface SecurityProps {
<div className="account-body account-container">
<Helmet defer={false} title={translate('my_account.security')} />
<Tokens login={user.login} />
- {user.local && <Password user={user} />}
+ {user.local && <ResetPassword user={user} />}
</div>
);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import Password from '../Password';
-
-it('renders correctly', () => {
- expect(shallow(<Password user={mockLoggedInUser()} />)).toMatchSnapshot();
-});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders correctly 1`] = `
-<section
- className="boxed-group"
->
- <h2
- className="spacer-bottom"
- >
- my_profile.password.title
- </h2>
- <form
- className="boxed-group-inner"
- onSubmit={[Function]}
- >
- <div
- className="form-field"
- >
- <label
- htmlFor="old_password"
- >
- my_profile.password.old
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <input
- autoComplete="off"
- id="old_password"
- name="old_password"
- required={true}
- type="password"
- />
- </div>
- <div
- className="form-field"
- >
- <label
- htmlFor="password"
- >
- my_profile.password.new
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <input
- autoComplete="off"
- id="password"
- name="password"
- required={true}
- type="password"
- />
- </div>
- <div
- className="form-field"
- >
- <label
- htmlFor="password_confirmation"
- >
- my_profile.password.confirm
- <em
- className="mandatory"
- >
- *
- </em>
- </label>
- <input
- autoComplete="off"
- id="password_confirmation"
- name="password_confirmation"
- required={true}
- type="password"
- />
- </div>
- <div
- className="form-field"
- >
- <SubmitButton
- id="change-password"
- >
- my_profile.password.submit
- </SubmitButton>
- </div>
- </form>
-</section>
-`;
<Tokens
login="luke"
/>
- <Password
+ <ResetPassword
user={
Object {
"groups": Array [],
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { changePassword } from '../../api/users';
+
+interface Props {
+ user: T.LoggedInUser;
+ onPasswordChange?: () => void;
+}
+
+interface State {
+ errors?: string[];
+ success: boolean;
+}
+
+export default class ResetPassword extends React.Component<Props, State> {
+ oldPassword: HTMLInputElement | null = null;
+ password: HTMLInputElement | null = null;
+ passwordConfirmation: HTMLInputElement | null = null;
+ state: State = {
+ success: false
+ };
+
+ handleSuccessfulChange = () => {
+ if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
+ return;
+ }
+ this.oldPassword.value = '';
+ this.password.value = '';
+ this.passwordConfirmation.value = '';
+ this.setState({ success: true, errors: undefined });
+ if (this.props.onPasswordChange) {
+ this.props.onPasswordChange();
+ }
+ };
+
+ setErrors = (errors: string[]) => {
+ this.setState({ success: false, errors });
+ };
+
+ handleChangePassword = (event: React.FormEvent) => {
+ event.preventDefault();
+ if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
+ return;
+ }
+ const { user } = this.props;
+ const previousPassword = this.oldPassword.value;
+ const password = this.password.value;
+ const passwordConfirmation = this.passwordConfirmation.value;
+
+ if (password !== passwordConfirmation) {
+ this.password.focus();
+ this.setErrors([translate('user.password_doesnt_match_confirmation')]);
+ } else {
+ changePassword({ login: user.login, password, previousPassword }).then(
+ this.handleSuccessfulChange,
+ () => {
+ // error already reported.
+ }
+ );
+ }
+ };
+
+ render() {
+ const { success, errors } = this.state;
+
+ return (
+ <section className="boxed-group">
+ <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>
+
+ <form className="boxed-group-inner" onSubmit={this.handleChangePassword}>
+ {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>}
+
+ {errors &&
+ errors.map((e, i) => (
+ /* eslint-disable-next-line react/no-array-index-key */
+ <Alert key={i} variant="error">
+ {e}
+ </Alert>
+ ))}
+
+ <div className="form-field">
+ <label htmlFor="old_password">
+ {translate('my_profile.password.old')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoComplete="off"
+ id="old_password"
+ name="old_password"
+ ref={elem => (this.oldPassword = elem)}
+ required={true}
+ type="password"
+ />
+ </div>
+ <div className="form-field">
+ <label htmlFor="password">
+ {translate('my_profile.password.new')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoComplete="off"
+ id="password"
+ name="password"
+ ref={elem => (this.password = elem)}
+ required={true}
+ type="password"
+ />
+ </div>
+ <div className="form-field">
+ <label htmlFor="password_confirmation">
+ {translate('my_profile.password.confirm')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoComplete="off"
+ id="password_confirmation"
+ name="password_confirmation"
+ ref={elem => (this.passwordConfirmation = elem)}
+ required={true}
+ type="password"
+ />
+ </div>
+ <div className="form-field">
+ <SubmitButton id="change-password">{translate('update_verb')}</SubmitButton>
+ </div>
+ </form>
+ </section>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
+import { changePassword } from '../../../api/users';
+import { mockEvent, mockLoggedInUser } from '../../../helpers/testMocks';
+import ResetPassword from '../ResetPassword';
+
+jest.mock('../../../api/users', () => ({
+ changePassword: jest.fn().mockResolvedValue({})
+}));
+
+it('should trigger on password change prop', () => {
+ const onPasswordChange = jest.fn();
+ const wrapper = shallowRender({ onPasswordChange });
+ wrapper.instance().handleSuccessfulChange();
+ expect(onPasswordChange).not.toBeCalled();
+ wrapper.instance().oldPassword = { value: '' } as HTMLInputElement;
+ wrapper.instance().password = { value: '' } as HTMLInputElement;
+ wrapper.instance().passwordConfirmation = { value: '' } as HTMLInputElement;
+ wrapper.instance().handleSuccessfulChange();
+ expect(onPasswordChange).toBeCalled();
+});
+
+it('should not trigger password change', () => {
+ const wrapper = shallowRender();
+ wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement;
+ wrapper.instance().password = { value: 'test', focus: () => {} } as HTMLInputElement;
+ wrapper.instance().passwordConfirmation = { value: 'test1' } as HTMLInputElement;
+ wrapper.instance().handleChangePassword(mockEvent());
+ expect(changePassword).not.toBeCalled();
+ expect(wrapper.state().errors).toBeDefined();
+});
+
+it('should trigger password change', async () => {
+ const user = mockLoggedInUser();
+ const wrapper = shallowRender({ user });
+ wrapper.instance().handleChangePassword(mockEvent());
+ await waitAndUpdate(wrapper);
+ expect(changePassword).not.toBeCalled();
+
+ wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement;
+ wrapper.instance().password = { value: 'test' } as HTMLInputElement;
+ wrapper.instance().passwordConfirmation = { value: 'test' } as HTMLInputElement;
+ wrapper.instance().handleChangePassword(mockEvent());
+ await waitAndUpdate(wrapper);
+
+ expect(changePassword).toBeCalledWith({
+ login: user.login,
+ password: 'test',
+ previousPassword: 'testold'
+ });
+});
+
+it('renders correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<ResetPassword['props']>) {
+ return shallow<ResetPassword>(<ResetPassword user={mockLoggedInUser()} {...props} />);
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<section
+ className="boxed-group"
+>
+ <h2
+ className="spacer-bottom"
+ >
+ my_profile.password.title
+ </h2>
+ <form
+ className="boxed-group-inner"
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="old_password"
+ >
+ my_profile.password.old
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoComplete="off"
+ id="old_password"
+ name="old_password"
+ required={true}
+ type="password"
+ />
+ </div>
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="password"
+ >
+ my_profile.password.new
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoComplete="off"
+ id="password"
+ name="password"
+ required={true}
+ type="password"
+ />
+ </div>
+ <div
+ className="form-field"
+ >
+ <label
+ htmlFor="password_confirmation"
+ >
+ my_profile.password.confirm
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoComplete="off"
+ id="password_confirmation"
+ name="password_confirmation"
+ required={true}
+ type="password"
+ />
+ </div>
+ <div
+ className="form-field"
+ >
+ <SubmitButton
+ id="change-password"
+ >
+ update_verb
+ </SubmitButton>
+ </div>
+ </form>
+</section>
+`;
my_profile.groups=Groups
my_profile.scm_accounts=SCM Accounts
my_profile.scm_accounts.tooltip=SCM accounts are used for automatic issue assignment. Login and email are automatically considered as SCM account.
-my_profile.password.title=Change password
+my_profile.password.title=Enter a new password
my_profile.password.old=Old Password
my_profile.password.new=New Password
my_profile.password.confirm=Confirm Password
-my_profile.password.submit=Change password
my_profile.password.changed=The password has been changed!
my_profile.notifications.submit=Save changes
my_profile.overall_notifications.title=Overall notifications
my_account.add_project.bitbucket=Bitbucket
my_account.add_project.github=GitHub
my_account.add_project.gitlab=GitLab
+my_account.reset_password=Update your password
+my_account.reset_password.explain=This account should not use the default password.
my_account.create_new_project_portfolio_or_application=Analyze new project / Create new portfolio or application