Merged in feature/ui-AdminPasswordChange (pull request #102)

Allows admin user to set other users' passwords
This commit is contained in:
Philipp Czora
2018-11-06 15:31:04 +00:00
9 changed files with 326 additions and 26 deletions

View File

@@ -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.",

View File

@@ -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<Props, State> {
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 = (
<Notification
type={"success"}
children={t("password.set-password-successful")}
onClose={() => this.onClose()}
/>
);
} else if (error) {
message = <ErrorNotification error={error} />;
}
return (
<form onSubmit={this.submit}>
{message}
<InputField
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={this.state.password ? this.state.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordConfirmationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
/>
<SubmitButton
disabled={!this.passwordIsValid()}
loading={loading}
label={t("user-form.submit")}
/>
</form>
);
}
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);

View File

@@ -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<Props, State> {
mailValidationError: false,
displayNameValidationError: false,
nameValidationError: false,
passwordValidationError: false,
passwordConfirmationError: false,
validatePasswordError: false,
validatePassword: ""
};
@@ -70,7 +70,7 @@ class UserForm extends React.Component<Props, State> {
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<Props, State> {
const user = this.state.user;
let nameField = null;
let passwordFields = null;
if (!this.props.user) {
nameField = (
<InputField
@@ -100,6 +101,28 @@ class UserForm extends React.Component<Props, State> {
helpText={t("help.usernameHelpText")}
/>
);
passwordFields = (
<>
<InputField
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={user ? user.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordConfirmationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
/>
</>
);
}
return (
<form onSubmit={this.submit}>
@@ -120,24 +143,7 @@ class UserForm extends React.Component<Props, State> {
errorMessage={t("validation.mail-invalid")}
helpText={t("help.mailHelpText")}
/>
<InputField
label={t("user.password")}
type="password"
onChange={this.handlePasswordChange}
value={user ? user.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
helpText={t("help.passwordHelpText")}
/>
<InputField
label={t("validation.validatePassword")}
type="password"
onChange={this.handlePasswordValidationChange}
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordValidationError}
errorMessage={t("validation.passwordValidation-invalid")}
helpText={t("help.passwordConfirmHelpText")}
/>
{passwordFields}
<Checkbox
label={t("user.admin")}
onChange={this.handleAdminChange}
@@ -189,7 +195,7 @@ class UserForm extends React.Component<Props, State> {
);
this.setState({
validatePasswordError: !userValidator.isPasswordValid(password),
passwordValidationError: validatePasswordError,
passwordConfirmationError: validatePasswordError,
user: { ...this.state.user, password }
});
};
@@ -201,7 +207,7 @@ class UserForm extends React.Component<Props, State> {
);
this.setState({
validatePassword,
passwordValidationError: !validatePasswordError
passwordConfirmationError: !validatePasswordError
});
};

View File

@@ -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<Props> {
render() {
const { t, passwordUrl } = this.props;
if (!this.hasPermissionToSetPassword()) {
return null;
}
return <NavLink label={t("set-password-button.label")} to={passwordUrl} />;
}
hasPermissionToSetPassword = () => {
return this.props.user._links.password;
};
}
export default translate("users")(ChangePasswordNavLink);

View File

@@ -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(
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const user = {
_links: {
password: {
href: "/password"
}
}
};
const navLink = shallow(
<ChangePasswordNavLink user={user} passwordUrl="/user/password" />
);
expect(navLink.text()).not.toBe("");
});

View File

@@ -1,2 +1,3 @@
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
export { default as EditUserNavLink } from "./EditUserNavLink";
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";

View File

@@ -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 };
});
}

View File

@@ -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();
});
});
});

View File

@@ -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<Props> {
path={`${url}/edit`}
component={() => <EditUser user={user} />}
/>
<Route
path={`${url}/password`}
component={() => <SetUserPassword user={user} />}
/>
</div>
<div className="column">
<Navigation>
@@ -106,6 +115,10 @@ class SingleUser extends React.Component<Props> {
label={t("single-user.information-label")}
/>
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
<SetPasswordNavLink
user={user}
passwordUrl={`${url}/password`}
/>
</Section>
<Section label={t("single-user.actions-label")}>
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} />