Adds support for html in custom login text

This commit is contained in:
mabashian 2021-03-08 10:54:07 -05:00
parent 5c0850b279
commit 3211323a4e
4 changed files with 314 additions and 168 deletions

View File

@ -11103,6 +11103,11 @@
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
"dev": true
},
"klona": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
},
"language-subtag-registry": {
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
@ -11827,6 +11832,11 @@
"dev": true,
"optional": true
},
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
"integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw=="
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -12560,6 +12570,11 @@
"json-parse-better-errors": "^1.0.1"
}
},
"parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
},
"parse5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
@ -15623,6 +15638,106 @@
}
}
},
"sanitize-html": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.2.tgz",
"integrity": "sha512-p7neuskvC8pSurUjdVmbWPXmc9A4+QpOXIL+4gwFC+av5h+lYCXFT8uEneqsFQg/wEA1IH+cKQA60AaQI6p3cg==",
"requires": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^6.0.0",
"is-plain-object": "^5.0.0",
"klona": "^2.0.3",
"parse-srcset": "^1.0.2",
"postcss": "^8.0.2"
},
"dependencies": {
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"dom-serializer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz",
"integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w=="
},
"domhandler": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz",
"integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==",
"requires": {
"domelementtype": "^2.1.0"
}
},
"domutils": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz",
"integrity": "sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==",
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0"
}
},
"entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"htmlparser2": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz",
"integrity": "sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.4.4",
"entities": "^2.0.0"
}
},
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"postcss": {
"version": "8.2.7",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.7.tgz",
"integrity": "sha512-DsVLH3xJzut+VT+rYr0mtvOtpTjSyqDwPf5EZWXcb0uAKfitGpTY9Ec+afi2+TgdN8rWS9Cs88UDYehKo/RvOw==",
"requires": {
"colorette": "^1.2.2",
"nanoid": "^3.1.20",
"source-map": "^0.6.1"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"sanitize.css": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz",

View File

@ -27,6 +27,7 @@
"react-router-dom": "^5.1.2",
"react-virtualized": "^9.21.1",
"rrule": "^2.6.4",
"sanitize-html": "^2.3.2",
"styled-components": "^4.2.0"
},
"devDependencies": {

View File

@ -4,12 +4,20 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Formik } from 'formik';
import styled from 'styled-components';
import sanitizeHtml from 'sanitize-html';
import {
Brand,
LoginMainFooterLinksItem,
LoginForm,
LoginPage as PFLoginPage,
Login as PFLogin,
LoginHeader,
LoginFooter,
LoginMainHeader,
LoginMainBody,
LoginMainFooter,
Tooltip,
} from '@patternfly/react-core';
import {
AzureIcon,
GoogleIcon,
@ -23,7 +31,7 @@ import ErrorDetail from '../../components/ErrorDetail';
const loginLogoSrc = '/static/media/logo-login.svg';
const LoginPage = styled(PFLoginPage)`
const Login = styled(PFLogin)`
& .pf-c-brand {
max-height: 285px;
}
@ -112,171 +120,185 @@ function AWXLogin({ alt, i18n, isAuthenticated }) {
helperText = i18n._(t`There was a problem signing in. Please try again.`);
}
return (
<LoginPage
brandImgSrc={logo}
brandImgAlt={alt || brandName}
loginTitle={
brandName
? i18n._(t`Welcome to Ansible ${brandName}! Please Sign In.`)
: ''
}
textContent={loginInfo}
socialMediaLoginContent={
<>
{socialAuthOptions &&
Object.keys(socialAuthOptions).map(authKey => {
const loginUrl = socialAuthOptions[authKey].login_url;
if (authKey === 'azuread-oauth2') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with Azure AD`)}>
<AzureIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with GitHub`)}>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-org') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(t`Sign in with GitHub Organizations`)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-team') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with GitHub Teams`)}>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-enterprise') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(t`Sign in with GitHub Enterprise`)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-enterprise-org') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(
t`Sign in with GitHub Enterprise Organizations`
)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-enterprise-team') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(t`Sign in with GitHub Enterprise Teams`)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'google-oauth2') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with Google`)}>
<GoogleIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey.startsWith('saml')) {
const samlIDP = authKey.split(':')[1] || null;
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={
samlIDP
? i18n._(t`Sign in with SAML ${samlIDP}`)
: i18n._(t`Sign in with SAML`)
}
>
<UserCircleIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
const HeaderBrand = <Brand src={logo} alt={alt || brandName} />;
const Header = <LoginHeader headerBrand={HeaderBrand} />;
const Footer = (
<LoginFooter
dangerouslySetInnerHTML={{
__html: sanitizeHtml(loginInfo),
}}
/>
);
return null;
})}
</>
}
>
<Formik
initialValues={{
password: '',
username: '',
}}
onSubmit={handleSubmit}
>
{formik => (
<LoginForm
className={authError ? 'pf-m-error' : ''}
helperText={helperText}
isLoginButtonDisabled={isAuthenticating}
isValidPassword={!authError}
isValidUsername={!authError}
loginButtonLabel={i18n._(t`Log In`)}
onChangePassword={val => {
formik.setFieldValue('password', val);
dismissAuthError();
}}
onChangeUsername={val => {
formik.setFieldValue('username', val);
dismissAuthError();
}}
onLoginButtonClick={formik.handleSubmit}
passwordLabel={i18n._(t`Password`)}
passwordValue={formik.values.password}
showHelperText={authError}
usernameLabel={i18n._(t`Username`)}
usernameValue={formik.values.username}
/>
)}
</Formik>
{loginInfoError && (
<AlertModal
isOpen={loginInfoError}
variant="error"
title={i18n._(t`Error!`)}
onClose={dismissLoginInfoError}
return (
<Login header={Header} footer={Footer}>
<LoginMainHeader
title={
brandName
? i18n._(t`Welcome to Ansible ${brandName}! Please Sign In.`)
: ''
}
/>
<LoginMainBody>
<Formik
initialValues={{
password: '',
username: '',
}}
onSubmit={handleSubmit}
>
{i18n._(
t`Failed to fetch custom login configuration settings. System defaults will be shown instead.`
{formik => (
<LoginForm
className={authError ? 'pf-m-error' : ''}
helperText={helperText}
isLoginButtonDisabled={isAuthenticating}
isValidPassword={!authError}
isValidUsername={!authError}
loginButtonLabel={i18n._(t`Log In`)}
onChangePassword={val => {
formik.setFieldValue('password', val);
dismissAuthError();
}}
onChangeUsername={val => {
formik.setFieldValue('username', val);
dismissAuthError();
}}
onLoginButtonClick={formik.handleSubmit}
passwordLabel={i18n._(t`Password`)}
passwordValue={formik.values.password}
showHelperText={authError}
usernameLabel={i18n._(t`Username`)}
usernameValue={formik.values.username}
/>
)}
<ErrorDetail error={loginInfoError} />
</AlertModal>
)}
</LoginPage>
</Formik>
{loginInfoError && (
<AlertModal
isOpen={loginInfoError}
variant="error"
title={i18n._(t`Error!`)}
onClose={dismissLoginInfoError}
>
{i18n._(
t`Failed to fetch custom login configuration settings. System defaults will be shown instead.`
)}
<ErrorDetail error={loginInfoError} />
</AlertModal>
)}
</LoginMainBody>
<LoginMainFooter
socialMediaLoginContent={
<>
{socialAuthOptions &&
Object.keys(socialAuthOptions).map(authKey => {
const loginUrl = socialAuthOptions[authKey].login_url;
if (authKey === 'azuread-oauth2') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with Azure AD`)}>
<AzureIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with GitHub`)}>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-org') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(t`Sign in with GitHub Organizations`)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-team') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with GitHub Teams`)}>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-enterprise') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(t`Sign in with GitHub Enterprise`)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-enterprise-org') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(
t`Sign in with GitHub Enterprise Organizations`
)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'github-enterprise-team') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={i18n._(
t`Sign in with GitHub Enterprise Teams`
)}
>
<GithubIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey === 'google-oauth2') {
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip content={i18n._(t`Sign in with Google`)}>
<GoogleIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
if (authKey.startsWith('saml')) {
const samlIDP = authKey.split(':')[1] || null;
return (
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
<Tooltip
content={
samlIDP
? i18n._(t`Sign in with SAML ${samlIDP}`)
: i18n._(t`Sign in with SAML`)
}
>
<UserCircleIcon />
</Tooltip>
</LoginMainFooterLinksItem>
);
}
return null;
})}
</>
}
/>
</Login>
);
}

View File

@ -24,7 +24,6 @@ describe('<Login />', () => {
async function findChildren(wrapper) {
const [
awxLogin,
loginPage,
loginForm,
usernameInput,
passwordInput,
@ -32,7 +31,6 @@ describe('<Login />', () => {
loginHeaderLogo,
] = await Promise.all([
waitForElement(wrapper, 'AWXLogin', el => el.length === 1),
waitForElement(wrapper, 'LoginPage', el => el.length === 1),
waitForElement(wrapper, 'LoginForm', el => el.length === 1),
waitForElement(
wrapper,
@ -49,7 +47,6 @@ describe('<Login />', () => {
]);
return {
awxLogin,
loginPage,
loginForm,
usernameInput,
passwordInput,
@ -61,7 +58,8 @@ describe('<Login />', () => {
beforeEach(() => {
RootAPI.read.mockResolvedValue({
data: {
custom_login_info: '',
custom_login_info:
'<div id="custom-button" onmouseover="alert()">TEST</div>',
custom_logo: 'images/foo.jpg',
},
});
@ -114,6 +112,16 @@ describe('<Login />', () => {
done();
});
test('custom login info handled correctly', async done => {
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => false} />);
});
await findChildren(wrapper);
expect(wrapper.find('footer').html()).toContain('<div>TEST</div>');
done();
});
test('data initialization error is properly handled', async done => {
RootAPI.read.mockRejectedValueOnce(
new Error({