mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 05:55:44 +01:00
Refactoring
This commit is contained in:
@@ -1,10 +1,8 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { InputField } from "@scm-manager/ui-components";
|
|
||||||
import { compose } from "redux";
|
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import * as userValidator from "./userValidation";
|
import InputField from "./InputField";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
password: string,
|
password: string,
|
||||||
@@ -14,6 +12,7 @@ type State = {
|
|||||||
};
|
};
|
||||||
type Props = {
|
type Props = {
|
||||||
passwordChanged: string => void,
|
passwordChanged: string => void,
|
||||||
|
passwordValidator?: string => boolean,
|
||||||
// Context props
|
// Context props
|
||||||
t: string => string
|
t: string => string
|
||||||
};
|
};
|
||||||
@@ -43,27 +42,36 @@ class PasswordConfirmation extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InputField
|
<InputField
|
||||||
label={t("user.password")}
|
label={t("password.label")}
|
||||||
type="password"
|
type="password"
|
||||||
onChange={this.handlePasswordChange}
|
onChange={this.handlePasswordChange}
|
||||||
value={this.state.password ? this.state.password : ""}
|
value={this.state.password ? this.state.password : ""}
|
||||||
validationError={!this.state.passwordValid}
|
validationError={!this.state.passwordValid}
|
||||||
errorMessage={t("validation.password-invalid")}
|
errorMessage={t("password.passwordInvalid")}
|
||||||
helpText={t("help.passwordHelpText")}
|
helpText={t("password.passwordHelpText")}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
label={t("validation.validatePassword")}
|
label={t("password.confirmPassword")}
|
||||||
type="password"
|
type="password"
|
||||||
onChange={this.handlePasswordValidationChange}
|
onChange={this.handlePasswordValidationChange}
|
||||||
value={this.state ? this.state.confirmedPassword : ""}
|
value={this.state ? this.state.confirmedPassword : ""}
|
||||||
validationError={this.state.passwordConfirmationFailed}
|
validationError={this.state.passwordConfirmationFailed}
|
||||||
errorMessage={t("validation.passwordValidation-invalid")}
|
errorMessage={t("password.passwordConfirmFailed")}
|
||||||
helpText={t("help.passwordConfirmHelpText")}
|
helpText={t("password.passwordConfirmHelpText")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validatePassword = password => {
|
||||||
|
const { passwordValidator } = this.props;
|
||||||
|
if (passwordValidator) {
|
||||||
|
return passwordValidator(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.length >= 6 && password.length < 32;
|
||||||
|
};
|
||||||
|
|
||||||
handlePasswordValidationChange = (confirmedPassword: string) => {
|
handlePasswordValidationChange = (confirmedPassword: string) => {
|
||||||
const passwordConfirmed = this.state.password === confirmedPassword;
|
const passwordConfirmed = this.state.password === confirmedPassword;
|
||||||
|
|
||||||
@@ -82,7 +90,7 @@ class PasswordConfirmation extends React.Component<Props, State> {
|
|||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
passwordValid: userValidator.isPasswordValid(password),
|
passwordValid: this.validatePassword(password),
|
||||||
passwordConfirmationFailed,
|
passwordConfirmationFailed,
|
||||||
password: password
|
password: password
|
||||||
},
|
},
|
||||||
@@ -101,4 +109,4 @@ class PasswordConfirmation extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(translate("users"))(PasswordConfirmation);
|
export default translate("commons")(PasswordConfirmation);
|
||||||
@@ -5,4 +5,5 @@ export { default as Checkbox } from "./Checkbox.js";
|
|||||||
export { default as InputField } from "./InputField.js";
|
export { default as InputField } from "./InputField.js";
|
||||||
export { default as Select } from "./Select.js";
|
export { default as Select } from "./Select.js";
|
||||||
export { default as Textarea } from "./Textarea.js";
|
export { default as Textarea } from "./Textarea.js";
|
||||||
|
export { default as PasswordConfirmation } from "./PasswordConfirmation.js";
|
||||||
|
|
||||||
|
|||||||
@@ -49,5 +49,13 @@
|
|||||||
"error-subtitle": "Cannot display profile",
|
"error-subtitle": "Cannot display profile",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error-message": "'me' is undefined"
|
"error-message": "'me' is undefined"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "Password",
|
||||||
|
"passwordHelpText": "Plain text password of the user.",
|
||||||
|
"passwordConfirmHelpText": "Repeat the password for confirmation.",
|
||||||
|
"confirmPassword": "Confirm password",
|
||||||
|
"passwordInvalid": "Password has to be between 6 and 32 characters",
|
||||||
|
"passwordConfirmFailed": "Passwords have to be identical"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,22 +50,15 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"mail-invalid": "This email is invalid",
|
"mail-invalid": "This email is invalid",
|
||||||
"name-invalid": "This name is invalid",
|
"name-invalid": "This name is invalid",
|
||||||
"displayname-invalid": "This displayname 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 identical",
|
|
||||||
"validatePassword": "Confirm password"
|
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"current-password": "Current password",
|
|
||||||
"set-password-successful": "Password successfully set"
|
"set-password-successful": "Password successfully set"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"usernameHelpText": "Unique name of the user.",
|
"usernameHelpText": "Unique name of the user.",
|
||||||
"displayNameHelpText": "Display name of the user.",
|
"displayNameHelpText": "Display name of the user.",
|
||||||
"mailHelpText": "Email address of the user.",
|
"mailHelpText": "Email address of the user.",
|
||||||
"currentPasswordHelpText": "Enter your current password",
|
|
||||||
"passwordHelpText": "Plain text password of the user.",
|
|
||||||
"passwordConfirmHelpText": "Repeat the password for confirmation.",
|
|
||||||
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
|
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
|
||||||
"activeHelpText": "Activate or deactive the user."
|
"activeHelpText": "Activate or deactive the user."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import {
|
|||||||
SubmitButton,
|
SubmitButton,
|
||||||
Notification,
|
Notification,
|
||||||
ErrorNotification,
|
ErrorNotification,
|
||||||
InputField
|
InputField,
|
||||||
} from "../../../scm-ui-components/packages/ui-components/src/index";
|
PasswordConfirmation
|
||||||
|
} from "@scm-manager/ui-components";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import { updatePassword } from "../users/components/changePassword";
|
|
||||||
import PasswordConfirmation from "../users/components/PasswordConfirmation";
|
|
||||||
import type { Me } from "@scm-manager/ui-types";
|
import type { Me } from "@scm-manager/ui-types";
|
||||||
|
import { changePassword } from "../modules/changePassword";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
me: Me,
|
me: Me,
|
||||||
@@ -69,7 +69,7 @@ class ChangeUserPassword extends React.Component<Props, State> {
|
|||||||
if (this.state.password) {
|
if (this.state.password) {
|
||||||
const { oldPassword, password } = this.state;
|
const { oldPassword, password } = this.state;
|
||||||
this.setLoadingState();
|
this.setLoadingState();
|
||||||
updatePassword(this.props.me._links.password.href, oldPassword, password)
|
changePassword(this.props.me._links.password.href, oldPassword, password)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
this.setErrorState(result.error);
|
this.setErrorState(result.error);
|
||||||
|
|||||||
19
scm-ui/src/modules/changePassword.js
Normal file
19
scm-ui/src/modules/changePassword.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// @flow
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
export const CONTENT_TYPE_PASSWORD_CHANGE =
|
||||||
|
"application/vnd.scmm-passwordChange+json;v=2";
|
||||||
|
export function changePassword(
|
||||||
|
url: string,
|
||||||
|
oldPassword: string,
|
||||||
|
newPassword: string
|
||||||
|
) {
|
||||||
|
return apiClient
|
||||||
|
.put(url, { oldPassword, newPassword }, CONTENT_TYPE_PASSWORD_CHANGE)
|
||||||
|
.then(response => {
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
return { error: err };
|
||||||
|
});
|
||||||
|
}
|
||||||
25
scm-ui/src/modules/changePassword.test.js
Normal file
25
scm-ui/src/modules/changePassword.test.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
import { changePassword, CONTENT_TYPE_PASSWORD_CHANGE } from "./changePassword";
|
||||||
|
|
||||||
|
describe("change password", () => {
|
||||||
|
const CHANGE_PASSWORD_URL = "/me/password";
|
||||||
|
const oldPassword = "old";
|
||||||
|
const newPassword = "new";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update password", done => {
|
||||||
|
fetchMock.put("/api/v2" + CHANGE_PASSWORD_URL, 204, {
|
||||||
|
headers: { "content-type": CONTENT_TYPE_PASSWORD_CHANGE }
|
||||||
|
});
|
||||||
|
|
||||||
|
changePassword(CHANGE_PASSWORD_URL, oldPassword, newPassword).then(
|
||||||
|
content => {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,11 +4,11 @@ import type { User } from "@scm-manager/ui-types";
|
|||||||
import {
|
import {
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
Notification,
|
Notification,
|
||||||
ErrorNotification
|
ErrorNotification,
|
||||||
|
PasswordConfirmation
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import { setPassword } from "./changePassword";
|
import { setPassword } from "./setPassword";
|
||||||
import PasswordConfirmation from "./PasswordConfirmation";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: User,
|
user: User,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { User } from "@scm-manager/ui-types";
|
|||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
InputField,
|
InputField,
|
||||||
|
PasswordConfirmation,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
validation as validator
|
validation as validator
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
@@ -21,10 +22,7 @@ type State = {
|
|||||||
user: User,
|
user: User,
|
||||||
mailValidationError: boolean,
|
mailValidationError: boolean,
|
||||||
nameValidationError: boolean,
|
nameValidationError: boolean,
|
||||||
displayNameValidationError: boolean,
|
displayNameValidationError: boolean
|
||||||
passwordConfirmationError: boolean,
|
|
||||||
validatePasswordError: boolean,
|
|
||||||
validatePassword: string
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserForm extends React.Component<Props, State> {
|
class UserForm extends React.Component<Props, State> {
|
||||||
@@ -43,10 +41,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
},
|
},
|
||||||
mailValidationError: false,
|
mailValidationError: false,
|
||||||
displayNameValidationError: false,
|
displayNameValidationError: false,
|
||||||
nameValidationError: false,
|
nameValidationError: false
|
||||||
passwordConfirmationError: false,
|
|
||||||
validatePasswordError: false,
|
|
||||||
validatePassword: ""
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +62,13 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
isValid = () => {
|
isValid = () => {
|
||||||
const user = this.state.user;
|
const user = this.state.user;
|
||||||
return !(
|
return !(
|
||||||
this.state.validatePasswordError ||
|
|
||||||
this.state.nameValidationError ||
|
this.state.nameValidationError ||
|
||||||
this.state.mailValidationError ||
|
this.state.mailValidationError ||
|
||||||
this.state.passwordConfirmationError ||
|
|
||||||
this.state.displayNameValidationError ||
|
this.state.displayNameValidationError ||
|
||||||
this.isFalsy(user.name) ||
|
this.isFalsy(user.name) ||
|
||||||
this.isFalsy(user.displayName)
|
this.isFalsy(user.displayName) ||
|
||||||
|
this.isFalsy(user.mail) ||
|
||||||
|
this.isFalsy(user.password)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +84,6 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
const user = this.state.user;
|
const user = this.state.user;
|
||||||
|
|
||||||
let nameField = null;
|
let nameField = null;
|
||||||
let passwordFields = null;
|
|
||||||
if (!this.props.user) {
|
if (!this.props.user) {
|
||||||
nameField = (
|
nameField = (
|
||||||
<InputField
|
<InputField
|
||||||
@@ -101,28 +95,6 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
helpText={t("help.usernameHelpText")}
|
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 (
|
return (
|
||||||
<form onSubmit={this.submit}>
|
<form onSubmit={this.submit}>
|
||||||
@@ -143,7 +115,7 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
errorMessage={t("validation.mail-invalid")}
|
errorMessage={t("validation.mail-invalid")}
|
||||||
helpText={t("help.mailHelpText")}
|
helpText={t("help.mailHelpText")}
|
||||||
/>
|
/>
|
||||||
{passwordFields}
|
<PasswordConfirmation passwordChanged={this.handlePasswordChange} />
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={t("user.admin")}
|
label={t("user.admin")}
|
||||||
onChange={this.handleAdminChange}
|
onChange={this.handleAdminChange}
|
||||||
@@ -189,32 +161,11 @@ class UserForm extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handlePasswordChange = (password: string) => {
|
handlePasswordChange = (password: string) => {
|
||||||
const validatePasswordError = !this.checkPasswords(
|
|
||||||
password,
|
|
||||||
this.state.validatePassword
|
|
||||||
);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
validatePasswordError: !userValidator.isPasswordValid(password),
|
|
||||||
passwordConfirmationError: validatePasswordError,
|
|
||||||
user: { ...this.state.user, password }
|
user: { ...this.state.user, password }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePasswordValidationChange = (validatePassword: string) => {
|
|
||||||
const validatePasswordError = this.checkPasswords(
|
|
||||||
this.state.user.password,
|
|
||||||
validatePassword
|
|
||||||
);
|
|
||||||
this.setState({
|
|
||||||
validatePassword,
|
|
||||||
passwordConfirmationError: !validatePasswordError
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPasswords = (password1: string, password2: string) => {
|
|
||||||
return password1 === password2;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAdminChange = (admin: boolean) => {
|
handleAdminChange = (admin: boolean) => {
|
||||||
this.setState({ user: { ...this.state.user, admin } });
|
this.setState({ user: { ...this.state.user, admin } });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import { apiClient } from "@scm-manager/ui-components";
|
|
||||||
const CONTENT_TYPE_PASSWORD_OVERWRITE =
|
|
||||||
"application/vnd.scmm-passwordOverwrite+json;v=2";
|
|
||||||
const CONTENT_TYPE_PASSWORD_CHANGE =
|
|
||||||
"application/vnd.scmm-passwordChange+json;v=2";
|
|
||||||
|
|
||||||
export function setPassword(url: string, password: string) {
|
|
||||||
return apiClient
|
|
||||||
.put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE)
|
|
||||||
.then(response => {
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
return { error: err };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updatePassword(
|
|
||||||
url: string,
|
|
||||||
oldPassword: string,
|
|
||||||
newPassword: string
|
|
||||||
) {
|
|
||||||
return apiClient
|
|
||||||
.put(url, { oldPassword, newPassword }, CONTENT_TYPE_PASSWORD_CHANGE)
|
|
||||||
.then(response => {
|
|
||||||
return response;
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
return { error: err };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import fetchMock from "fetch-mock";
|
|
||||||
import { setPassword, updatePassword } from "./changePassword";
|
|
||||||
|
|
||||||
describe("password change", () => {
|
|
||||||
const SET_PASSWORD_URL = "/users/testuser/password";
|
|
||||||
const CHANGE_PASSWORD_URL = "/me/password";
|
|
||||||
const oldPassword = "old";
|
|
||||||
const newPassword = "testpw123";
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
fetchMock.reset();
|
|
||||||
fetchMock.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Verify content type
|
|
||||||
it("should set password", done => {
|
|
||||||
fetchMock.put("/api/v2" + SET_PASSWORD_URL, 204);
|
|
||||||
|
|
||||||
setPassword(SET_PASSWORD_URL, newPassword).then(content => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Verify content type
|
|
||||||
it("should update password", done => {
|
|
||||||
fetchMock.put("/api/v2" + CHANGE_PASSWORD_URL, 204);
|
|
||||||
|
|
||||||
updatePassword(CHANGE_PASSWORD_URL, oldPassword, newPassword).then(
|
|
||||||
content => {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
15
scm-ui/src/users/components/setPassword.js
Normal file
15
scm-ui/src/users/components/setPassword.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//@flow
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
export const CONTENT_TYPE_PASSWORD_OVERWRITE =
|
||||||
|
"application/vnd.scmm-passwordOverwrite+json;v=2";
|
||||||
|
|
||||||
|
export function setPassword(url: string, password: string) {
|
||||||
|
return apiClient
|
||||||
|
.put(url, { newPassword: password }, CONTENT_TYPE_PASSWORD_OVERWRITE)
|
||||||
|
.then(response => {
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
return { error: err };
|
||||||
|
});
|
||||||
|
}
|
||||||
25
scm-ui/src/users/components/setPassword.test.js
Normal file
25
scm-ui/src/users/components/setPassword.test.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//@flow
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
import { CONTENT_TYPE_PASSWORD_OVERWRITE, setPassword } from "./setPassword";
|
||||||
|
|
||||||
|
describe("password change", () => {
|
||||||
|
const SET_PASSWORD_URL = "/users/testuser/password";
|
||||||
|
const newPassword = "testpw123";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set password", done => {
|
||||||
|
fetchMock.put("/api/v2" + SET_PASSWORD_URL, 204, {
|
||||||
|
headers: {
|
||||||
|
"content-type": CONTENT_TYPE_PASSWORD_OVERWRITE
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setPassword(SET_PASSWORD_URL, newPassword).then(content => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user