diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index d4ed859ea980..719d14ac6732 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -11098,6 +11098,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", @@ -11818,6 +11823,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", @@ -12551,6 +12561,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", @@ -15607,6 +15622,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", diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index 24371abfc791..2cfae063a57a 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -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": { diff --git a/awx/ui_next/src/screens/Login/Login.jsx b/awx/ui_next/src/screens/Login/Login.jsx index 8887d7351fe9..b0055b93945e 100644 --- a/awx/ui_next/src/screens/Login/Login.jsx +++ b/awx/ui_next/src/screens/Login/Login.jsx @@ -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 ( - - {socialAuthOptions && - Object.keys(socialAuthOptions).map(authKey => { - const loginUrl = socialAuthOptions[authKey].login_url; - if (authKey === 'azuread-oauth2') { - return ( - - - - - - ); - } - if (authKey === 'github') { - return ( - - - - - - ); - } - if (authKey === 'github-org') { - return ( - - - - - - ); - } - if (authKey === 'github-team') { - return ( - - - - - - ); - } - if (authKey === 'github-enterprise') { - return ( - - - - - - ); - } - if (authKey === 'github-enterprise-org') { - return ( - - - - - - ); - } - if (authKey === 'github-enterprise-team') { - return ( - - - - - - ); - } - if (authKey === 'google-oauth2') { - return ( - - - - - - ); - } - if (authKey.startsWith('saml')) { - const samlIDP = authKey.split(':')[1] || null; - return ( - - - - - - ); - } + const HeaderBrand = ; + const Header = ; + const Footer = ( + + ); - return null; - })} - - } - > - - {formik => ( - { - 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} - /> - )} - - {loginInfoError && ( - + + + - {i18n._( - t`Failed to fetch custom login configuration settings. System defaults will be shown instead.` + {formik => ( + { + 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} + /> )} - - - )} - + + {loginInfoError && ( + + {i18n._( + t`Failed to fetch custom login configuration settings. System defaults will be shown instead.` + )} + + + )} + + + {socialAuthOptions && + Object.keys(socialAuthOptions).map(authKey => { + const loginUrl = socialAuthOptions[authKey].login_url; + if (authKey === 'azuread-oauth2') { + return ( + + + + + + ); + } + if (authKey === 'github') { + return ( + + + + + + ); + } + if (authKey === 'github-org') { + return ( + + + + + + ); + } + if (authKey === 'github-team') { + return ( + + + + + + ); + } + if (authKey === 'github-enterprise') { + return ( + + + + + + ); + } + if (authKey === 'github-enterprise-org') { + return ( + + + + + + ); + } + if (authKey === 'github-enterprise-team') { + return ( + + + + + + ); + } + if (authKey === 'google-oauth2') { + return ( + + + + + + ); + } + if (authKey.startsWith('saml')) { + const samlIDP = authKey.split(':')[1] || null; + return ( + + + + + + ); + } + + return null; + })} + + } + /> + ); } diff --git a/awx/ui_next/src/screens/Login/Login.test.jsx b/awx/ui_next/src/screens/Login/Login.test.jsx index 5e9aa989096d..c5be9f12242a 100644 --- a/awx/ui_next/src/screens/Login/Login.test.jsx +++ b/awx/ui_next/src/screens/Login/Login.test.jsx @@ -24,7 +24,6 @@ describe('', () => { async function findChildren(wrapper) { const [ awxLogin, - loginPage, loginForm, usernameInput, passwordInput, @@ -32,7 +31,6 @@ describe('', () => { 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('', () => { ]); return { awxLogin, - loginPage, loginForm, usernameInput, passwordInput, @@ -61,7 +58,8 @@ describe('', () => { beforeEach(() => { RootAPI.read.mockResolvedValue({ data: { - custom_login_info: '', + custom_login_info: + '
TEST
', custom_logo: 'images/foo.jpg', }, }); @@ -114,6 +112,16 @@ describe('', () => { done(); }); + test('custom login info handled correctly', async done => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( false} />); + }); + await findChildren(wrapper); + expect(wrapper.find('footer').html()).toContain('
TEST
'); + done(); + }); + test('data initialization error is properly handled', async done => { RootAPI.read.mockRejectedValueOnce( new Error({