Merge with develop

This commit is contained in:
Sebastian Sdorra
2020-11-04 08:22:41 +01:00
125 changed files with 2108 additions and 851 deletions

View File

@@ -50436,7 +50436,7 @@ exports[`Storyshots RepositoryEntry Avatar EP 1`] = `
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
href="/repo/hitchhiker/heartOfGold/branches"
href="/repo/hitchhiker/heartOfGold/branches/"
onClick={[Function]}
>
<i
@@ -50550,7 +50550,7 @@ exports[`Storyshots RepositoryEntry Before Title EP 1`] = `
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
href="/repo/hitchhiker/heartOfGold/branches"
href="/repo/hitchhiker/heartOfGold/branches/"
onClick={[Function]}
>
<i
@@ -50661,7 +50661,7 @@ exports[`Storyshots RepositoryEntry Default 1`] = `
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
href="/repo/hitchhiker/heartOfGold/branches"
href="/repo/hitchhiker/heartOfGold/branches/"
onClick={[Function]}
>
<i
@@ -50772,7 +50772,7 @@ exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
>
<a
className="RepositoryEntryLink__PointerEventsLink-sc-1hpqj0w-0 iZuqoP level-item"
href="/repo/hitchhiker/heartOfGold/branches"
href="/repo/hitchhiker/heartOfGold/branches/"
onClick={[Function]}
>
<i

View File

@@ -34,6 +34,7 @@ type State = {
type Props = WithTranslation & {
passwordChanged: (p1: string, p2: boolean) => void;
passwordValidator?: (p: string) => boolean;
onReturnPressed?: () => void;
};
class PasswordConfirmation extends React.Component<Props, State> {
@@ -57,7 +58,7 @@ class PasswordConfirmation extends React.Component<Props, State> {
}
render() {
const { t } = this.props;
const { t, onReturnPressed } = this.props;
return (
<div className="columns is-multiline">
<div className="column is-half">
@@ -78,6 +79,7 @@ class PasswordConfirmation extends React.Component<Props, State> {
value={this.state ? this.state.confirmedPassword : ""}
validationError={this.state.passwordConfirmationFailed}
errorMessage={t("password.passwordConfirmFailed")}
onReturnPressed={onReturnPressed}
/>
</div>
</div>

View File

@@ -44,7 +44,7 @@ class RepositoryEntry extends React.Component<Props> {
renderBranchesLink = (repository: Repository, repositoryLink: string) => {
if (repository._links["branches"]) {
return <RepositoryEntryLink icon="code-branch" to={repositoryLink + "/branches"} />;
return <RepositoryEntryLink icon="code-branch" to={repositoryLink + "/branches/"} />;
}
return null;
};

View File

@@ -28,6 +28,12 @@ export const isNameValid = (name: string) => {
return nameRegex.test(name);
};
export const branchRegex = /^[\w-,;\]{}@&+=$#`|<>]([\w-,;\]{}@&+=$#`|<>/.]*[\w-,;\]{}@&+=$#`|<>])?$/;
export const isBranchValid = (name: string) => {
return branchRegex.test(name);
};
const mailRegex = /^[ -~]+@[A-Za-z0-9][\w\-.]*\.[A-Za-z0-9][A-Za-z0-9-]+$/;
export const isMailValid = (mail: string) => {

View File

@@ -14,7 +14,7 @@
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"file-loader": "^4.2.0",
"mini-css-extract-plugin": "^0.11.0",
"mini-css-extract-plugin": "^0.12.0",
"mustache": "^3.1.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"react-refresh": "^0.8.0",

View File

@@ -45,6 +45,7 @@ export type Config = {
pluginUrl: string;
loginAttemptLimitTimeout: number;
enabledXsrfProtection: boolean;
enabledUserConverter: boolean;
namespaceStrategy: string;
loginInfoUrl: string;
releaseFeedUrl: string;

View File

@@ -39,5 +39,6 @@ export type User = {
type?: string;
creationDate?: string;
lastModified?: string;
external: boolean;
_links: Links;
};

View File

@@ -49,6 +49,7 @@
"release-feed-url": "Release Feed URL",
"mail-domain-name": "Fallback E-Mail Domain Name",
"enabled-xsrf-protection": "XSRF Protection aktivieren",
"enabled-user-converter": "Benutzer Konverter aktivieren",
"namespace-strategy": "Namespace Strategie",
"login-info-url": "Login Info URL"
},
@@ -81,6 +82,7 @@
"proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.",
"proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.",
"enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.",
"enabledUserConverterHelpText": "Benutzer Konverter aktivieren. Interne Benutzer werden beim Einloggen über ein Fremdsystem zu externen Benutzern konvertiert.",
"nameSpaceStrategyHelpText": "Strategie für Namespaces.",
"loginInfoUrlHelpText": "URL zu der Login Information (Plugin und Feature Tipps auf der Login Seite). Um die Login Information zu deaktivieren, kann das Feld leer gelassen werden."
}

View File

@@ -6,6 +6,7 @@
"password": "Passwort",
"active": "Aktiv",
"inactive": "Inaktiv",
"externalFlag": "Extern",
"type": "Typ",
"creationDate": "Erstellt",
"lastModified": "Zuletzt bearbeitet"
@@ -20,7 +21,8 @@
"displayNameHelpText": "Anzeigename des Benutzers",
"mailHelpText": "E-Mail Adresse des Benutzers",
"adminHelpText": "Ein Administrator kann Repositories, Gruppen und Benutzer erstellen, bearbeiten und löschen.",
"activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers"
"activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers",
"externalFlagHelpText": "Der Benutzer wird über ein Fremdsystem verwaltet."
},
"users": {
"title": "Benutzer",
@@ -61,7 +63,17 @@
},
"userForm": {
"subtitle": "Benutzer bearbeiten",
"button": "Speichern"
"userIsInternal": "Der Benutzer wird intern vom SCM-Manager verwaltet",
"userIsExternal": "Der Benutzer wird von einem externen System verwaltet",
"button": {
"submit": "Speichern",
"convertToExternal": "Zu externem Benutzer konvertieren",
"convertToInternal": "Zu internem Benutzer konvertieren"
},
"modal": {
"passwordRequired": "Neues Passwort für internen Benutzer setzen",
"convertToInternal": "Zu internem Benutzer konvertieren"
}
},
"publicKey": {
"noStoredKeys": "Es wurden keine Schlüssel gefunden.",

View File

@@ -49,6 +49,7 @@
"release-feed-url": "Release Feed URL",
"mail-domain-name": "Fallback Mail Domain Name",
"enabled-xsrf-protection": "Enabled XSRF Protection",
"enabled-user-converter": "Enabled User Converter",
"namespace-strategy": "Namespace Strategy",
"login-info-url": "Login Info URL"
},
@@ -81,6 +82,7 @@
"proxyUserHelpText": "The username for the proxy server authentication.",
"proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.",
"enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.",
"enabledUserConverterHelpText": "Enable User Converter. Internal users will automatically be converted to external on their first login using an external system.",
"nameSpaceStrategyHelpText": "The namespace strategy.",
"loginInfoUrlHelpText": "URL to login information (plugin and feature tips at login page). If this is omitted, no login information will be displayed."
}

