diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index 07540134be..7199cb2135 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -29,6 +29,9 @@ "edit-user-button": { "label": "Edit" }, + "set-password-button": { + "label": "Set password" + }, "user-form": { "submit": "Submit" }, @@ -49,8 +52,11 @@ "name-invalid": "This name is invalid", "displayname-invalid": "This displayname is invalid", "password-invalid": "Password has to be between 6 and 32 characters", - "passwordValidation-invalid": "Passwords have to be the same", - "validatePassword": "Please validate password here" + "passwordValidation-invalid": "Passwords have to be identical", + "validatePassword": "Confirm password" + }, + "password": { + "set-password-successful": "Password successfully set" }, "help": { "usernameHelpText": "Unique name of the user.", diff --git a/scm-ui/src/users/components/SetUserPassword.js b/scm-ui/src/users/components/SetUserPassword.js new file mode 100644 index 0000000000..ff859f59bf --- /dev/null +++ b/scm-ui/src/users/components/SetUserPassword.js @@ -0,0 +1,177 @@ +// @flow +import React from "react"; +import type { User } from "@scm-manager/ui-types"; +import { + InputField, + SubmitButton, + Notification, + ErrorNotification +} from "@scm-manager/ui-components"; +import * as userValidator from "./userValidation"; +import { translate } from "react-i18next"; +import { updatePassword } from "./updatePassword"; + +type Props = { + user: User, + t: string => string +}; + +type State = { + password: string, + loading: boolean, + passwordConfirmationError: boolean, + validatePasswordError: boolean, + validatePassword: string, + error?: Error, + passwordChanged: boolean +}; + +class SetUserPassword extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + password: "", + loading: false, + passwordConfirmationError: false, + validatePasswordError: false, + validatePassword: "", + passwordChanged: false + }; + } + + passwordIsValid = () => { + return !( + this.state.validatePasswordError || this.state.passwordConfirmationError + ); + }; + + setLoadingState = () => { + this.setState({ + ...this.state, + loading: true + }); + }; + + setErrorState = (error: Error) => { + this.setState({ + ...this.state, + error: error, + loading: false + }); + }; + + setSuccessfulState = () => { + this.setState({ + ...this.state, + loading: false, + passwordChanged: true, + password: "", + validatePassword: "", + validatePasswordError: false, + passwordConfirmationError: false + }); + }; + + submit = (event: Event) => { + event.preventDefault(); + if (this.passwordIsValid()) { + const { user } = this.props; + const { password } = this.state; + this.setLoadingState(); + updatePassword(user._links.password.href, password) + .then(result => { + if (result.error) { + this.setErrorState(result.error); + } else { + this.setSuccessfulState(); + } + }) + .catch(err => {}); + } + }; + + render() { + const { t } = this.props; + const { loading, passwordChanged, error } = this.state; + + let message = null; + + if (passwordChanged) { + message = ( + this.onClose()} + /> + ); + } else if (error) { + message = ; + } + + return ( +
+ {message} + + + + + ); + } + + handlePasswordChange = (password: string) => { + const validatePasswordError = !this.checkPasswords( + password, + this.state.validatePassword + ); + this.setState({ + validatePasswordError: !userValidator.isPasswordValid(password), + passwordConfirmationError: validatePasswordError, + password: password + }); + }; + + handlePasswordValidationChange = (validatePassword: string) => { + const passwordConfirmed = this.checkPasswords( + this.state.password, + validatePassword + ); + this.setState({ + validatePassword, + passwordConfirmationError: !passwordConfirmed + }); + }; + + checkPasswords = (password1: string, password2: string) => { + return password1 === password2; + }; + + onClose = () => { + this.setState({ + ...this.state, + passwordChanged: false + }); + }; +} + +export default translate("users")(SetUserPassword); diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 80ade5e070..2b8aa7b1e8 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -22,7 +22,7 @@ type State = { mailValidationError: boolean, nameValidationError: boolean, displayNameValidationError: boolean, - passwordValidationError: boolean, + passwordConfirmationError: boolean, validatePasswordError: boolean, validatePassword: string }; @@ -44,7 +44,7 @@ class UserForm extends React.Component { mailValidationError: false, displayNameValidationError: false, nameValidationError: false, - passwordValidationError: false, + passwordConfirmationError: false, validatePasswordError: false, validatePassword: "" }; @@ -70,7 +70,7 @@ class UserForm extends React.Component { this.state.validatePasswordError || this.state.nameValidationError || this.state.mailValidationError || - this.state.passwordValidationError || + this.state.passwordConfirmationError || this.state.displayNameValidationError || this.isFalsy(user.name) || this.isFalsy(user.displayName) @@ -89,6 +89,7 @@ class UserForm extends React.Component { const user = this.state.user; let nameField = null; + let passwordFields = null; if (!this.props.user) { nameField = ( { helpText={t("help.usernameHelpText")} /> ); + passwordFields = ( + <> + + + + ); } return (
@@ -120,24 +143,7 @@ class UserForm extends React.Component { errorMessage={t("validation.mail-invalid")} helpText={t("help.mailHelpText")} /> - - + {passwordFields} { ); this.setState({ validatePasswordError: !userValidator.isPasswordValid(password), - passwordValidationError: validatePasswordError, + passwordConfirmationError: validatePasswordError, user: { ...this.state.user, password } }); }; @@ -201,7 +207,7 @@ class UserForm extends React.Component { ); this.setState({ validatePassword, - passwordValidationError: !validatePasswordError + passwordConfirmationError: !validatePasswordError }); }; diff --git a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js new file mode 100644 index 0000000000..43b7a4b5a4 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { User } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + user: User, + passwordUrl: String +}; + +class ChangePasswordNavLink extends React.Component { + render() { + const { t, passwordUrl } = this.props; + + if (!this.hasPermissionToSetPassword()) { + return null; + } + return ; + } + + hasPermissionToSetPassword = () => { + return this.props.user._links.password; + }; +} + +export default translate("users")(ChangePasswordNavLink); diff --git a/scm-ui/src/users/components/navLinks/SetPasswordNavLink.test.js b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.test.js new file mode 100644 index 0000000000..75ce4e58cf --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPasswordNavLink.test.js @@ -0,0 +1,31 @@ +import React from "react"; +import { shallow } from "enzyme"; +import "../../../tests/enzyme"; +import "../../../tests/i18n"; +import ChangePasswordNavLink from "./SetPasswordNavLink"; + +it("should render nothing, if the password link is missing", () => { + const user = { + _links: {} + }; + + const navLink = shallow( + + ); + expect(navLink.text()).toBe(""); +}); + +it("should render the navLink", () => { + const user = { + _links: { + password: { + href: "/password" + } + } + }; + + const navLink = shallow( + + ); + expect(navLink.text()).not.toBe(""); +}); diff --git a/scm-ui/src/users/components/navLinks/index.js b/scm-ui/src/users/components/navLinks/index.js index a3ccd16a32..a6d8370c00 100644 --- a/scm-ui/src/users/components/navLinks/index.js +++ b/scm-ui/src/users/components/navLinks/index.js @@ -1,2 +1,3 @@ export { default as DeleteUserNavLink } from "./DeleteUserNavLink"; export { default as EditUserNavLink } from "./EditUserNavLink"; +export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; diff --git a/scm-ui/src/users/components/updatePassword.js b/scm-ui/src/users/components/updatePassword.js new file mode 100644 index 0000000000..3915c90bd9 --- /dev/null +++ b/scm-ui/src/users/components/updatePassword.js @@ -0,0 +1,15 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; +const CONTENT_TYPE_PASSWORD_OVERWRITE = + "application/vnd.scmm-passwordOverwrite+json;v=2"; + +export function updatePassword(url: string, password: string) { + return apiClient + .put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE) + .then(response => { + return response; + }) + .catch(err => { + return { error: err }; + }); +} diff --git a/scm-ui/src/users/components/updatePassword.test.js b/scm-ui/src/users/components/updatePassword.test.js new file mode 100644 index 0000000000..a5762406b2 --- /dev/null +++ b/scm-ui/src/users/components/updatePassword.test.js @@ -0,0 +1,23 @@ +//@flow +import fetchMock from "fetch-mock"; +import { updatePassword } from "./updatePassword"; + +describe("get content type", () => { + const PASSWORD_URL = "/users/testuser/password"; + const password = "testpw123"; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should update password", done => { + + fetchMock.put("/api/v2" + PASSWORD_URL, 204); + + updatePassword(PASSWORD_URL, password).then(content => { + + done(); + }); + }); +}); diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index ed5d6df0c3..5f20598962 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -24,9 +24,14 @@ import { getDeleteUserFailure } from "../modules/users"; -import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks"; +import { + DeleteUserNavLink, + EditUserNavLink, + SetPasswordNavLink +} from "./../components/navLinks"; import { translate } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; +import SetUserPassword from "../components/SetUserPassword"; type Props = { name: string, @@ -97,6 +102,10 @@ class SingleUser extends React.Component { path={`${url}/edit`} component={() => } /> + } + />
@@ -106,6 +115,10 @@ class SingleUser extends React.Component { label={t("single-user.information-label")} /> +