mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-09 22:00:20 +01:00
Merged in feature/ui-AdminPasswordChange (pull request #102)
Allows admin user to set other users' passwords
This commit is contained in:
@@ -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.",
|
||||
|
||||
177
scm-ui/src/users/components/SetUserPassword.js
Normal file
177
scm-ui/src/users/components/SetUserPassword.js
Normal 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);
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
28
scm-ui/src/users/components/navLinks/SetPasswordNavLink.js
Normal file
28
scm-ui/src/users/components/navLinks/SetPasswordNavLink.js
Normal 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);
|
||||
@@ -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("");
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
|
||||
export { default as EditUserNavLink } from "./EditUserNavLink";
|
||||
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
|
||||
|
||||
15
scm-ui/src/users/components/updatePassword.js
Normal file
15
scm-ui/src/users/components/updatePassword.js
Normal 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 };
|
||||
});
|
||||
}
|
||||
23
scm-ui/src/users/components/updatePassword.test.js
Normal file
23
scm-ui/src/users/components/updatePassword.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user