View File

@@ -6,6 +6,7 @@
"password": "Password",
"active": "Active",
"inactive": "Inactive",
"externalFlag": "External",
"type": "Type",
"creationDate": "Creation Date",
"lastModified": "Last Modified"
@@ -20,7 +21,8 @@
"displayNameHelpText": "Display name of the user.",
"mailHelpText": "Email address of the user.",
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
"activeHelpText": "Activate or deactivate the user."
"activeHelpText": "Activate or deactivate the user.",
"externalFlagHelpText": "This user is managed by an external system."
},
"users": {
"title": "Users",
@@ -61,7 +63,17 @@
},
"userForm": {
"subtitle": "Edit User",
"button": "Submit"
"userIsInternal": "This user is managed internally by SCM-Manager",
"userIsExternal": "This user is managed by an external system",
"button": {
"submit": "Submit",
"convertToExternal": "Convert user to external",
"convertToInternal": "Convert user to internal"
},
"modal": {
"passwordRequired": "Set new password for internal user",
"convertToInternal": "Convert to internal"
}
},
"publicKey": {
"noStoredKeys": "No keys found.",

View File

@@ -72,6 +72,7 @@ class ConfigForm extends React.Component<Props, State> {
pluginUrl: "",
loginAttemptLimitTimeout: 0,
enabledXsrfProtection: true,
enabledUserConverter: false,
namespaceStrategy: "",
loginInfoUrl: "",
_links: {}
@@ -146,6 +147,7 @@ class ConfigForm extends React.Component<Props, State> {
releaseFeedUrl={config.releaseFeedUrl}
mailDomainName={config.mailDomainName}
enabledXsrfProtection={config.enabledXsrfProtection}
enabledUserConverter={config.enabledUserConverter}
namespaceStrategy={config.namespaceStrategy}
onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name)}
hasUpdatePermission={configUpdatePermission}

View File

@@ -38,6 +38,7 @@ type Props = WithTranslation & {
releaseFeedUrl: string;
mailDomainName: string;
enabledXsrfProtection: boolean;
enabledUserConverter: boolean;
namespaceStrategy: string;
namespaceStrategies?: NamespaceStrategies;
onChange: (p1: boolean, p2: any, p3: string) => void;
@@ -54,6 +55,7 @@ class GeneralSettings extends React.Component<Props> {
releaseFeedUrl,
mailDomainName,
enabledXsrfProtection,
enabledUserConverter,
anonymousMode,
namespaceStrategy,
hasUpdatePermission,
@@ -140,6 +142,18 @@ class GeneralSettings extends React.Component<Props> {
helpText={t("help.releaseFeedUrlHelpText")}
/>
</div>
<div className="column is-half">
<Checkbox
label={t("general-settings.enabled-user-converter")}
onChange={this.handleEnabledUserConverterChange}
checked={enabledUserConverter}
title={t("general-settings.enabled-user-converter")}
disabled={!hasUpdatePermission}
helpText={t("help.enabledUserConverterHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<InputField
label={t("general-settings.mail-domain-name")}
@@ -163,6 +177,9 @@ class GeneralSettings extends React.Component<Props> {
handleEnabledXsrfProtectionChange = (value: boolean) => {
this.props.onChange(true, value, "enabledXsrfProtection");
};
handleEnabledUserConverterChange = (value: boolean) => {
this.props.onChange(true, value, "enabledUserConverter");
};
handleAnonymousMode = (value: string) => {
this.props.onChange(true, value, "anonymousMode");
};

View File

@@ -124,15 +124,13 @@ class BranchForm extends React.Component<Props, State> {
handleSourceChange = (source: string) => {
this.setState({
...this.state,
source
});
};
handleNameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isNameValid(name),
...this.state,
nameValidationError: !validator.isBranchValid(name),
name
});
};

View File

@@ -0,0 +1,137 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useState } from "react";
import {
Button,
Modal,
PasswordConfirmation,
SubmitButton,
ErrorNotification,
Level
} from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { Link, User } from "@scm-manager/ui-types";
import { convertToExternal, convertToInternal } from "./convertUser";
import styled from "styled-components";
const ExternalDescription = styled.div`
display: flex;
align-items: center;
font-weight: 400;
`;
type Props = {
user: User;
fetchUser: (user: User) => void;
};
const UserConverter: FC<Props> = ({ user, fetchUser }) => {
const [t] = useTranslation("users");
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [password, setPassword] = useState("");
const [passwordValid, setPasswordValid] = useState(false);
const [error, setError] = useState<Error | undefined>();
const toInternal = () => {
convertToInternal((user._links.convertToInternal as Link).href, password)
.then(() => fetchUser(user))
.then(() => setShowPasswordModal(false))
.catch(setError);
};
const toExternal = () => {
convertToExternal((user._links.convertToExternal as Link).href)
.then(() => fetchUser(user))
.catch(setError);
};
const changePassword = (password: string, valid: boolean) => {
setPassword(password);
setPasswordValid(valid);
};
const getUserExternalDescription = () => {
if (user.external) {
return t("userForm.userIsExternal");
} else {
return t("userForm.userIsInternal");
}
};
const getConvertButton = () => {
if (user.external) {
return (
<Button
label={t("userForm.button.convertToInternal")}
action={() => setShowPasswordModal(true)}
icon="exchange-alt"
className="is-pulled-right"
/>
);
} else {
return <Button label={t("userForm.button.convertToExternal")} action={() => toExternal()} icon="exchange-alt" />;
}
};
const onReturnPressed = () => {
if (password && passwordValid) {
toInternal();
}
};
const passwordChangeField = (
<PasswordConfirmation passwordChanged={changePassword} onReturnPressed={onReturnPressed} />
);
const passwordModal = (
<Modal
body={passwordChangeField}
closeFunction={() => setShowPasswordModal(false)}
active={showPasswordModal}
title={t("userForm.modal.passwordRequired")}
footer={
<SubmitButton
action={() => password && passwordValid && toInternal()}
disabled={!passwordValid}
scrollToTop={false}
label={t("userForm.modal.convertToInternal")}
/>
}
/>
);
return (
<div>
{showPasswordModal && passwordModal}
{error && <ErrorNotification error={error} />}
<div className="columns is-multiline">
<ExternalDescription className="column is-half">{getUserExternalDescription()}</ExternalDescription>
<div className="column is-half">
<Level right={getConvertButton()} />
</div>
</div>
</div>
);
};
export default UserConverter;

View File

@@ -23,9 +23,10 @@
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { User } from "@scm-manager/ui-types";
import { Link, User } from "@scm-manager/ui-types";
import {
Checkbox,
ErrorNotification,
InputField,
Level,
PasswordConfirmation,
@@ -47,6 +48,7 @@ type State = {
nameValidationError: boolean;
displayNameValidationError: boolean;
passwordValid: boolean;
error?: Error;
};
class UserForm extends React.Component<Props, State> {
@@ -60,6 +62,7 @@ class UserForm extends React.Component<Props, State> {
mail: "",
password: "",
active: true,
external: false,
_links: {}
},
mailValidationError: false,
@@ -80,14 +83,10 @@ class UserForm extends React.Component<Props, State> {
}
}
isFalsy(value) {
return !value;
}
createUserComponentsAreInvalid = () => {
const user = this.state.user;
if (!this.props.user) {
return this.state.nameValidationError || this.isFalsy(user.name) || !this.state.passwordValid;
return this.state.nameValidationError || !user.name || (!user.external && !this.state.passwordValid);
} else {
return false;
}
@@ -99,37 +98,40 @@ class UserForm extends React.Component<Props, State> {
return (
this.props.user.displayName === user.displayName &&
this.props.user.mail === user.mail &&
this.props.user.active === user.active
this.props.user.active === user.active &&
this.props.user.external === user.external
);
} else {
return false;
}
};
isValid = () => {
const user = this.state.user;
return !(
isInvalid = () => {
const { user } = this.state;
return (
this.createUserComponentsAreInvalid() ||
this.editUserComponentsAreUnchanged() ||
this.state.mailValidationError ||
this.state.displayNameValidationError ||
this.isFalsy(user.displayName)
this.state.nameValidationError ||
!user.displayName
);
};
submit = (event: Event) => {
event.preventDefault();
if (this.isValid()) {
if (!this.isInvalid()) {
this.props.submitForm(this.state.user);
}
};
render() {
const { loading, t } = this.props;
const user = this.state.user;
const { user, error } = this.state;
const passwordChangeField = <PasswordConfirmation passwordChanged={this.handlePasswordChange} />;
let nameField = null;
let passwordChangeField = null;
let subtitle = null;
if (!this.props.user) {
// create new user
@@ -145,8 +147,6 @@ class UserForm extends React.Component<Props, State> {
/>
</div>
);
passwordChangeField = <PasswordConfirmation passwordChanged={this.handlePasswordChange} />;
} else {
// edit existing user
subtitle = <Subtitle subtitle={t("userForm.subtitle")} />;
@@ -179,18 +179,37 @@ class UserForm extends React.Component<Props, State> {
/>
</div>
</div>
{passwordChangeField}
<div className="columns">
<div className="column">
<Checkbox
label={t("user.active")}
onChange={this.handleActiveChange}
checked={user ? user.active : false}
helpText={t("help.activeHelpText")}
/>
</div>
</div>
<Level right={<SubmitButton disabled={!this.isValid()} loading={loading} label={t("userForm.button")} />} />
{!this.props.user && (
<>
<div className="columns">
<div className="column">
<Checkbox
label={t("user.externalFlag")}
onChange={this.handleExternalChange}
checked={user.external}
helpText={t("help.externalFlagHelpText")}
/>
</div>
</div>
</>
)}
{!user.external && (
<>
{!this.props.user && passwordChangeField}
<div className="columns">
<div className="column">
<Checkbox
label={t("user.active")}
onChange={this.handleActiveChange}
checked={user ? user.active : false}
helpText={t("help.activeHelpText")}
/>
</div>
</div>
</>
)}
{error && <ErrorNotification error={error} />}
<Level right={<SubmitButton disabled={this.isInvalid()} loading={loading} label={t("userForm.button.submit")} />} />
</form>
</>
);
@@ -232,7 +251,7 @@ class UserForm extends React.Component<Props, State> {
...this.state.user,
password
},
passwordValid: !this.isFalsy(password) && passwordValid
passwordValid: !!password && passwordValid
});
};
@@ -244,6 +263,15 @@ class UserForm extends React.Component<Props, State> {
}
});
};
handleExternalChange = (external: boolean) => {
this.setState({
user: {
...this.state.user,
external
}
});
};
}
export default withTranslation("users")(UserForm);

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { apiClient } from "@scm-manager/ui-components";
import { CONTENT_TYPE_USER } from "../modules/users";
export function convertToInternal(url: string, newPassword: string) {
return apiClient
.put(
url,
{
newPassword
},
CONTENT_TYPE_USER
)
.then(response => {
return response;
});
}
export function convertToExternal(url: string) {
return apiClient.put(url, {}, CONTENT_TYPE_USER).then(response => {
return response;
});
}

View File

@@ -42,7 +42,8 @@ class ChangePasswordNavLink extends React.Component<Props> {
}
hasPermissionToSetPassword = () => {
return this.props.user._links.password;
const { user } = this.props;
return user._links.password;
};
}

View File

@@ -62,8 +62,10 @@ class Details extends React.Component<Props> {
</td>
</tr>
<tr>
<th>{t("user.type")}</th>
<td>{user.type}</td>
<th>{t("user.externalFlag")}</th>
<td>
<Checkbox checked={!!user.external} />
</td>
</tr>
<tr>
<th>{t("user.creationDate")}</th>

View File

@@ -27,10 +27,17 @@ import { withRouter } from "react-router-dom";
import UserForm from "../components/UserForm";
import DeleteUser from "./DeleteUser";
import { User } from "@scm-manager/ui-types";
import { getModifyUserFailure, isModifyUserPending, modifyUser, modifyUserReset } from "../modules/users";
import {
fetchUserByLink,
getModifyUserFailure,
isModifyUserPending,
modifyUser,
modifyUserReset
} from "../modules/users";
import { History } from "history";
import { ErrorNotification } from "@scm-manager/ui-components";
import { compose } from "redux";
import UserConverter from "../components/UserConverter";
type Props = {
loading: boolean;
@@ -39,6 +46,7 @@ type Props = {
// dispatch functions
modifyUser: (user: User, callback?: () => void) => void;
modifyUserReset: (p: User) => void;
fetchUser: (user: User) => void;
// context objects
user: User;
@@ -64,7 +72,9 @@ class EditUser extends React.Component<Props> {
return (
<div>
<ErrorNotification error={error} />
<UserForm submitForm={user => this.modifyUser(user)} user={user} loading={loading} />
<UserForm submitForm={this.modifyUser} user={user} loading={loading} />
<hr />
<UserConverter user={user} fetchUser={this.props.fetchUser} />
<DeleteUser user={user} />
</div>
);
@@ -87,6 +97,9 @@ const mapDispatchToProps = (dispatch: any) => {
},
modifyUserReset: (user: User) => {
dispatch(modifyUserReset(user));
},
fetchUser: (user: User) => {
dispatch(fetchUserByLink(user));
}
};
};

View File

@@ -56,7 +56,7 @@ export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`;
export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
export const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
// TODO i18n for error messages