From 3eec4c08a596db097c7ab2824610afb06a7c9ed9 Mon Sep 17 00:00:00 2001 From: Ray Lee Date: Thu, 21 Sep 2023 18:58:19 -0400 Subject: [PATCH] DRYD-1244: Add SSO support. (#190) --- package-lock.json | 4 +-- package.json | 2 +- .../service/PasswordResetRequestPage.jsx | 25 +++++++++----- .../pages/service/ServiceLoginPage.jsx | 32 +++++++++++++++++ src/plugins/recordTypes/account/fields.js | 34 +++++++++++++++++-- .../recordTypes/account/forms/default.jsx | 1 + src/service.jsx | 2 ++ styles/cspace-ui/ServicePage.css | 10 ++++-- 8 files changed, 93 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a3a47939..c75cb96c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cspace-ui", - "version": "9.0.0-dev.1", + "version": "9.0.0-dev.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cspace-ui", - "version": "9.0.0-dev.1", + "version": "9.0.0-dev.2", "license": "ECL-2.0", "dependencies": { "classnames": "^2.2.5", diff --git a/package.json b/package.json index 317d96b53..6b24e747b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cspace-ui", - "version": "9.0.0-dev.1", + "version": "9.0.0-dev.2", "description": "CollectionSpace user interface for browsers", "author": "Ray Lee ", "license": "ECL-2.0", diff --git a/src/components/pages/service/PasswordResetRequestPage.jsx b/src/components/pages/service/PasswordResetRequestPage.jsx index f26c8828d..d2213d2c3 100644 --- a/src/components/pages/service/PasswordResetRequestPage.jsx +++ b/src/components/pages/service/PasswordResetRequestPage.jsx @@ -15,50 +15,55 @@ import { isValidEmail } from '../../../helpers/validationHelpers'; const messages = defineMessages({ title: { - id: 'PasswordResetRequestPage.title', + id: 'passwordResetRequestPage.title', description: 'Title of the password reset request page.', defaultMessage: 'Reset Password', }, prompt: { - id: 'PasswordResetRequestPage.prompt', + id: 'passwordResetRequestPage.prompt', description: 'The prompt displayed on the password reset request page.', defaultMessage: 'Please enter your email address to request a password reset.', }, email: { - id: 'PasswordResetRequestPage.email', + id: 'passwordResetRequestPage.email', description: 'Label for the email field on the password reset request page.', defaultMessage: 'Email', }, submit: { - id: 'PasswordResetRequestPage.submit', + id: 'passwordResetRequestPage.submit', description: 'Label for the submit button on the password reset request page.', defaultMessage: 'Submit', }, success: { - id: 'PasswordResetRequestPage.success', + id: 'passwordResetRequestPage.success', description: 'Message displayed when a password reset has been successfully requested.', defaultMessage: 'An email has been sent to {email}. Follow the instructions in the email to finish resetting your password.', }, error: { - id: 'PasswordResetRequestPage.error', + id: 'passwordResetRequestPage.error', description: 'Generic message to display when a password reset request fails, and no more specific message is available.', defaultMessage: 'An error occurred while attempting to request the password reset: {detail}', }, errorNotFound: { - id: 'PasswordResetRequestPage.errorNotFound', + id: 'passwordResetRequestPage.errorNotFound', description: 'Message to display when the email is not found for a password reset request.', defaultMessage: 'Could not find an account with the email {email}.', }, errorMissingEmail: { - id: 'PasswordResetRequestPage.errorMissingEmail', + id: 'passwordResetRequestPage.errorMissingEmail', description: 'Message to display when no email is entered on the password reset request page.', defaultMessage: 'Please enter an email address.', }, errorInvalidEmail: { - id: 'PasswordResetRequestPage.errorInvalidEmail', + id: 'passwordResetRequestPage.errorInvalidEmail', description: 'Message to display when the email entered on the password reset request page is not a valid email address.', defaultMessage: '{email} is not a valid email address.', }, + errorSSORequired: { + id: 'passwordResetRequestPage.errorSSORequired', + description: 'Message to display on the password reset page when the account requires single sign-on.', + defaultMessage: '{email} is required to sign in using a single sign-on provider. The CollectionSpace account password cannot be reset.', + }, }); const propTypes = { @@ -137,6 +142,8 @@ function PasswordResetRequestPage(props) { if (response.status === 404) { setError(); + } else if (/requires single sign-on/.test(text)) { + setError(); } else { setError(); } diff --git a/src/components/pages/service/ServiceLoginPage.jsx b/src/components/pages/service/ServiceLoginPage.jsx index b74ab94f5..a1066e916 100644 --- a/src/components/pages/service/ServiceLoginPage.jsx +++ b/src/components/pages/service/ServiceLoginPage.jsx @@ -14,6 +14,8 @@ const propTypes = { error: PropTypes.string, isLogoutSuccess: PropTypes.bool, intl: intlShape.isRequired, + locale: PropTypes.string, + sso: PropTypes.object, tenantId: PropTypes.string, }; @@ -21,6 +23,8 @@ const defaultProps = { csrf: null, error: null, isLogoutSuccess: false, + locale: 'en-US', + sso: {}, tenantId: null, }; @@ -50,6 +54,10 @@ const messages = defineMessages({ description: 'Text of the forgot password link.', defaultMessage: 'Forgot password', }, + ssoLink: { + id: 'serviceLoginPage.ssoLink', + defaultMessage: 'Continue with {name}', + }, localLogin: { id: 'serviceLoginPage.localLogin', defaultMessage: 'Continue with email and password', @@ -70,9 +78,31 @@ function ServiceLoginPage(props) { error, isLogoutSuccess, intl, + locale, + sso, tenantId, } = props; + const ssoLinks = Object.entries(sso) + .sort((a, b) => a[1].name.localeCompare(b[1].name, locale, { sensitivity: 'base' })) + .map(([url, config]) => { + const { icon } = config; + + const style = icon + ? { backgroundImage: `url(${icon})` } + : undefined; + + return ( + + + + ); + }); + + const ssoPanel = (ssoLinks.length > 0) + ?
{ssoLinks}
+ : undefined; + const csrfInput = csrf ? : undefined; @@ -107,6 +137,8 @@ function ServiceLoginPage(props) {

+ {ssoPanel} +
{/* Ignore an eslint misfire. */} diff --git a/src/plugins/recordTypes/account/fields.js b/src/plugins/recordTypes/account/fields.js index 4ddf99930..f8dfa6f9a 100644 --- a/src/plugins/recordTypes/account/fields.js +++ b/src/plugins/recordTypes/account/fields.js @@ -20,6 +20,7 @@ const areRolesImmutable = ({ recordData }) => ( export default (configContext) => { const { + CheckboxInput, CompoundInput, OptionPickerInput, RolesInput, @@ -31,6 +32,10 @@ export default (configContext) => { configKey: config, } = configContext.configHelpers; + const { + DATA_TYPE_BOOL, + } = configContext.dataTypes; + return { 'ns2:accounts_common': { [config]: { @@ -142,6 +147,25 @@ export default (configContext) => { }, }, }, + requireSSO: { + [config]: { + cloneable: false, + dataType: DATA_TYPE_BOOL, + defaultValue: false, + messages: defineMessages({ + name: { + id: 'field.accounts_common.requireSSO.name', + defaultMessage: 'Require single sign-on (if available)', + }, + }), + view: { + type: CheckboxInput, + props: { + readOnly: isMetadataImmutable, + }, + }, + }, + }, password: { [config]: { cloneable: false, @@ -156,7 +180,10 @@ export default (configContext) => { defaultMessage: 'Password', }, }), - required: ({ recordData }) => isNewRecord(recordData), + required: ({ recordData }) => ( + isNewRecord(recordData) + && recordData.getIn(['ns2:accounts_common', 'requireSSO']) === false + ), validate: ({ data, fieldDescriptor }) => { if (data && !isValidPassword(data)) { return { @@ -185,7 +212,10 @@ export default (configContext) => { defaultMessage: 'Confirm password', }, }), - required: ({ recordData }) => isNewRecord(recordData), + required: ({ recordData }) => ( + isNewRecord(recordData) + && recordData.getIn(['ns2:accounts_common', 'requireSSO']) === false + ), view: { type: PasswordInput, props: { diff --git a/src/plugins/recordTypes/account/forms/default.jsx b/src/plugins/recordTypes/account/forms/default.jsx index cece811a8..2962b75ee 100644 --- a/src/plugins/recordTypes/account/forms/default.jsx +++ b/src/plugins/recordTypes/account/forms/default.jsx @@ -20,6 +20,7 @@ const template = (configContext) => { + diff --git a/src/service.jsx b/src/service.jsx index 72ee17312..e57c5ab7e 100644 --- a/src/service.jsx +++ b/src/service.jsx @@ -40,6 +40,7 @@ export default (uiConfig) => { locale, logo, messages, + sso, tenantId, token, } = config; @@ -84,6 +85,7 @@ export default (uiConfig) => { error={error} isLogoutSuccess={isLogoutSuccess} locale={locale} + sso={sso} tenantId={tenantId} /> )} diff --git a/styles/cspace-ui/ServicePage.css b/styles/cspace-ui/ServicePage.css index 407fdb54c..84c4d9cd2 100644 --- a/styles/cspace-ui/ServicePage.css +++ b/styles/cspace-ui/ServicePage.css @@ -30,7 +30,7 @@ body { margin-top: 8px; } -.common button { +.common :global(.sso) a, .common button { display: block; box-sizing: border-box; outline-offset: -1px; @@ -47,11 +47,11 @@ body { font: inherit; } -.common button:focus { +.common :global(.sso) a:focus, .common button:focus { outline: 2px solid textDark; } -.common button:enabled:hover { +.common :global(.sso) a:hover, .common button:enabled:hover { background-color: #fff; } @@ -71,6 +71,10 @@ body { background-image: url(../../images/lockReset.svg); } +.common :global(.sso) + form { + margin-top: 36px; +} + .common form { box-sizing: border-box; margin-top: 16px;